前端开发中,由于内容的缺失或者大小的不定,经常会碰到参差不齐的块块。那么如何优雅得展示这些内容,美化我们的产品呢?
通常来说,将不规整的内容规整化是最直观的解决方案。例如,对宽高比不同的图片放入到统一尺寸的容器中,如下述例图:
But,过于规整的限制反而约束了图片自身该有的魅力(许多图片都发生了形变)。同时,当图片的数量达到一定的量级,很容易给用户带来审美疲劳。
定义
接下来给大家介绍一下瀑布流布局,广泛应用于淘x、x东、小红x等各大主流移动端应用中。所谓瀑布流布局,便是指表现为参差不齐的多栏布局,即页面上的内容(如图片、文章摘要等)按照一定的规则排列,但每列的高度不固定。随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。————文心一言
直观来看,瀑布流布局的表现如下图
实现方式
1、纯CSS
使用column-count可以设定数据划分的列数,并通过column-gap设定列间距。该方式实现起来较为简洁,但在这种实现方式下,数据会按照左上、左中、左下、中上、中中、中下、右上、右中、右下的原则排列(以三列为例)。因此当有新数据增加时,总是会出现在右下的位置,同时也会导致旧数据的位置发生变化。(不太推荐)
<script setup>
// 图片列表路径
const imgList = [
'/src/assets/jpgs/01.webp'... // 其余数据省略
]
</script>
<template>
<div class="waterfall-width-column">
<div class="image-box" v-for="img in imgList" :key="img">
<img :src="img" :alt="img" style="margin-top: 12px" />
</div>
</div>
</template>
<style lang="scss" scoped>
.waterfall-width-column {
column-count: 3;
column-gap: 10px;
.image-box {
img {
display: block;
width: 100%;
}
}
}
</style>
2、Flex布局
首先,介绍该实现方式的思路。
- 根据设备分辨率大小,设定合适的列数以及列宽(可以添加resize事件进行更新);
- 为每个列设定高度记录器,记录现存的每列高度,同时为每列预留好要使用的数据源;
- 当需要插入新元素时,首先将使用该元素的宽高比按照列宽进行换算,计算出实际要展示的高度;
- 找出当前高度记录器中记录的高度最低的列索引,将该元素插入到该列数据源中,并更新其高度计数器;
<script lang="ts" setup>
import { onMounted, Ref, ref } from 'vue'
// 可配置参数
const props = defineProps({
marginRow: {
type: Number,
default: 16,
},
marginColum: {
type: Number,
default: 12,
},
minWidth: {
type: Number,
default: 200,
},
maxWidth: {
type: Number,
default: 300,
},
})
// 图片路径,通常以接口形式返回
const imgList = [
'/src/assets/jpgs/01.webp',
'/src/assets/jpgs/02.webp'... // 省略其他数据
]
// 图片宽高比,通常以接口形式返回
const widthAndHeight = [
[4, 3],
[102, 125]... // 省略其他数据
]
// 图片列表容器
const imgsContainer = ref()
// 图片自适应宽度
const imgWidth = ref(200)
// 列表列数
const imgColumn = ref(0)
// 每列图片的数组
const imgSplitList: Ref<any[]> = ref([])
// 高度纪录器
let heightRecord: any[] = []
const loaded = ref(false)
// 把鸡蛋放到篮子里
const loadImg = () => {
// 获取容器宽度
const containerWidth = imgsContainer.value.offsetWidth - 20
if (containerWidth <= props.minWidth * 2 + props.marginRow) {
// 容器宽度处于瀑布布局的下限
imgWidth.value = props.minWidth
imgColumn.value = 2
} else {
// 寻找合适的列数
let columNum = 1
let width: number
do {
columNum++
width = Math.floor(
(containerWidth - (columNum - 1) * props.marginColum) / columNum
)
} while (width >= props.maxWidth)
imgWidth.value = width
imgColumn.value = columNum
}
imgSplitList.value = []
for (let index = 0; index < imgColumn.value; index++) {
imgSplitList.value.push([])
}
// 初始化高度记录器
heightRecord = new Array(imgColumn.value).fill(0)
// 循环装载图片
imgList.forEach((_, index) => getOffest(index))
loaded.value = true
}
const getOffest = (index: number) => {
// 遍历目前纵向偏移最少的值 所在的索引
let yIndex = 0,
yOffest = heightRecord[0]
for (let i = 0; i < heightRecord.length; i++) {
if (heightRecord[i] < yOffest) {
yOffest = heightRecord[i]
yIndex = i
}
}
// 更新该列的纵向偏移 在原基础上添加图片展示的高度以及下外边距
heightRecord[yIndex] = parseInt(
yOffest + getImgHeight(index) + props.marginRow
)
imgSplitList.value[yIndex].push(imgList[index])
}
// 根据图片自身的宽高比,转化为实际应该展示的宽度
const getImgHeight = (index: number) => {
const [width, height] = widthAndHeight[index]
return (height * imgWidth.value) / width
}
onMounted(() => {
loadImg()
})
window.addEventListener('resize', loadImg)
</script>
<template>
<div
ref="imgsContainer"
style="
display: flex;
flex-direction: row;
overflow-y: auto;
padding-right: 7px;
"
>
<template v-if="loaded">
<div v-for="column in imgColumn" :key="column" style="flex: 1">
<img
v-for="item in imgSplitList[column - 1]"
:key="item"
:src="item"
:style="{ width: imgWidth + 'px' }"
/>
</div>
</template>
</div>
</template>
<style lang="scss" scoped></style>
3、绝对定位方式
绝对定位的实现逻辑与Flex布局的实现方式类似,均要保留高度记录器。所不同的是,插入元素时,需要额外手动计算元素的横向偏移量和纵向偏移量。在该方式下,我们可以将所有元素的绝对定位的top值和left值均设置为0,转而使用css3的transform的translate方法实现,这种方案可以借助GPU加速进行优化。
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
// 可配置参数
const props = defineProps({
marginRow: {
type: Number,
default: 16,
},
marginColum: {
type: Number,
default: 12,
},
minWidth: {
type: Number,
default: 200,
},
maxWidth: {
type: Number,
default: 300,
},
})
// 图片路径
const imgList = [
'/src/assets/jpgs/01.webp',
'/src/assets/jpgs/02.webp',
]
// 图片宽高比
const widthAndHeight = [
[4, 3],
[102, 125],
]
// 图片列表容器
const imgsContainer = ref()
// 图片自适应宽度
const imgWidth = ref(200)
// 列表列数
const imgColumn = ref(0)
// 高度纪录器
let heightRecord: any[] = []
const loaded = ref(false)
// 把鸡蛋放到篮子里
const loadImg = () => {
// 获取容器宽度
const containerWidth = imgsContainer.value.offsetWidth - 20
if (containerWidth <= props.minWidth * 2 + props.marginRow) {
// 容器宽度处于瀑布布局的下限
imgWidth.value = props.minWidth
} else {
// 寻找合适的列数
let columNum = 1
let width: number
do {
columNum++
width = Math.floor(
(containerWidth - (columNum - 1) * props.marginColum) / columNum
)
} while (width >= props.maxWidth)
imgWidth.value = width
imgColumn.value = columNum
// 初始化高度记录器
heightRecord = new Array(columNum).fill(0)
loaded.value = true
}
}
const getOffest = (index: number) => {
// 遍历目前纵向偏移最少的值 所在的索引
let yIndex = 0,
yOffest = heightRecord[0]
for (let i = 0; i < heightRecord.length; i++) {
if (heightRecord[i] < yOffest) {
yOffest = heightRecord[i]
yIndex = i
}
}
// 求取横向偏移
let xOffest = yIndex * (imgWidth.value + props.marginColum)
// 更新该列的纵向偏移 在原基础上添加图片展示的高度以及下外边距
heightRecord[yIndex] = parseInt(
yOffest + getImgHeight(index) + props.marginRow
)
return { transform: `translate(${xOffest}px, ${yOffest}px)` }
}
// 根据图片自身的宽高比,转化为实际应该展示的宽度
const getImgHeight = (index: number) => {
const [width, height] = widthAndHeight[index]
return (height * imgWidth.value) / width
}
onMounted(() => {
loadImg()
})
window.addEventListener('resize', loadImg)
</script>
<template>
<div
ref="imgsContainer"
style="position: relative; overflow-y: auto; padding-right: 7px"
>
<template v-if="loaded">
<template v-for="(item, index) in imgList" :key="item">
<img
:src="item"
:style="{
position: 'absolute',
left: 0,
top: 0,
...getOffest(index),
width: `${imgWidth}px`,
'object-fit': 'contain',
}"
/>
</template>
</template>
</div>
</template>
<style lang="scss" scoped></style>
4、Flex与绝对定位实现方式的差异
虽然前述介绍了Flex的实现原理与绝对定位实现方式很相似,但两者仍存在着一个本质的不同:Flex方式是先设定好每个元素所在的列和顺序后再展示,**即所有元素计算完成后再渲染。而绝对定位方式是在渲染时再计算该元素应该放置的位置。**可以对比两者代码中高度计数器更新时机,便可以分析得出该结论。
5、最终效果
这里是原作者哦