Skip to content

瀑布流梳理

js 实现思路

  • 先排列好第一行的内容(i<this.cols或者heightsArr.length<this.cols),并用数组「heightsArr」记录每一列的高度;
  • 接下来排列剩余行时,找到高度和最低的那一列,并记录列的索引「minHeightColIndex」;
  • 将新的那一个数据项「newItem」放置在「minHeightColIndex」那一列的下方(通过 style.left,style.top);
  • 然后更新「minHeightColIndex」列的高度(heightsArr[minHeightColIndex]+newItem.height+rowGap);
  • 直到所有项遍历结束。

js 优化思路

本地获取图片再得到图片宽高信息,效率较低,会出现白屏,体验较差。

完整 JS 代码

js
/**
 * 方案思路
 *
 * 基本实现:
 * 1.先排列好第一行的内容,并用数组「heightsArr」记录每一列的高度;
 * 2.接下来排列剩余行时,找到高度最低的那一列,并记录列的索引「minHeightColIndex」;
 * 3.将新的那一个数据项「newItem」放置在「minHeightColIndex」那一列(通过style.left),
 * 并更新「minHeightColIndex」列的高度(heightsArr[minHeightColIndex]+newItem.height+rowGap);
 *
 * 参考链接:https://zhuanlan.zhihu.com/p/55575862
 *
 * 优化:
 * 本地获取图片再得到图片宽高信息,效率较低,会出现白屏,体验较差。
 * 优化思路1:服务端直接返回图片宽高信息;
 * 优化思路2:[web端]从云存储服务器获取图片时加上?imageInfo,可直接返回图片信息,速度很快。
 * 例如七牛:https://img.fatiaoya.com/bdperson/1670316442405_%E4%BD%99%E5%A7%9A%E8%B7%AF169%E5%8F%B7.png?imageInfo
 *
 * 参考链接:https://juejin.cn/post/6844903998625972238
 */

/**
 * 「瀑布流」主要方法如下:
 * mutilLoader 用于批量处理
 *
 * singleLoader 用于单个处理
 *    return {
 *              width(图片原始宽),
 *              height(图片原始高),
 *              imgRatioWidth(图片等比缩放宽),
 *              imgRatioHeight(图片等比缩放高),
 *              blockWidth(整块宽),
 *              blockHeight(整块高),
 *              primaryColor(图片主色调,默认#f5f5f5),
 *              textColor(根据主色调的「亮度」,获取文字颜色[黑|白])
 *    }
 *
 * getMaxHeight 返回最大列高,可用于动态修改页面瀑布流外层容器的高度,给loading预留位置
 *
 * reset 用于重置瀑布没列的高度信息
 */
export class Waterfall {
  cols;
  rowGap;
  colGap;
  colWidth;
  heightsArr = []; //每一列所有项的高度和
  minHeightColIndex = 0; // 高度最低的那一列的索引
  hasServerImageInfo = false;
  isWXMP = true;

  /**
   * 参数说明
   * @param options
   * @param options.pageWidth 页面宽度
   * @param options.cols 需要展示多少列
   * @param options.colGap 列间隙
   * @param options.rowGap 行间隙
   * @param options.addonHeight 附加高度(比如有文字介绍)
   * @param options.hasServerImageInfo 服务端是否返回图片信息,若是,则本地无需再通过其他方式加载图片获取图片的宽高信息了
   */
  constructor({
    pageWidth,
    cols,
    colGap,
    rowGap,
    addonHeight,
    hasServerImageInfo = false,
    isWXMP = true,
  }) {
    this.cols = cols;
    this.rowGap = rowGap;
    this.colGap = colGap;
    this.addonHeight = addonHeight;
    this.hasServerImageInfo = hasServerImageInfo;
    this.isWXMP = isWXMP;
    // colWidth => [screenWidth = cols * colwidth + 3 * colGap] = (screenWidth - 3*colGap)/cols
    this.colWidth = (pageWidth - (cols + 1) * colGap) / cols;
    console.log("列数,列宽: ", this.cols, this.colWidth);
  }

  /**
   * 多项加载
   * @param {*} list
   */
  async mutilLoader(list = []) {
    for (let i = 0; i < list.length; i++) {
      let info = await this.singleLoader(list[i]);
      list[i] = {
        ...info,
      };
    }
    return [...list];
  }

  /**
   * 单项加载
   * @param {*} data
   */
  async singleLoader(data) {
    let item = {
      ...data,
    };

    // step1:处理基本信息
    let imgInfo = {
      width: 0,
      height: 0,
      primaryColor: "#f5f5f5",
    };

    if (this.hasServerImageInfo) {
      imgInfo = {
        width: item.width || 260,
        height: item.height || 320,
        primaryColor: item.main_color,
      };
    } else {
      let res = {};
      if (this.isWXMP) {
        res = await this.wxGetImageInfo(item.thumb_img);
      } else {
        res = await this.webGetImageInfo(item.thumb_img);
      }
      imgInfo = {
        ...imgInfo,
        ...res,
      };
    }

    const { width, height, primaryColor } = imgInfo;
    let ratioHeight = this.colWidth * (height / width); //等比缩放的高度 height/width = h2/colWidth
    item.imgRatioHeight = ratioHeight; // 等比例缩放后的图片高度
    item.blockHeight = ratioHeight + this.addonHeight; //实际高度=等比例缩放的高度+附加高度(如展示title)
    item.imgRatioWidth = this.colWidth;
    item.blockWidth = this.colWidth;
    item.primaryColor = primaryColor; //图片主色调
    item.textColor = this.getContrastColor(primaryColor); //根据主色调的亮度,设置文字颜色[黑|白]

    // step2:装载入列,计算位置
    let i = this.heightsArr.length;
    if (i < this.cols) {
      // 第一行,记录列高
      this.heightsArr.push(item.blockHeight);
      item.left = i * this.colWidth + this.colGap * (i + 1);
      item.top = 0;
    } else {
      // 剩余行,寻找整列高最小的列加入
      let minHeight = Math.min(...this.heightsArr);
      let minHeightIndex = this.heightsArr.indexOf(minHeight);
      // console.log(this.heightsArr, minHeightIndex, minHeight, item.id)
      item.left =
        minHeightIndex * this.colWidth + this.colGap * (minHeightIndex + 1);
      item.top = this.heightsArr[minHeightIndex] + this.rowGap;

      // 更新这里列的高度
      this.heightsArr[minHeightIndex] = item.top + item.blockHeight;
    }

    return item;
  }

