小程序简单两栏瀑布流效果

发布于 6 年前作者 fang131612 次浏览最后编辑 6 年前来自 share

瀑布流又称瀑布流式布局,是比较流行的一种网站页面布局方式。视觉表现为参差不齐的多栏布局,即多行等宽元素排列,后面的元素依次添加到其后,等宽不等高,根据图片原比例缩放直至宽度达到我们的要求,依次放入到高度最低的那一栏。

先上代码:https://developers.weixin.qq.com/s/Fgm5s1mz7Wdm

所谓简单,是指只考虑图片,图片之外的其他元素高度固定,不在考虑范围内。

说一下基本的实现思路:
1、加载列表数据
2、在一个隐藏的view中加载图片,通过image组件的bindload获取图片的实际宽高并存储
3、等所有图片加载完成后遍历列表,将图片插入到高度低的那一栏,同时更新该栏高度

我也考虑过在第二步bindload获取到宽高后就直接插入到栏位中,但是会出现小的图片先加载完先出现到页面中,虽然瀑布流不是普通的列表那样的排序,但是也不能小的图片在上面这样太乱顺序,所以就改成了获取宽高先存储,等所有图片加载完成后再往页面上渲染。

来看看实际的代码

不需要渲染到wxml中的数据,我放到了jsData中,主要是两栏的高度和是否在加载数据的标记。
tempPics是第一次加载的数据,临时存放,用于加载图片宽高
columns是两个栏位的实际展示数据

jsData: {
  columnsHeight: [0, 0],
  isLoading: false
},
data: {
  columns: [
    [],
    []
  ],
  tempPics: []
}

1、加载列表数据

这一步没什么好说的,主要是触发方式,我的代码里是放在页面加载以及拉到页面底部时触发

onLoad: function() {
  this.loadData()
},
onReachBottom: function() {
  this.loadData()
}

加载后将列表数据存到tempPics中,用于页面加载获取宽高

2、在一个隐藏的view中加载图片,通过image组件的bindload获取图片的实际宽高并存储

<view class="hide">
  <image wx:for="{{tempPics}}" src="{{item.pic}}" bindload="loadPic" binderror="loadPicError" data-index="{{index}}" />
</view>

主要是image组件的bindload来获取实际宽高,这里还增加了binderror,防止出现图片加载出错的时候卡死

loadPic: function(e) {
  var that = this,
    data = that.data,
    tempPics = data.tempPics,
    index = e.currentTarget.dataset.index
  if (tempPics[index]) {
    //以750为宽度算出相对应的高度
    tempPics[index].height = e.detail.height * 750 / e.detail.width
    tempPics[index].isLoad = true
  }
  that.setData({
    tempPics: tempPics
  }, function() {
    that.finLoadPic()
  })
}

获取到宽高后,以750为宽度计算出相对应的高度并存储,然后增加一个加载完成的标记。加载出错后就强制高度为750,这样展示的时候就是一个正方形。
单个图片加载完成并存储后调用finLoadPic方法来判断所有图片是否都加载完成。
遍历列表,只要有一个图片没有加载完成的标记,就判断为没有加载完成。
加载完成后进入下一步。

finLoadPic: function() {
  var that = this,
    data = that.data,
    tempPics = data.tempPics,
    length = tempPics.length,
    fin = true
  for (var i = 0; i < length; i++) {
    if (!tempPics[i].isLoad) {
      fin = false
      break
    }
  }
  if (fin) {
    wx.hideLoading()
    if (that.jsData.isLoading) {
      that.jsData.isLoading = false
      that.renderPage()
    }
  }
}

3、等所有图片加载完成后遍历列表,将图片插入到高度低的那一栏,同时更新该栏高度

这里需要再便利一遍列表,根据当前栏位的高度情况,将图片插入到高度底的那一栏,同时把这一栏高度加上当前图片的高度(不是实际高度,是上一步以750为宽度算出来的高度)

renderPage: function() {
  var that = this,
    data = that.data,
    columns = data.columns,
    tempPics = data.tempPics,
    length = tempPics.length,
    columnsHeight = that.jsData.columnsHeight,
    index = 0
  for (var i = 0; i < length; i++) {
    index = columnsHeight[1] < columnsHeight[0] ? 1 : 0
    columns[index].push(tempPics[i])
    columnsHeight[index] += tempPics[i].height
  }
  that.setData({
    columns: columns,
    tempPics: []
  })
  that.jsData.columnsHeight = columnsHeight
}

