小程序瀑布流的实践

VisionM

前言:最近在工作中,实践了下在微信小程序中实现瀑布流列表(左右两栏,动态图文),最终的效果还不错,所以在此记录,仅供有需要的人参考。

最终的效果:代码片段https://developers.weixin.qq....
补充说明的是,要做瀑布流,最好是可以知道图片的高度,由接口下发,来提前占位,否则,即使是原生app,也会因为图片的加载闪屏使得不好的用户体验。在小程序中,没有原生app的流式layout控件,所以不知道图片宽高的情况下,只能减少每一页的page_size,在加载完图片之后,再计算插入的位置(对产品来说,这可能是一个不可接受的漫长过程)

尝试过的小程序瀑布流实现方式

1.左右两列判断奇偶性渲染

//demo.js

Page({
    data:{
        renderLists:[]
    },
    fetchData(){
        request(url,params).then(res=>{
            const PreData=this.data.renderLists
            this.setData({
                renderLists:PreData.concat(res)
            })
        })
    }
})
//demo.wxml
<view class="waterfall">
    <view class="waterfall__left">
        <your-compoent 
            wx:for="{{renderLists}}"
            wx:key="id"
            wx:if="{{index % 2 === 0}}"
        />
    </view>
    <view class="waterfall__right">
        <your-compoent 
            wx:for="{{renderLists}}"
            wx:key="id"
            wx:if="{{index % 2 === 1}}"
        />
    </view>
</view>
说明 优点 缺点
左右列重复wx:for 渲染数据,根据index索引的奇偶性来wx:if 简单 1.重复for循环。2.如果每个item高度相差较大,很容易造成左右矮的那一栏是部分空白的
或许京东购物小程序-首页就是这样做的?(不确定)

afa61e56-df0f-48bf-8113-737cf8147928.jpg

2.绝对定位

这个方式可能就是目前pc端的实现方式,原理都一样。但是在小程序中,有以下几个需要解决的问题

  1. 如果列表是动态图文,即使知道图片宽高,如何获取文字内容区域的高度?
  2. 安卓下实验过会出现白屏,卡顿等性能问题(这个问题导致直接放弃了这个做法)

3.左右两列计算插入渲染

//demo.js

Page({
    data:{
        leftLists:[],
        rightLists:[]
    },
    onLoad(){
        this.leftHeight=0
        this.rightHeight=0
    },
    fetchData(){
        request(url,params).then(res=>{
            this.generate(res)
        })
    },
    generate(list){
        let leftList=[],rightList=[]
        list.map(async (item)=>{
            //每个item的高度=图片宽高+内容区域
            const itemHeight=getImageHeight(item)+getContentHeight(item)+gap
            this.leftHeight>this.rightHeight?rightList.push[item]:leftList.push(item)
        })
        this.render({leftList,rightList})
    },
    getImageHeight(){
        //如果知道图片宽高,就return item.height
        //如果不知道,wx.getImageInfo(需要配合域名)或者通过display:"node" image 然后 bindload。
    },
    getContentHeight(){
        //文字内容区域高度固定,直接返回
        //不固定,做数据映射,例如10个字一行,行高按照设计稿,做rpxToPx返回
    },
    render(params){
        const {leftList,rightList}=params
        const preLeft=this.data.leftList;
        const preRight=this.data.rightList
        this.setData({
            leftList:preLeft.concat(leftList),
            rightList:preRight.concat(rightList)
        },()=>{
             const query = this.createSelectorQuery(); 
       query.select('.wrapper__left').boundingClientRect();
      query.select('.wrapper__right').boundingClientRect(); 
           query.exec((res) => {
             this.leftHeight = res[0].height; 
             this.rightHeight = res[1].height;
           });
        })
    }
})
//demo.wxml
<view class="waterfall">
    <view class="waterfall__left">
        <your-compoent 
            wx:for="{{leftLists}}"
            wx:key="id"
        />
    </view>
    <view class="waterfall__right">
        <your-compoent 
            wx:for="{{rightLists}}"
            wx:key="id"
        />
    </view>
