瀑布流效果实现

328 阅读4分钟

前言

在需要大批量展示图片的场景中,通常的做法是卡片展示对应图片,这样的卡片宽高固定,图片会跟着卡片进行一些适配,比如使用contain来让图片能够完整的展示出来,这样展示的图片视觉上看起来不太统一。

瀑布流效果,指的是多图展示时,图片能够保留原比例多列平均展示,视觉观感上每一列就像倾泻下的流水般,尤其是配合滚动加载效果,更像是源源不断的水流一顷而下。因为图片都能完整的展示,所以不会有割裂感。

瀑布流效果如今也不是一个新的展示效果,大都是使用绝对定位,通过计算图片的位置来实现,这次要介绍的是使用flex布局来实现,不需要大量的计算位置信息,同时达到图片自适应的效果。

准备

在开始之前,先要了解两个知识点:

  1. flex布局
  2. 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布局实现也会更容易哦。