瀑布流布局
实现思路
首先我们先介绍一下瀑布流布局实现的思路
- 控制容器内每一列卡片的宽度相同(不同图片尺寸等比例缩放)
- 第一行卡片紧挨着排列,第二行开始,每张卡片摆放到当前所有列中高度最小的一列下面
组件结构
首先展示一下整个DOM的结构
<template>
<div class="waterfall-container" ref="containerRef" @scroll="handleScroll">
<div class="waterfall-list" ref="listRef">
<div
class="waterfall-item"
v-for="(item, index) in state.cardList"
:key="item.id"
:style="{
width: `${state.cardWidth}px`,
transform: `translate3d(${state.cardPos[index].x}px, ${state.cardPos[index].y}px, 0)`,
}"
>
<slot
name="item"
:item="item"
:index="index"
:imageHeight="state.cardPos[index].imageHeight"
></slot>
</div>
</div>
</div>
</template>
主要的结构就是waterfall-container
,waterfall-list
,waterfall-item
container
作为整个瀑布流的容器,在它上面展示滚动条, list
作为 item
的容器可以开启相对定位,而 item
开启绝对定位,通过 translate 来控制每张卡片的位置实现最后的布局,所以每张卡片定位统一放到左上角即可
<style scoped>
.waterfall-container {
width: 100%;
height: 100vh;
overflow-y: scroll;
overflow-x: hidden;
}
.waterfall-list {
width: 100%;
position: relative;
}
.waterfall-item {
position: absolute;
left: 0;
top: 0;
box-sizing: border-box;
transition: all 0.2s;
}
</style>
对于这里用到的数据如下:
const props = defineProps({
//用于拿到数据
request: {
type: Function,
required: true // 确保父组件必须提供一个函数
},
//列信息
column: {
type: Number,
default: 4
},
//间距
gap: {
type: Number,
default: 10
},
//页码,通过这个来获取信息
pageSize: {
type: Number,
default: 20
},
//触底加载新数据
bottom: {
type: Number,
default: 20
}
})
const state = ref({
isFinish: false,
loading: false,
page: 1, //数据的页码
cardWidth: 0, //卡片的宽度
cardList: [], //数据源
cardPos: [], //卡片的位置信息
columnHeight: new Array(props.column).fill(0), //列高度信息
preLen: 0
})
函数封装
接下来就是进行计算每张卡片的具体位置,为最后的布局做准备
//计算出高度最小的那一列的索引和高度
const minColumn = computed(() => {
let minIndex = -1,
minHeight = Infinity
state.value.columnHeight.forEach((item, index) => {
if (item < minHeight) {
minHeight = item
minIndex = index
}
})
return {
minIndex,
minHeight
}
})
这里通过遍历存储每列高度信息的数组,返回高度最小的那一列的索引和高度
//根据页码得到数据
const getCardList = async (page, pageSize) => {
if (state.value.isFinish) {
return
}
state.value.loading = true
const list = await props.request(page, pageSize)
state.value.page++
if (!list.length) {
state.value.isFinish = true
return
}
state.value.cardList = [...state.value.cardList, ...list]
computedCardPos(list)
state.value.loading = false
}
封装获取数据的函数,通过页码来获得新数据
//计算卡片的图片的高度
const computedImageHeight = list => {
list.forEach(item => {
const imageHeight = Math.floor(
(item.height * state.value.cardWidth) / item.width
)
state.value.cardPos.push({
width: state.value.cardWidth,
imageHeight: imageHeight,
cardHeight: 0,
x: 0,
y: 0
})
})
}
这里通过给定的图片宽高信息,和每一列的宽度,进而计算出每张图片缩放之后的高度
//根据真实dom计算出卡片的具体位置
const computedRealDomPos = list => {
const children = listRef.value.children
list.forEach((item, index) => {
const nextIndex = state.value.preLen + index
const cardHeight = children[nextIndex].getBoundingClientRect().height
if (index < props.column && state.value.cardList.length <= props.pageSize) {
state.value.cardPos[nextIndex] = {
...state.value.cardPos[nextIndex],
cardHeight: cardHeight,
x:
nextIndex % props.column !== 0
? nextIndex * (state.value.cardWidth + props.gap)
: 0,
y: 0
}
state.value.columnHeight[nextIndex] = cardHeight + props.gap
} else {
const { minIndex, minHeight } = minColumn.value
state.value.cardPos[nextIndex] = {
...state.value.cardPos[nextIndex],
cardHeight: cardHeight,
x: minIndex ? minIndex * (state.value.cardWidth + props.gap) : 0,
y: minHeight
}
state.value.columnHeight[minIndex] += cardHeight + props.gap
}
})
state.value.preLen = state.value.cardPos.length
}
这个函数则是要计算每张卡片的具体偏移量,做法是对于传入的数据数组,利用记录列高度数据的数组,计算每张卡片的水平和垂直偏移量,再存入数组中
//计算每个卡片的位置
const computedCardPos = async list => {
computedImageHeight(list)
await nextTick()
computedRealDomPos(list)
}
计算每个卡片的位置
//初始化,根据列数计算每列卡片的宽度
const init = () => {
if (containerRef.value) {
const containerWidth = containerRef.value.clientWidth
state.value.cardWidth =
(containerWidth - props.gap * (props.column - 1)) / props.column
getCardList(state.value.page, props.pageSize)
resizeObserver.observe(containerRef.value)
}
}
onMounted(() => {
init()
})
初始化函数