  /**
   * 获取最大列高
   * 可用于动态修改页面瀑布流外层容器的高度,给loading预留位置
   */
  getMaxHeight() {
    return Math.max(...this.heightsArr);
  }

  /**
   * 重置高度记录,适用于列表重载
   */
  reset() {
    this.heightsArr = [];
  }

  /**小程序端 */
  wxGetImageInfo(src) {
    return new Promise((resolve) => {
      wx.getImageInfo({
        src: src,
        success: (res) => {
          resolve(res);
        },
        fail(err) {
          console.log(err);
        },
      });
    });
  }

  /**web端 */
  webGetImageInfo(src) {
    return new Promise((resolve, reject) => {
      let img = new Image();
      img.crossOrigin = "Anonymous"; //解决跨域图片问题
      img.onload = (e) => {
        resolve({
          img,
          width: img.naturalWidth,
          height: img.naturalHeight,
          whRatio: (img.naturalWidth / img.naturalHeight).toFixed(2) * 1,
          hwRatio: (img.naturalHeight / img.naturalWidth).toFixed(2) * 1,
        });
      };
      img.onerror = (err) => {
        reject(err);
      };
      img.src = src;
    });
  }

  /**获取相对色 */
  getContrastColor(rgbstr) {
    if (!rgbstr || !rgbstr.length) {
      return "";
    }

    let rgb = "";
    if (rgbstr.includes("rgb")) {
      rgb = rgbstr.replace(/rgb|\(|\)/g, "");
    }
    rgb = rgb.split(",");

    let yuv = this.rgb2yuv(rgb[0], rgb[1], rgb[2]);
    return yuv >= 128 ? "rgb(0,0,0)" : "rgb(255,255,255)";
  }

  /**rgb转亮度yuv */
  rgb2yuv(r = 0, g = 0, b = 0) {
    return r * 0.299 + g * 0.578 + b * 0.114;
  }
}

在微信小程序中使用

html
<import src="/templates/listLoadingIcon" />
<nav-menus menus="{{menus}}" bind:change="onMenuChange"></nav-menus>

<!-- 设置外层容器高度,给loading预留位置 -->
<view class="wrap" style="height: {{pageHeight}}px;">
  <view
    class="item"
    wx:for="{{listFetchConfig.data}}"
    style="left:{{item.left}}px;top:{{item.top}}px;width:{{item.blockWidth}}px;height:{{item.blockHeight}}px;background-color: {{item.primaryColor}};"
    key="index"
    bindtap="goMaterialDetail"
    data-video="{{item}}"
  >
    <!-- !!!这里需要手动设置图片的宽高,如果是mode='xxx'自适应的写法,会出现加载新数据时页面抖动问题。 -->
    <image
      src="{{item.thumb_img}}"
      style="width:{{item.imgRatioWidth}}px;height:{{item.imgRatioHeight}}px;background-color: {{item.primaryColor}};"
    >
    </image>
    <view class="title t13 t-2" style="color:{{item.textColor}};"
      >{{item.title}}</view
    >
    <view wx:if="{{item.is_recommend}}" class="recommend">荐</view>
  </view>

  <!-- loading位置计算 -->
  <view class="f-r-c-c" style="position: absolute; top: {{pageHeight-60}}px;">
    <template
      is="list_loading_icon"
      data="{{loading:listFetchConfig.isLoading}}"
    ></template>
  </view>
</view>
js
const { screenWidth } = wx.getSystemInfoSync();

let wf = new Waterfall({
  pageWidth: screenWidth,
  cols: 2,
  colGap: 7.5,
  rowGap: 7.5,
  addonHeight: 60,
  hasServerImageInfo: true
});

// 单个计算加载
let info = await wf.singleLoader(item);

// 获取最大列高
this.setData({
  pageHeight: wf.getMaxHeight(),
});

// 重置瀑布流列高信息
wf.reset();

css 实现

flex 实现

html
<div class="wrap">
  <div class="item" style="height:400px;">
    <h4>1</h4>
    <img />
  </div>
  <div class="item" style="height:200px;">
    <h4>2</h4>
    <img />
  </div>
  <div class="item" style="height:300px;">
    <h4>3</h4>
    <img />
  </div>
</div>
css
.wrap {
  column-count: 2;
  column-gap: 20rpx;
  position: relative;
}

.item {
  position: absolute;
  box-sizing: border-box;
  width: calc(50vw - 10rpx);
  border: 1px solid #999;
  margin-bottom: 10px;
  /* 元素是否可以被中断:渲染完再换行 */
  break-inside: avoid;
  background-color: cadetblue;
  border-radius: 30rpx;
}

image {
  width: 100%;
  border-radius: 30rpx;
}

参考链接

css 瀑布流

js 瀑布流

Released under the MIT License.