小程序瀑布流布局的实现方案

1. 瀑布流布局

瀑布流分为横向和纵向,前者是每个元素高度固定,宽度参差不齐;后者是宽度相对固定,高度不一。
虽然展现的效果不太一样,但实现原理差不多,大同小异,以下内容主要基于实际项目遇到的情况来总结的

2. 纵向瀑布流

宽度固定的瀑布流可能有两类情况:

  • 一种是初始化时拿到所有数据渲染, 全程渲染一次,不在乎排列顺序;
  • 一种是注重排列顺序或者懒加载的,例如滚动到底部加载新数据;

2.1 一次性渲染所有元素

CSS中column-count可以设置内容展示的列数,可以通过设置column-count: 2将子元素分两列展示,从而实现简单的瀑布流布局

column-count的具体使用方法可以看张鑫旭大佬的博客⬇️
https://www.zhangxinxu.com/wordpress/2019/01/css-css3-columns-layout/

页面布局代码大体如下:

// wxml
<view>
  <view class="layout-column">
    <view wx:for="{{list}}" wx:index="index" wx:item="item" wx:key="index" class="card">
      <view>
        <text class="card-title">卡片{{index + 1}}</text>
        <view class="card-img">我是一张图</view>
        <text class="card-name">卡片内容内容很多内容</text>
      </view>
    </view>
  </view>
</view>
// wxss
.layout-column {
  column-count: 2;
  padding: 32rpx;
}
.card {
  break-inside: avoid;
  border: 1rpx solid #dfdfdf;
  padding: 16rpx;
  margin-bottom: 16rpx;
  border-radius: 6rpx;
}
 
.card-title,
.card-name {
  line-height: 2em;
  color: #232323;
  font-size: 32rpx;
}
 
.card-img {
  height: 120rpx;
  background: pink;
  color: #eee;
  text-align: center;
  line-height: 120rpx;
}
 
.card-name {
  font-size: 24rpx;
}

效果如下:

注意注意:

  • 子元素里可以设置属性break-inside; 表示的是断行方式,因为css会让两列的高度差尽可能的小,也就是说当前列剩余空间不足以存放下一个元素时,这个元素可能会被切分;
    设置break-inside: avoid以后,当前列剩余空间不足时也不会对该元素切分,确保子元素连贯展示;

  • 由上图可以看出,column-count实现的瀑布流中,元素的排列顺序会从左倒右,从上到下依次插入;
    所以当数据一次性渲染且对顺序没有要求的情况下,这个方法是比较快速方便的;
    但是像商品列表,对商品的展示顺序由要求,并且商品数据是动态加载的,这个方法就行不通咯😂 实现方法可参照下一节

2.2 动态加载新数据

通过column-count实现的瀑布流会出现新数据总是在最右侧的情况,在一些情况下,这样是不行的🙅

如下常见的用来展示图片的瀑布流,新加载出来的图片追加到底部

以上瀑布流实现思路:

  • wxml中分为左右两列布局,每一列用flex布局。
  • 图片数据对应分成两个数组存放,从接口拿到新的数据后,按照一定的算法依次插入到对应的数组中。
  • 维护两列的高度值,当拿到的图片信息包含了分辨率的时候,可以计算目标图片高度,插入到当前较短的一列对应的数组。如果拿不到图片的分辨率,可以在页面上先把图片加载出来,通过bindload事件拿到图片的分辨率信息。

以图片分辨率未知为例,主要实现代码如下:

<!-- wxml -->
<view class="layout-flex">
  <view>
    // 多包裹一层为了更准备的获取到当前列的实际高度
    <view class="left-goods">
      <view wx:for="{{imgListLeft}}" wx:index="index" wx:item="item" wx:key="index" class="card-flex">
          <image class="image-item" mode="widthFix" src="{{item.url}}"></image>
      </view>
    </view>
  </view>
  <view>
    <view class="right-goods">
      <view wx:for="{{imgListRight}}" wx:index="index" wx:item="item" wx:key="index" class="card-flex">
        <image class="image-item" mode="widthFix" src="{{item.url}}" ></image>
      </view>
    </view>
  </view>
