vue简单实现瀑布流

瀑布流原理

瀑布流,即多行等宽元素排列,后面的元素依次添加到其后,等宽不等高,根据图片原比例缩放宽高;

瀑布流布局特点,从第2行开始,接下来的每一张图片都会放在现有列中高度最低的那一列,如下图:

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

所以每次排列一张图片时,就需要判断一次现有列中累计高度最小的列,下一张图片就排在哪一列,即瀑布流算法去判断图片的确定位置;

实现思路

目前常见瀑布流实现都采用定位布局+js计算位置(left,top)方式,搭配后端返回图片真实宽高,实现起来方便,效果更好;

  1. 设定列数,列间距,根据容器宽度计算出每列宽度;
  2. 定义行间距;
  3. 遍历元数据,生成最终瀑布流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版权协议,转载请附上原文出处链接和本声明。