前言
在需要大批量展示图片的场景中,通常的做法是卡片展示对应图片,这样的卡片宽高固定,图片会跟着卡片进行一些适配,比如使用contain来让图片能够完整的展示出来,这样展示的图片视觉上看起来不太统一。
瀑布流效果,指的是多图展示时,图片能够保留原比例多列平均展示,视觉观感上每一列就像倾泻下的流水般,尤其是配合滚动加载效果,更像是源源不断的水流一顷而下。因为图片都能完整的展示,所以不会有割裂感。
瀑布流效果如今也不是一个新的展示效果,大都是使用绝对定位,通过计算图片的位置来实现,这次要介绍的是使用flex布局来实现,不需要大量的计算位置信息,同时达到图片自适应的效果。
准备
在开始之前,先要了解两个知识点:
flex布局padding-bottom来自动设置元素高度。
因为图片会以多列的方式进行展示,所以flex-direction: column可以完美的处理这个情况。同时在flex的加持下,原本计算位置信息需要考虑的间距问题也可以完美的规避掉。当然flex布局唯一需要考虑的一点就是兼容性了,不过当前主流浏览器中都不需要担心这个问题。
为什么padding-bottom可以自动设置元素高度呢,如果告知padding设置成百分比后会根据元素的宽度进行自动计算的话,那么问题就不告而解了。
不过还有一点需要注意的是,我们在进行布局排版时需要提前知道图片的宽高信息,因为需要图片的比例进行padding-bottom的设置,多数情况下在获取图片信息的时候,服务端能够返回图片的宽高。加入没有返回的话,那么在开始之前可能需要先计算一下,比如使用new Image()来获取。
flex可以参考 阮一峰的网络日志 - Flex 布局教程:语法篇
开始
数据结构
先设定基本的数据结构
// 列数据
const columnData = [
{
totalHeight: 0, // 每一列的高度,用于新增图片时计算最低高度的列进行填充
items: [ // 图片信息
{
// ...otherinfo
ratio: 0, // 图片 高度/宽度,用于设置padding-bottom打到自适应高度的效果
}
]
},
// ...
];
视图结构
<template>
<div class="wrapper">
<div
class="column"
v-for="({ items }, index) of columnData"
:key="`${index}_${keyPrefix}`"
:style="{
width: `${100 / column}%`,
marginRight: `${index + 1 < column ? gap : 0}px`
}"
>
<div
class="column-item"
v-for="({ ratio, color }, k) of items"
:key="k"
:style="{
background: color,
marginBottom: `${gap}px`,
paddingBottom: `${ratio * 100}%`,
}"
>
</div>
</div>
</div>
</template>
wrapper就是瀑布流的整个容器,column则是其中的每一列,我们要做的就是填充里面的column-item,在设定padding-bottom后,column-item的高度就会通过自身的宽度进行动态计算,但是我们没有直接给div设置宽度,所以会使用父级的宽度来进行计算。
接来下是样式的设置
.wrapper {
display: flex;
}
.column {
display: flex;
flex-direction: column;
}
只需要给wrapper和column设置样式即可,wrapper不一定需要使用flex,但是column是一定需要的。如果wrapper不是flex的话,可以调整column为display: inline-block,不过需要注意HTML中给column设置了margin,如果不处理的话一行是不能显示出所有列的。
控制器
最后到了最关键的js环节,通过定义好的数据结构,可以知道,我们的判断只和列数和图片信息有关,因此可以定义一个Waterfall类
export default class Waterfall {
/**
* @param {num} column 列数
*/
constructor(column) {
this.column = column;
this.columnData = [];
}
init(column) {
this.column = column;
// 根据列数初始化columnData
for (let i = 0; i < this.column; i++) {
this.columnData[i] = {
totalHeight: 0,
items: [],
};
}
}
}
完成初始化后,接下来就到了关键的计算部分
update(column, data) {
this.init(column);
for (const item of data) {
const { width, height } = item;
item.ratio = height / width;
// 找到columnData中高度最小的那一项
const minColumn = this.columnData.reduce(
(a, b) => (b.totalHeight < a.totalHeight ? b : a),
this.columnData[0],
);
minColumn.items.push(item);
minColumn.totalHeight += item.ratio;
}
return this.columnData;
}
可以发现功能非常简单,就是找到高度最低的那一列,填入当前的元素,然后更新高度。
不过可以注意到一点的是,totalHeight保存的不是具体的高度,而是高宽比:
minColumn.totalHeight += item.ratio;
这么做的目的其实和直接保存高度的效果是一样的,因为高度的计算为:columnWidth * ratio,columnWidth为每一列的宽度,在多张图片时总高度为:columnWidth * ratio1 + columnWidth * ratio2 + columnWidth * ratio3
可以发现columnWidth在每次计算中都会使用到,所以提取公共项为:columnWidth * (ratio1 + ratio2 + ratio3),又列宽可以认作是一个常量,因为不会跟随的数据变化而变化(即是在resize后宽度变了,但是总的高度根据比例还是可以直接算出),所以此时只需要关心ratio的总值即可。
回到我们的组件中,在查询到数据后调用update方法获取最新的columnData,然后根据列信息进行渲染
export default {
setup() {
const searchedData = getData(200);
const state = reactive({
columnData: [],
gap: 16,
column: 6,
keyPrefix: 0,
});
const waterfall = new WaterFall();
const update = () => {
const columnData = waterfall.update(state.column, searchedData);
state.columnData = columnData;
state.keyPrefix += 1;
};
update();
return {
...toRefs(state),
};
},
};
总结
在使用flex布局后,瀑布流效果在弹性布局的加持下减轻了大量的DOM元素位置判断和计算,同时在移动端也能有很好的支持,因此在update()中可以很存粹的进行高度的计算。
如果有根据屏幕宽度动态调整列数的需求,可以在resize中重新调用update,即可在新的column下进行重新计算。
除了flex,grid布局实现也会更容易哦。