</view>
<!-- 用一个不可见的元素包裹所有图片,便于获得图片分辨率信息 -- >
<view style="display: none">
  <image wx:for="{{allList}}" wx:key="index" wx:item="item" src="{{item.url}}" id="index{{index}}" mode="widthFix" style="width: 320rpx" bindload="handleLoad"></image>
</view>

计算图片高度和插入图片

Page({
  data: {
    allList,
    imgListLeft: [], // 左列数组
    imgListRight: [], // 右列数组
    leftHeight: 0, // 左列高度
    rightHeight: 0, // 右列高度
    realWidth: 1, // 图片在wxml中的实际高度
  },
 
  onLoad() {
    // 获取图片在wxml中的实际宽度,近似值即可,不要求准确
    wx.getSystemInfo({
      success: (result) => {
        const { screenWidth } = result;
        const realWidth = 320 * screenWidth / 750;
        this.setData({
          realWidth
        })
      },
    })
  },
  // 目标图片加载成功后触发,用于获取图片高度,并计算出wxml中的实际高度,判断应该插入的数组
  handleLoad(event) {
    // wxml中设置的id, 便于获取图片全部信息
    let indexStr = event.currentTarget.id.match(/index(\d+)/)[1] || '';
    const index = Number(indexStr);
 
    const { height, width } = event.detail;
    const { leftHeight, rightHeight, imgListLeft, imgListRight, allList, realWidth } = this.data;
    // 目标图片插入较短的一列对应的数组中
    if (leftHeight <= rightHeight) {
      this.setData({
        leftHeight: leftHeight + height * (realWidth / width),
        imgListLeft: [...imgListLeft, { ...allList[index] }]
      })
    } else {
      this.setData({
        rightHeight: rightHeight + height * (realWidth / width),
        imgListRight: [...imgListRight,{ ...allList[index] }]
      })
    }
  }
})

然而,常见的商品列表中,每个商品的图片分辨率虽然固定,但是商品名称和营销标签都不同,导致每个商品的卡片高度是不确定(最起码在渲染到dom前是不确定的),每个商品卡片高度差不会太大。

拆分算法稍作更改,分为两步:

  1. 对于前n-1条数据,拆分前,先获取当前dom中两列商品列表的高度;优先从较短的一列对应的数组开始依次插入数据;
  2. 第n条数据时,先获取当前页面的两列商品高度,将其插入较短一列的数组中;

页面主要布局同上,获取商品列表高度的核心方法如下:

// 封装一个Behavior,在组件里通过设置behaviors: [basic]属性来使用
export const basic = Behavior({
    methods: {
        getRect(selector, all) {
            return new Promise(resolve => {
                wx.createSelectorQuery()
                    .in(this)[all ? 'selectAll' : 'select'](selector)
                    .boundingClientRect(rect => {
                    if (all && Array.isArray(rect) && rect.length) {
                        resolve(rect);
                    }
                    if (!all && rect) {
                        resolve(rect);
                    }
                })
                    .exec();
            });
        }
    }
});
 
// 获取元素高度
this.getRect(select).then(res => {
// 高度
    console.log(res.height);
})

效果:

总结

由于瀑布流两列的高度参差不齐的特性,必须把布局分为多列,数据也分为多份来管理。

为了子元素排列精准一些,就需要不断的统计比较两列的的高度,把新的元素插入到当前较短的一列中。

在PC端和H5中,Masonry是目前比较好用的,流畅美观的瀑布流插件,使用方法也比较简单,按照规则写html标签加简单的配置即可 ➡️ https://github.com/desandro/masonry

以上是根据实际业务情况整理出的瀑布流实现方案,瀑布流的大体实现思路是差不多的。

©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页