瀑布流布局

整个瀑布流组件的构建大体需要分成几部分

  1. 通过 props 传递关键数据
    • data:数据源
    • nodeKey:唯一标识
    • column:渲染的列数
    • columnSpacing:列间距
    • rowSpacing:行间距
    • picturePreReading:是否需要图片预渲染
  2. 瀑布流渲染机制:通过 absolute 配合 relative 完成布局,布局逻辑为:每个 item 应该横向排列,第二行的 item 顺序连接到当前最短的列中。
  3. 通过作用域插槽 将每个 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) }`

参考资料