瀑布流原理
瀑布流,即多行等宽元素排列,后面的元素依次添加到其后,等宽不等高,根据图片原比例缩放宽高;
瀑布流布局特点,从第2行开始,接下来的每一张图片都会放在现有列中高度最低的那一列,如下图:

再继续排列下去,第6张图片应该放在第1列,以此类推,如下图:

所以每次排列一张图片时,就需要判断一次现有列中累计高度最小的列,下一张图片就排在哪一列,即瀑布流算法去判断图片的确定位置;
实现思路
目前常见瀑布流实现都采用定位布局+js计算位置(left,top)方式,搭配后端返回图片真实宽高,实现起来方便,效果更好;
- 设定列数,列间距,根据容器宽度计算出每列宽度;
- 定义行间距;
- 遍历元数据,生成最终瀑布流list;
- 根据列宽,图片宽高计算每一项的列高;
- 定义列高数组保存每次排列后的每列高度;
- 第1行特殊处理,每一项top:0,left:(列宽 + 列间距)* 当前遍历index,同时保存每列高度;
- 非第1行时,获取当前列高最低列index及最低列高,每一项top:最小列高 + 行间距,left:列宽 + 列间距)* 最低列index,更新最低列列高(最小列高 + 行间距 + 当前项列高);
- 计算容器总高度,获取瀑布流list中top值最大的一项,top + 列高即为容器总高度;
实现代码
1、设定列数,列间距,根据容器宽度计算出每列宽度;
@Prop({ type: Number, default: 2 }) private readonly columnNum?: number; // 列数
@Prop({ type: Number, default: 10 }) private readonly columnSpan?: number; // 列间距,只有列与列之间才有间距,每行第一列的左边距/最后一列的右边距由外部容器控制
private columnWidth = 0; // 列宽
mounted() {
// 获取容器宽度
const containerWidth = this.$el.clientWidth;
const columnNum = this.columnNum as number;
const columnSpan = this.columnSpan as number;
// 获取列宽度
this.columnWidth =
(containerWidth - (columnNum - 1) * columnSpan) / columnNum;
}2、定义行间距;
@Prop({ type: Number, default: 20 }) private readonly rowSpan?: number; // 行间距,只有行与行之间才有间距,第一行的上边距/最后一行的下边距由外部容器控制3、遍历元数据,生成最终瀑布流list;
@Prop({ type: Array, required: true }) private readonly list?: Item[]; // 瀑布流数据
private containerHeight = 0; // 容器高度
private get waterfallFlowList(): WaterfallFlowItem[] | undefined {
const columnWidth = this.columnWidth;
if (columnWidth === 0) return [];
const columnHeightList: number[] = [];
const columnNum = this.columnNum as number;
const columnSpan = this.columnSpan as number;
const rowSpan = this.rowSpan as number;
const list = this.list?.map((i, d) => {
// 得到每一个item的列高
const columnHeight = (columnWidth * i.height) / i.width;
const item = { ...i, columnHeight: columnHeight, top: 0, left: 0 };
if (d < columnNum) {
item.left = (columnWidth + columnSpan) * d;
columnHeightList.push(columnHeight);
} else {
// 获取最小高度
const minColumnHeight = Math.min(...columnHeightList);
const minColumnHeightIndex = columnHeightList.findIndex(
i => i === minColumnHeight
);
item.left = (columnWidth + columnSpan) * minColumnHeightIndex;
item.top = minColumnHeight + rowSpan;
columnHeightList[minColumnHeightIndex] =
minColumnHeight + rowSpan + columnHeight;
}
return item;
});
// 获取容器高度
const maxTop = Math.max(...(list?.map(i => i.top) || []));
const maxItem = list?.find(i => i.top === maxTop);
if (maxItem) this.containerHeight = maxTop + maxItem.columnHeight;
return list;
}最终效果图

完整代码
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
@Component({ name: "WaterfallFlow" })
export default class WaterfallFlow extends Vue {
@Prop({ type: Number, default: 2 }) private readonly columnNum?: number; // 列数
@Prop({ type: Number, default: 10 }) private readonly columnSpan?: number; // 列间距,只有列与列之间才有间距,每行第一列的左边距/最后一列的右边距由外部容器控制
@Prop({ type: Number, default: 20 }) private readonly rowSpan?: number; // 行间距,只有行与行之间才有间距,第一行的上边距/最后一行的下边距由外部容器控制
@Prop({ type: Array, required: true }) private readonly list?: Item[]; // 瀑布流数据
private columnWidth = 0; // 列宽
private containerHeight = 0; // 容器高度
private get waterfallFlowList(): WaterfallFlowItem[] | undefined {
const columnWidth = this.columnWidth;
if (columnWidth === 0) return [];
const columnHeightList: number[] = [];
const columnNum = this.columnNum as number;
const columnSpan = this.columnSpan as number;
const rowSpan = this.rowSpan as number;
const list = this.list?.map((i, d) => {
// 得到每一个item的列高
const columnHeight = (columnWidth * i.height) / i.width;
const item = { ...i, columnHeight: columnHeight, top: 0, left: 0 };
if (d < columnNum) {
item.left = (columnWidth + columnSpan) * d;
columnHeightList.push(columnHeight);
} else {
// 获取最小高度
const minColumnHeight = Math.min(...columnHeightList);
const minColumnHeightIndex = columnHeightList.findIndex(
i => i === minColumnHeight
);
item.left = (columnWidth + columnSpan) * minColumnHeightIndex;
item.top = minColumnHeight + rowSpan;
columnHeightList[minColumnHeightIndex] =
minColumnHeight + rowSpan + columnHeight;
}
return item;
});
// 获取容器高度
const maxTop = Math.max(...(list?.map(i => i.top) || []));
const maxItem = list?.find(i => i.top === maxTop);
if (maxItem) this.containerHeight = maxTop + maxItem.columnHeight;
return list;
}
mounted() {
// 获取容器宽度
const containerWidth = this.$el.clientWidth;
const columnNum = this.columnNum as number;
const columnSpan = this.columnSpan as number;
// 获取列宽度
this.columnWidth =
(containerWidth - (columnNum - 1) * columnSpan) / columnNum;
}
}
interface Item {
id: number;
width: number;
height: number;
}
interface WaterfallFlowItem extends Item {
columnHeight: number;
top: number;
left: number;
}
</script>
<template>
<div class="waterfall-flow" :style="{ height: `${containerHeight}px` }">
<div
class="waterfall-flow-item"
v-for="(i, d) of waterfallFlowList"
:key="i.id"
:style="{
left: `${i.left}px`,
top: `${i.top}px`,
width: `${columnWidth}px`,
// eslint-disable-next-line prettier/prettier
height: `${i.columnHeight}px`,
}"
>
<slot :row="i" :index="d"></slot>
</div>
</div>
</template>
<style lang="less">
.waterfall-flow {
position: relative;
.waterfall-flow-item {
position: absolute;
}
}
</style>调用例子
<waterfall-flow :list="list":columnNum="4" style="width: 345px; margin: 10px auto 0">
<div class="home-waterfall-flow-item" slot-scope="{ index }">
{{ index + 1 }}
<!-- <img class="home-waterfall-flow-image" :src="row.url" alt="" /> -->
</div>
</waterfall-flow>
版权声明:本文为weixin_38115427原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。