小程序技巧系列——两栏瀑布流实现
前言:在移动端两栏的瀑布流交互方式还是很常见的,大多用在图文的展示上,在这里记录下自己的实现方式,供有需要的人参数
什么是瀑布流布局
视觉表现就是一种参差不齐的多栏布局,随着页面的滚动,随之加载数据。其实本质就是容器内有多个高度不一致的元素展示,既多行等宽元素的一种顺序排列。
布局原理
本片文章只实现移动端比较常见的两栏瀑布流,并且是小程序生产环境可用的,所以就不再赘述一些其他方式的实现。
- 页面分成左右两个容器,各对应一组数据。
- 将每一列当做一个容器,在数据侧先将数据分为左右两个数组。每次插入时,计算下左右的高度,依次插入到高度较低的那一个容器。
实际效果
实现
新建waterfall自定义组件
布局
<view class="waterfall-wrap">
<view class="waterfall-column" style="width: 340rpx">
<view class="left-container" id="left">
<block wx:for="{{leftList}}" wx:key="{{item}}">
<view class="item" style="height: {{item}}rpx"></view>
</block>
</view>
</view>
<view class="waterfall-column" style="width: 340rpx">
<view class="right-container" id="right">
<block wx:for="{{rightList}}" wx:key="{{item}}">
<view class="item" style="height: {{item}}rpx"></view>
</block>
</view>
</view>
</view>
.waterfall-wrap {
display: flex;
width: 100%;
box-sizing: border-box;
justify-content: space-between;
}
.item {
width: 100%;
border: 1px solid rosybrown;
border-radius: 16rpx;
margin-bottom: 20rpx;
}
渲染逻辑
使用小程序提供的boundingClientRect获取布局节点的位置信息,这样就可以获取到容器的高度,来做插入判断。 具体实现,看代码:
Component({
data: {
leftList: [],
rightList: []
},
methods: {
oneByOneRender(data = [], i, success) {
if (data.length > i) {
this.getBoundingClientRect((res) => {
const rects = res[0];
if (rects && rects.length) {
const leftH = rects[0].height;
const rightH = rects[1].height;
if (leftH <= rightH + 10) {
this.data.leftList.push(data[i]);
} else {
this.data.rightList.push(data[i]);
}
this.setData({
leftList: this.data.leftList,
rightList: this.data.rightList
}, () => {
this.oneByOneRender(data, ++i, success);
})
}
})
} else {
success && success();
}
},
run(data, success, isLast) {
if(this.columnNodes) {
this.render(data, success, isLast);
} else {
this.selectDom(() => {
this.render(data, success, isLast);
})
}
},
getBoundingClientRect(cb){
this.columnNodes.boundingClientRect().exec(cb)
},
render(data = [], success, isLast) {
if(isLast) {
return this.oneByOneRender(data, 0, success)
}
this.columnNodes.boundingClientRect().exec((res) => {
const rects = res[0];
if (rects && rects.length) {
let container = '';
if (rects[0].height <= rects[1].height) {
container = 'leftList';
} else {
container = 'rightList';
}
data.forEach(item => {
this.data[container].push(item);
if (container === 'leftList') {
container = 'rightList'
} else {
container = 'leftList'
}
})
}
this.setData({
leftList: this.data.leftList,
rightList: this.data.rightList
}, () => {
})
})
},
selectDom(cb) {
const query = this.createSelectorQuery();
this.columnNodes = query.selectAll('#left, #right');
cb && cb();
}
},
});
性能优化
由于小程序的双线程架构,频繁的setData会对性能有较大的损害,因此我们要尽可能的减少setData,因此对于一个瀑布流来说,我们正常滚动过程中,对一次加载的数据,先判断当前左右容器的高度,然后将数据遍历,分成2份,最后直接一次setData。 只有当是最后一页数据时,保证我们瀑布流不会出现一边很高,一边很低。这个时候我们做到每插入一个元素都去计算左右容器的高度,保证正确的插入顺序,样式上做到符合预期。 代码已在上面体现。
调用
调用这里,我们不使用数据传递,而是直接调用自定义组件内的方法,在使用的页面内:
// 模拟接口数据
setTimeout(() => {
this.waterfallRef = this.selectComponent('#waterfallFlow');
this.waterfallRef.run(this.data.dataList, () => {
});
}, 1000)
// 加载更多
onReachBottom() {
this.waterfallRef.run(this.data.dataList, () => {
}, true);
},
这里是使用页面的滚动到底部的钩子函数,同理也可以使用在scroll-view组件内。
总结
本文的重点其实性能优化这点,由于小程序双线程的架构,因此数据-》渲染这块,整体效率较低。为了交互更友好,体验更好。因此我这里想的是,正常的加载过程中,只判断当前容器的高度,然后直接将数据分为两份,然后对应加载。只有当是最后一页时,才没插入一个计算一个,保证最后左右两侧不会出现一边很高,一边很低的现象。
转载自:https://juejin.cn/post/7066778804827619364