基本原理
- 通过元素宽度和容器宽度计算出一排的数量
cols - 通过
cols的长度生成一个posY集合[0, 0, ....] - 遍历所有节点
- 获取
posY集合的最小值minY作为节点的y轴偏移,记录minY的minYIndex - 通过
minYIndex以及元素的宽度计算出节点的curX值最为节点的x轴偏移 - 更新
posY集合的最小值,原始高度minY加上节点的高度 - 循环结束后将
posY集合的最大值maxY设置为容器的高度
实现
模版样式
<div ref="waterfallWrapper" :style="{ height: `${wrapperHeight}px` }">
<div v-for="item in list" :key="item.id" class="waterfall-item">
<img :src="item.url">
</div>
</div>
将元素绝对定位在 (0, 0) 点,通过 tranlate 实现布局
.waterfall-item {
position: absolute;
left: 0;
top: 0;
}
.waterfall-item img {
display: black;
}
图片数据
生成了30张大小不一致的图片
const list = ref([])
for (let i = 1; i < 30; i++) {
list.value.push({
url: `https://images.weserv.nl/?url=https://api.mz-moe.cn/img/img${i}.jpg`,
})
}
容器高宽
const waterfallWrapper = ref(null)
const wrapperWidth = ref(0)
const wrapperHeight = ref(0)
onMounted(() => {
wrapperWidth.value = waterfallWrapper.value.offsetWidth
})
列与偏移
// 间隙
const gutter = ref(20)
// 单列宽
const colWidth = ref(300)
// 列数
const cols = computed(() => {
return Math.floor((wrapperWidth.value - gutter.value) / (colWidth.value + gutter.value))
})
// x偏移
const offsetX = computed(() => {
const contextWidth = cols.value * (colWidth.value + gutter.value) + gutter.value
return (wrapperWidth.value - contextWidth) / 2
})
核心排版
function layout() {
// 初始化 y 集合
const posY = new Array(cols.value).fill(gutter.value)
// 节点
const items = document.querySelectorAll('.waterfall-item')
// 遍历
for (let i = 0; i < items.length; i++) {
const curItem = items[i]
// 最小的y值
const minY = Math.min.apply(null, posY)
// 最小y的下标
const minYIndex = posY.indexOf(minY)
// 当前下标对应的x
const curX = gutter.value * (minYIndex + 1) + colWidth.value * minYIndex + offsetX.value
// 设置偏移
curItem.style.transform = `translate3d(${curX}px,${minY}px, 0)`
curItem.style.width = `${colWidth.value}px`
// 更新当前index的y值
const { height } = curItem.getBoundingClientRect()
posY[minYIndex] += height + gutter.value
}
// 设置容器高度
wrapperHeight.value = Math.max.apply(null, posY)
}
onMounted(() => {
layout()
})
图片高度
如果元素里面有图片 img,由于遍历节点的时候图片可能还未加载完成,无法正确的获取到节点的高度,为此我们需要在图片加载完成后重新执行 layout()
给 img 添加上 @load 方法
<div ref="waterfallWrapper" :style="{ height: `${wrapperHeight}px` }">
<div v-for="item in list" :key="item.id" class="waterfall-item">
<img :src="item.url" @load="imageLoad">
</div>
</div>
可以加上防抖避免段时间内多张图片加载完成触发多次重排
const imageLoad = debounce(() => {
layout()
}, 1000)
这样简单的瀑布流就实现了,效果如下:
完善的瀑布流插件
支持animate.css动画,响应式,图片懒加载