动态排列重组瀑布流
思路
核心:利用定位来进行瀑布流排布
<!-- 项目已提前引入了 tailwindcss -->
<div class="container flex mx-auto">
<div class="relative w-full water-fall-wrapper" ref="waterFallWrapper"></div>
</div>
随机生成 num 个瀑布流元素
const createManyItem = (waterFallWrapperElement: HTMLDivElement, num: number) => {
for (let i = 0; i < num; i++) {
const divElem = document.createElement('div')
divElem.className = 'absolute'
// 随机高度
divElem.style.height = `${Math.floor(300 * Math.random())}px`
// 随机背景色
divElem.style.backgroundColor = `rgb(${Math.floor(256 * Math.random())}, ${Math.floor(256 * Math.random())}, ${Math.floor(256 * Math.random())})`
// 过渡动画
divElem.style.transition = 'all 1s'
waterFallWrapperElement.appendChild(divElem)
}
}
生成数组来记录瀑布流每列的高度
// 长度为 列数 的数组
let fallLineHeightList: number[] = new Array(colNum).fill(0)
关键步骤,排布瀑布流元素逻辑
// 排列瀑布流元素
const arrangeWaterFallElem = (waterFallWrapperElement: HTMLDivElement) => {
waterFallWrapperElement.childNodes.forEach((itemElement) => {
// 每个 water-fall 元素
const divItemElem = itemElement as HTMLDivElement
// 找到瀑布流最矮的那一列,也就是我们要插入的那一列
// lowestInfo.height 最矮那列的高度
// lowestInfo.index 最矮那列的索引
const lowestInfo = findLowestFallLine()
const currentElemHeight = divItemElem.offsetHeight
const currentElemWidth = waterFallWrapperElement.clientWidth / colNum
// 更新元素的宽度和定位
divItemElem.style.width = `${currentElemWidth}px`
divItemElem.style.top = `${lowestInfo.height}px`
divItemElem.style.left = `${currentElemWidth * (lowestInfo.index % waterFallAttr.colNum)}px`
// 更新瀑布流每列高度
fallLineHeightList[lowestInfo.index] += currentElemHeight
})
}
其中 找到瀑布流最矮的那一列 的函数实现是
// 找到最低的瀑布流
const findLowestFallLine = (): iLowestInfo => {
const lowestInfo: iLowestInfo = { index: 0, height: Infinity }
fallLineHeightList.forEach((fallLineHeight, index) => {
if (fallLineHeight < lowestInfo.height) {
lowestInfo.index = index
lowestInfo.height = fallLineHeight
}
})
return lowestInfo
}
interface iLowestInfo {
index: number
height: number
}
至此,一个简易的瀑布流主要逻辑就完成了!
优化:
加上监听页面宽度变化,重新计算列数,元素位置,并提供了元素之间的gap数值
文末贴上vue3的代码实现:
<script setup lang="ts">
import { debounce } from '@/utils/utils'
import { onMounted, ref } from 'vue'
interface iLowestInfo {
index: number
height: number
}
const waterFallWrapper = ref(null)
// 窗口宽度对应瀑布流列数
const screenWidthMap = new Map([
[[0, 640], 2],
[[640, 768], 3],
[[768, 1024], 4],
[[1024, 1280], 6],
[[1280, 1536], 8],
[[1536, Infinity], 8]
])
// 瀑布流属性
const waterFallAttr = {
containerWidth: 0, // 容器宽度
colNum: 0, // 列数
colGap: 10, // 列gap
rowGap: 10 // 行gap
}
// 瀑布流高度 list
let fallLineHeightList: number[] = new Array(waterFallAttr.colNum).fill(0)
// 找到最低的瀑布流
const findLowestFallLine = (): iLowestInfo => {
const lowestInfo: iLowestInfo = { index: 0, height: Infinity }
fallLineHeightList.forEach((fallLineHeight, index) => {
if (fallLineHeight < lowestInfo.height) {
lowestInfo.index = index
lowestInfo.height = fallLineHeight
}
})
return lowestInfo
}
// 生成 num 个瀑布流元素
const createManyItem = (waterFallWrapperElement: HTMLDivElement, num: number) => {
for (let i = 0; i < num; i++) {
const divElem = document.createElement('div')
divElem.className = 'absolute'
divElem.style.height = `${Math.floor(300 * Math.random())}px`
divElem.style.backgroundColor = `rgb(${Math.floor(256 * Math.random())}, ${Math.floor(
256 * Math.random()
)}, ${Math.floor(256 * Math.random())})`
divElem.style.transition = 'all 1s'
waterFallWrapperElement.appendChild(divElem)
}
}
// 监听 容器宽度 的变化
const viewObserver = (waterFallWrapperElement: HTMLDivElement) => {
window.addEventListener(
'resize',
// 防抖一下,节省性能
debounce(() => {
if (waterFallWrapperElement.clientWidth !== waterFallAttr.containerWidth) {
waterFallAttr.containerWidth = waterFallWrapperElement.clientWidth
calcColNum(waterFallWrapperElement)
}
}, 100)
)
}
// 计算合适的排布 并 排列瀑布流元素
const calcColNum = (waterFallWrapperElement: HTMLDivElement) => {
for (let v of screenWidthMap.entries()) {
let beforeColNum = waterFallAttr.colNum
if (
waterFallAttr.containerWidth > v[0][0] &&
waterFallAttr.containerWidth <= v[0][1] &&
beforeColNum !== v[1]
) {
waterFallAttr.colNum = v[1]
arrangeWaterFallElem(waterFallWrapperElement)
break
}
}
}
// 排列瀑布流元素
const arrangeWaterFallElem = (waterFallWrapperElement: HTMLDivElement) => {
fallLineHeightList = new Array(waterFallAttr.colNum).fill(0)
waterFallWrapperElement.childNodes.forEach((itemElement) => {
// 每个 water-fall 元素
const divItemElem = itemElement as HTMLDivElement
const lowestInfo = findLowestFallLine()
const currentElemHeight = divItemElem.offsetHeight
const currentElemWidth =
waterFallWrapperElement.clientWidth / waterFallAttr.colNum -
((waterFallAttr.colNum - 1) / waterFallAttr.colNum) * waterFallAttr.colGap
divItemElem.style.width = `${currentElemWidth}px`
divItemElem.style.top = `${lowestInfo.height}px`
divItemElem.style.left = `${
(currentElemWidth + waterFallAttr.colGap) *
(lowestInfo.index % waterFallAttr.colNum)
}px`
fallLineHeightList[lowestInfo.index] += currentElemHeight + waterFallAttr.rowGap
})
}
onMounted(() => {
const waterFallWrapperElement = waterFallWrapper.value as unknown as HTMLDivElement
waterFallAttr.containerWidth = waterFallWrapperElement.clientWidth
createManyItem(waterFallWrapperElement, 200)
viewObserver(waterFallWrapperElement)
calcColNum(waterFallWrapperElement)
})
</script>
<template>
<div class="container flex mx-auto">
<div class="relative w-full water-fall-wrapper" ref="waterFallWrapper"></div>
</div>
</template>