</view>
说明 优点 缺点
下面↓↓↓ 性能好,实现的效果好 (⊙o⊙)…
  1. 每次数据加载回来,例如一次15条,做遍历注意下面的操作都必须是一个同步获取的过程,需要异步获取的都要async await,如果需要异步获取的数据多,例如图片,那就是一个耗时操作
  2. 获取图片宽高与文字高度。如果不知道图片宽高,就要使用wx.getImageInfo(需要域名的配置)。如果想随便哪找一张图都能瀑布流,那就需要额外的操作,先将图片列表setData去渲染,设置样式为display:none,那它就不会显示,但是会加载图片,通过image bindload,拿到所有图片宽高后,匹配url做一个mapping,一一对应图片和宽高。
  3. 文字内容区域,如果不是固定高度。那我们根据接口返回的字符长度做一个大概的高度映射就行了,例如10个字一行。可能有小概率会影响我们的计算,这个问题是存在的且可以接受的。而且在第4步操作之后,不会影响下一轮。
  4. 最后,每次setData渲染完之后,我们再通过createSelectorQuery获取左右两栏的真实高度,更新一下,为下一次渲染准备
可以看下小程序附近的餐厅,效果很好,他的图片宽高是服务器返回的,文字内容区域是固定的(标题只有几个字,也是两行)

94a63658-ea3d-4828-832e-4fee0d09990e.jpg

4.wx.createIntersectionObserver

waterfall1.png

//waterfall.wxml
<view class="waterfall" style="padding:0 {{gap}}rpx;">
  <view class="waterfall__left waterfall__view" data-nextposition="right" style="width:calc(50% - {{gap/2}}rpx);margin-right:{{gap/2}}rpx">
    <layout wx:for="{{leftList}}" wx:key="index" info="{{item}}"/>
  </view>
  <view class="waterfall__right waterfall__view" data-nextposition="left" style="width:calc(50% - {{gap/2}}rpx);margin-left:{{gap/2}}rpx">
    <layout wx:for="{{rightList}}" wx:key="index" info="{{item}}"/>
  </view>
  <view class="waterfall__observer"></view>
</view>

//waterfall.less
.waterfall {
  display: block;
  position: relative;
  background: #F6F8FA;
  overflow: hidden;

  &__left {
    float: left;
  }

  &__right {
    float: right;
    min-height: 100rpx;
  }

  &__observer {
    position: absolute;
    bottom: 10px;
    left: 0;
    background: red;
    width: 100%;
    height: 1px;
    opacity:0;
  }
}
//waterfall.json

{
  "component": true,
  "componentGenerics": {
    "layout": true
  }
}

//waterfall.js
Component({
  properties: {
    gap: {
      type: Number,
      value: 10,
    }
  },
  data: {
    leftList: [],
    rightList: [],
  },
  attached() {
    this._list = []
    this._leftIndex = 0
    this._rightIndex = 0
  },
  methods: {
    /**
     * public
     *
     * @param {*} list
     */
    render(list) {
      this._list = list
      this._updateWaterfall()
    },
    /**
     * public
     *
     */
    reset() {
      this.setData({
        leftList: [],
        rightList: []
      })
      this._leftIndex = 0
      this._rightIndex = 0
    },
    _updateWaterfall(list) {
      if (this._list.length) {
        // console.log("右:", this._rightIndex, this.data.rightList.length, "左:", this._leftIndex, this.data.leftList.length)
        const item = this._list.shift()
        this._createObserver().then(pos => {
          const updateIndex = this[`_${pos}Index`]
          this.setData({
            [`${pos}List[${updateIndex}]`]: item,
          }, () => {
            this[`_${pos}Index`] += 1
            this._updateWaterfall()
          })
        })
      }
    },
    _createObserver() {
      return new Promise(resolve => {
        this._observer && this._observer.disconnect()
        this._observer = this.createIntersectionObserver({
          observeAll: true,
        })
        this._observer
          .relativeTo('.waterfall__observer')
          .observe('.waterfall__view', ({ dataset: { nextposition = "" } }) => {
            // console.log(nextposition)
            resolve(nextposition)
          })
      })
    },

  },
});

我们可以循环列表,通过微信的IntersectionObserver判断左右两栏与底部细线的相交状态(细节根据最外层wrapper绝对定位)。得出两栏高矮,判断下一个item的插入位置。这是一个同步的过程,不断的setData,但是他的性能比wx.createSelectorQuery()好。并且我们通过索引来更新data。通过抽象节点,还可以实现抽离出组件以复用

代码片段 https://developers.weixin.qq....

结论

目前来说,第三、四种方案的实现效果最好,也是我们正在线上使用的方式,推荐使用。

相关文章

小程序movable-view拖动排序这里

阅读 2.3k
829 声望
14 粉丝
0 条评论
你知道吗?

829 声望
14 粉丝
文章目录
宣传栏