瀑布流布局
整个瀑布流组件的构建大体需要分成几部分
- 通过 props 传递关键数据
- data:数据源
- nodeKey:唯一标识
- column:渲染的列数
- columnSpacing:列间距
- rowSpacing:行间距
- picturePreReading:是否需要图片预渲染
- 瀑布流渲染机制:通过 absolute 配合 relative 完成布局,布局逻辑为:每个 item 应该横向排列,第二行的 item 顺序连接到当前最短的列中。
- 通过作用域插槽 将每个 item 中涉及到的关键数据,传递到 item 视图中。
计算每列宽度
计算大体方法就是,拿到容器宽度(不包括 margin,padding,border),
const useContainerWidth = () => {
const { paddingLeft, paddingRight } = getComputedStyle(
containerRef.value,
null
);
// 容器左边距
containerLeft.value = parseFloat(paddingLeft);
// 容器宽度
containerWidth.value =
containerRef.value.offsetWidth -
parseFloat(paddingLeft) -
parseFloat(paddingRight);
};并且获取容器中每个 item 元素的总间距。
// 列间距总大小 (column - 1) * columnSpacing
const columnSpacingTotal = computed(() => {
return (props.column - 1) * props.columnSpacing;
});然后用当前容器减去总间距,再除以列数。
const useColumnWidth = () => {
// 获取容器宽度
useContainerWidth();
// 获取列宽
columnWidth.value =
(containerWidth.value - columnSpacingTotal.value) / props.column;
};获取每个元素的高度
图片是否定义了高度,如果定义高度,可以直接计算出每个 item 的高度
const useItemHeight = () => {
// 初始化item高度列表
let itemsHeight = []
// 获取 item 元素
const itemElements = […document.getElementsByClassName('hm-waterfall-item')]
// 获取item高度
itemElements.forEach((itemEl) => {
itemsHeight.push(itemEl.offsetHeight)
})
// 渲染位置
useItemLocation()
}如果未定义高度,我们需要在图片加载完成后,才能计算高度。
- 获取 item 元素
- 获取 itm 元素中图片路径
/**
* 获取所有item中img元素
*/
export function getImgElements(itemElements) {
const imgElements = []
itemElements.forEach((el) => {
imgElements.push(…el.getElementsByTagName('img'))
})
return imgElements
}
/**
* 获取所有图片路径
*/
export function getAllImgSrc(imgElements) {
const allImgSrc = []
imgElements.forEach((item) => {
allImgSrc.push(item.getAttribute('src'))
})
return allImgSrc
}- 通过 image 对象的 load 事件来判断图片是否加载完毕,然后计算高度。
export function allImgComplete(allImgSrc) {
// 存放所有图片加载的promise对象
const promises = [];
// 循环allImgSrc
allImgSrc.forEach((imgSrc, index) => {
promises.push(
new Promise((resolve) => {
const imgObj = new Image();
imgObj.src = imgSrc;
imgObj.onload = () => {
resolve({ imgSrc, index });
};
})
);
});
return Promise.all(promises);
}const waitImgComplete = () => {
// 初始化item高度列表
itemsHeight = []
// 获取 item 元素
const itemElements = […document.getElementsByClassName('hm-waterfall-item')]
// 获取所有元素的 img 标签
const imgElements = getImgElements(itemElements)
// 获取所有 img 图片路径
const allImgSrc = getAllImgSrc(imgElements)
// 计算图片预加载,然后计算高度
allImgComplete(allImgSrc).then(() => {
itemElements.forEach((itemEl) => {
itemsHeight.push(itemEl.offsetHeight)
})
})
// 渲染位置
useItemLocation()
}计算每个元素的偏移量
都是通过获取列最小高度基础上计算的一些值。
需要先将每列高度初始化为 0,使用该对象作为容器,key 为列下标,值为列高度。
// 容器的总高度
const containerHeight = ref(0);
// 记录每列高度的容器。key:所在列 val:列高
const columnHeightObj = ref({});
/**
* 构建记录各列的高度的对象。初始化都为0
*/
const useColumnHeightObj = () => {
columnHeightObj.value = {};
for (let i = 0; i < props.column; i++) {
columnHeightObj.value[i] = 0;
}
};获取 left 偏移量时,我们需要拿到最小高度列。
/**
* 获取最小高度
*/
export function getMinHeight(columnHeightObj) {
const columnHeightValue = Object.values(columnHeightObj)
return Math.min(…columnHeightValue)
}
/**
* 获取最小高度的column
*/
export function getMinHeightColumn(columnHeightObj) {
// 获取最小高度
const minHeight = getMinHeight(columnHeightObj)
const columns = Object.keys(columnHeightObj)
const minHeightColumn = columns.find((col) => {
return columnHeightObj[col] === minHeight
})
return minHeightColumn
}获取最小高度列后,直接乘以列宽和加上间距就行
/**
* 计算当前元素的left偏移量
*/
const getItemLeft = () => {
// 获取最小高度的列
const column = getMinHeightColumn(columnHeightObj.value);
// 计算left
return (
(columnWidth.value + props.columnSpacing) * column + containerLeft.value
);
};top 偏移量的计算,我们可以直接拿到最小高度列高就行
/**
* 计算当前元素的top偏移量
*/
const getItemTop = () => {
// 获取列最小高度
const minHeight = getMinHeight(columnHeightObj.value);
return minHeight;
};需要注意的是,我们在完成每次元素偏移量赋值的时候,都需要将最小高度列重新计算高度。
/**
* 重新计算最小高度列高度
*/
const increasingHeight = (index) => {
// 获取最小高度的列
const column = getMinHeightColumn(columnHeightObj.value);
// 该列高度重新计算
columnHeightObj.value[column] =
columnHeightObj.value[column] + itemsHeight[index] + props.rowSpacing;
};最后将最大高度列高度赋值给容器高度即可。
// 渲染位置
const useItemLocation = () => { props.data.forEach((item, index) => {
// 避免重复计算
if (item._style) return
// 拿到最小高度,计算_style中的left, top item._style = {} item._style.left = getItemLeft() item._style.top = getItemTop() // 每次设置完偏移量时,都需要更改最短列的高度。 increasingHeight(index) }) // 当所有item设置好偏移量时,将容器高度设置为列最高的高度 containerHeight.value = getMaxHeight(columnHeightObj.value) }`