在wxml中展示的时候image组件的mode要使用widthFix,同时wxss中图片的高度和宽度一样,这样加载出错的图片可以正方形展示

11月21日增加:
根据@杨泉的建议,也尝试了使用wx.getImageInfo来获取图片的宽高(具体代码可以参考评论区),代码也精简了很多。但是实际比较下来速度要比用image组件慢,初步推测原因是wx.getImageInfo会返回本地路径,多了写本地临时文件的时间
ps:用到瀑布流的地方,最好能后端直接返回图片的宽高,省去小程序端获取宽高的麻烦
再ps:我个人并不建议小程序端使用瀑布流

8 回复
wmeng
wmeng1 楼6 年前

路过点赞

jiehe
jiehe2 楼6 年前

小程序群里过来的,先赞在看

xiulanzhu
xiulanzhu3 楼6 年前

\u8d5e

qianjing
qianjing4 楼6 年前

真棒

qiangwei
qiangwei5 楼6 年前

手动点赞,支持群友

guiying60
guiying606 楼6 年前

手动点赞群友

songwei
songwei7 楼6 年前

仔细阅读了一遍,加载代码片段跑了一下。

对于瀑布流的加载,不管是移动端app开发还是h5开发,其实都有相对成熟的方案。

简单实现的话,直接使用flex wrap布局即可实现,然而可能会出的问题是,图片高度不一,会导致左右两侧的高度相差可能出现较大的偏差。所以基本都实现了动态填充的方式,计算出左右两列的高度,那边高度低,则新的一张图片放到哪边。

作者的思路是在wxml中利用隐藏的image组件预加载获取的图片列表,待图片全部加载完毕后获得图片实际高度,再按照以上原则动态填充到左侧或者右侧。

这里提出另两种解决方案,利用wx.getImageInfo在js中处理,代码逻辑更为精简。

首先对于 wx.getImageinfo的promise化:

function getImageInfo(src) {
  return new Promise((resolve, reject) => {
    wx.getImageInfo({
      src,
      success(res) {
        const height = res.height * 750 / res.width
        resolve({ ...res,
          height,
          src
        })
      },
      fail(e) {
        console.error(e)
        resolve({
          type: 'error',
          height: 750,
          src
        })
      }
    })
  })
}

使用接口获取到图片列表后,渲染图片的方式:

  1. (Loop的方式) 递归方式获取图片信息,成功一个,往页面渲染一个,核心代码如下:

    loopPics = function(){
          let pic = picList.shift()
          if(!pic) return
          getImageInfo(pic).then(res => {
            index = columnsHeight[1] < columnsHeight[0] ? 1 : 0
            columns[index].push(res.path)
            columnsHeight[index] += res.height
     
            that.setData({
              columns: columns,
            })
            that.jsData.columnsHeight = columnsHeight
            loopPics()
          })
        }


  2. (Batch的方式) 并发获取图片列表的图片信息,全部获取成功后整批次的渲染图片

    function(){
          let loadPicPs = []
          for(let i=0;i<picList.length;i++){
            loadPicPs.push(getImageInfo(picList[i]))
          }
          Promise.all(loadPicPs).then(results => {
            for(let i=0;i<results.length;i++){
              let res = results[i]
              console.log('wtfffff', res)
              index = columnsHeight[1] < columnsHeight[0] ? 1 : 0
              columns[index].push(res.path)
              columnsHeight[index] += res.height
            }
            that.setData({
              columns: columns,
            })
            that.jsData.columnsHeight = columnsHeight
            wx.hideLoading()
          })
        }

详细的代码片段见:

https://developers.weixin.qq.com/s/8kue22mw7cdJ

比较这两种方式效果,前者速度较快,渲染效果有点像挤牙膏,后者速度慢,但是最终渲染图片是一起加载,视觉上稍好一些。

最后,比较了作者利用隐藏image加载的方式以及上面这两种方式的渲染速度。

意想不到的是作者的方式图片加载速度反而更快?

细作分析之后发现,wx.getImageInfo在回调函数中返回的除了图片的高宽信息之外,还有一个关键的字段 path----图片的本地路径。这也就意味着 getImageInfo不仅获取了图片信息,还有一个保存图片到临时文件的io过程。因此从加载速度上反而比作者隐藏image组件加载的方式显慢。

大家可以按需选择使用。

P.S

当然,瀑布流的最佳实践是在上传图片的时候就由后台图片服务器保存图片的比例或者宽高信息,请求图片接口的时候一并返回,避免由客户端来进行不必要的计算和渲染。

izou
izou8 楼4 年前

0110 0110 0110