前言
之前分享过一篇瀑布流如何实现的文章,经过时间的证明,之前的做法并不好,性能上会有问题,所以还是不投机取巧了,老老实实的实现。
回顾:
- 通过grid-auto-rows的特性实现
- item通过grid-row设置高度
- js获取节点高度计算span的值
- 通过wxs设置css的变量实现修改样式
痛点:
- grid-auto-rows数值越大,span计算准确度越低。
- 谷歌浏览器、微信开发工具,如果界面高度超过
1000 * grid-auto-rows
的高度,那么后面的内容就不会显示了,谷歌解释说是为了不过渡消耗性能。 - 因为性能问题,超过1000的item就不会显示了,全会挤压在最下面,导致页面非常卡,开发工具能直接卡崩溃,手机上还没发现这个问题,之前也忽视了这个问题,后面调试的时候就非常恼火,开发工具跟真机上效果不一致。
- 为了保证span计算的准确度高,grid-auto-rows一般设置成1-10px,1px准确就等于view的高度,但是超过1000px就卡没了。
实现思路
- 通过selectAllComponents获取所有的子节点
- 通过getComputedStyle获取节点的高度
- 简单的排序算法计算节点位置
- 设置节点的样式
- 通过wxs的
getState
储存每屏节点渲染的数据 - 触发
image
组件的load
事件重新计算并渲染节点位置
创建组件
需要开启抽象节点
// waterfall/index.json
{
"componentGenerics": {
"selectable": true
}
}
利用wxs响应事件获取页面的节点
<view
class="waterfall"
views="{{ views.length }}"
data-option="{{ {span} }}"
change:views="{{ wxs.init }}"
>
<!-- 嵌套遍历views二维数组 -->
<block
wx:for="{{ views }}"
wx:key="item"
wx:for-index="i"
>
<selectable
class="item view-{{ i }}"
wx:for="{{ item }}"
wx:key="item"
value="{{ item }}"
/>
</block>
</view>
创建item的x,y边距变量 --span
.waterfall {
--span: 5px;
width: 100%;
position: relative;
.item {
width: calc(50% - var(--span));
position: absolute;
}
}
创建 index.wxs
,核心业务代码都写在这里
// 当views被setData的时候会被触发
module.export = {
init: function(newValue, oldValue, ownerInstance, instance) {
console.log(newValue, oldValue, ownerInstance, instance)
}
}
业务逻辑
步骤一:获取所有节点
function init(length, oldValue, ownerInstance, instance) {
// 加个判断,避免views长度为0时,或者长度为发生变化时也会执行业务代码
// 只有当views被push新的内容才会执行下面的业务
if (!length || length === oldValue) return
// index 其实就是views的长度减一,就等于当前的数组下标
var index = length - 1
var views = ownerInstance.selectAllComponents('.view-' + index)
console.log(JSON.stringify(views))
}
步骤二:遍历views获取节点的高度
views.forEach(function(v, k){
var viewStyle = v.getComputedStyle(['width', 'height'])
// 获取高度
var height = viewStyle.height
console.log(viewStyle)
// [WXS Runtime info] {"width":"182.5px", "height":"242px"}
})
步骤三:计算view的位置信息
var LH = 0
var RH = 0
views.forEach(function (v, k) {
var viewStyle = v.getComputedStyle(['width', 'height'])
// 格式化高度,将px去掉
var height = fixUnit(viewStyle.height)
var style = {}
if (LH <= RH) {
style = { left: 0, top: LH + 'px' }
LH += height
} else if (RH < LH) {
style = { right: 0, top: RH + 'px' }
RH += height
}
// 设置view的样式
v.setStyle(style)
})
此时,页面的节点会根据position自动排列好
步骤四:储存LH,RH到局部变量
function init(length, oldValue, ownerInstance, instance) {
if (!length || length === oldValue) return
// 获取局部变量
var state = ownerInstance.getState()
// 获取当前节点的dataset
var dataset = instance.getDataset()
var index = length - 1
state.option = dataset.option
state.page = length
// 创建并生成记录左侧、右侧高度
// 用二维数组来记录
if (!state.heights) {
state.heights = [[0, 0]]
}
// 记录初次渲染时间戳
if (!state.timeouts) {
state.timeouts = []
}
// 获取时间戳,并且加上3000毫秒,用于后面计算图片loaded完是否超时
state.timeouts[index] = getDate().getTime() + 3000
refreshViews(index, ownerInstance, state)
}
function refreshViews(index, ownerInstance, state) {
var views = ownerInstance.selectAllComponents('.view-' + index)
var span = state.option.span
var LH = state.heights[index][0] // 左侧
var RH = state.heights[index][1] // 右侧
views.forEach(function (v, k) {
var viewStyle = v.getComputedStyle(['width', 'height'])
var height = fixUnit(viewStyle.height)
var style = {}
if (LH <= RH) {
style = { left: 0, top: LH + 'px' }
LH += height + span[0]
} else if (RH < LH) {
style = { right: 0, top: RH + 'px' }
RH += height + span[0]
}
v.setStyle(style)
// 保存LH, RH的值到state.heights
// 当前的LH,RH其实就是下屏开始的坐标
state.heights[index + 1] = [LH, RH]
console.log('渲染', index, k)
})
}
步骤五:图片加载完重新计算位置
// waterfall/index.js
Component({
properties: {
views: Array,
span: {
type: Array,
value: [10, 10],
},
},
methods: {
onLoaded({ detail: { width, height, pIndex, index } }) {
this.setData({
[`views[${pIndex}][${index}].loaded`]: { width, height },
})
},
},
})
function loaded(value, oldValue, ownerInstance, instance) {
if (!value.item.loaded || !oldValue) return
// 获取局部变量
var state = ownerInstance.getState()
// 判断加载是否超时,如果超时则不触发计算渲染事件
// 让该节点保持当前的位置及高度
var timeout = state.timeouts[value.pIndex]
if (timeout < getDate().getTime()) {
console.log('加载超时')
return
}
var view = instance.selectComponent('.loaded-view')
var viewWidth = view.getComputedStyle(['width']).width
// 设置虚拟节点card组件里的loaded-view高度
view.setStyle({
height:
computedHeight(
viewWidth,
value.item.loaded.width,
value.item.loaded.height
) + 'px',
})
// 加个函数防抖,因为图片加载快的情况下,会并发触发事件
// 尽可能的少触发计算,渲染事件,保证性能
ownerInstance.clearTimeout(timer)
timer = ownerInstance.setTimeout(function () {
// 渲染当前图片加载完后面的所有views
// for循环处理当前图片所在的views,以及后面所有的views
// 因为有些图片过大,可能会加载5s左右,但是用户如果上拉又加载了
// 一屏内容并且也通过计算渲染了,这时候上一屏又触发了计算渲染
// 那么可能位置信息就会发生变化,导致被遮挡,或者有空白,这时候只能
// 计算触发事件的图片以及后面的图片,保证位置信息是正确的
for (var i = value.pIndex; i < state.page; i++) {
console.log('需要渲染', i)
refreshViews(i, ownerInstance, state)
}
}, 300)
}
优化
瀑布流最好后台会返回图片的尺寸信息,然后初次渲染的时候就计算好节点的长宽比例,这样就不用监听图片loaded事件了,瀑布流组件代码也不会频繁触发计算渲染,性能也好,方法也简单。
<image
src="xxxx"
style="{{ wxs.computed({width, height}) }}"
/>
// wxs
function computed(option) {
// 节点宽度自己去计算
var viewWidth = 375 / 2
var width = option.width
var height = option.height
return (viewWidth / width) * height + 'px
}
完整代码
打开代码片段https://developers.weixin.qq.com/s/SO5q6UmF7doL,可直接运行。
https://github.com/liziwork/li-ui github 如果打不开,请切换到码云,gitee.com,代码同步更新的,觉得有用动动您的小手点个Star。
item过多,会卡顿的问题,还是没解决啊
item过多,会卡顿的问题,还是没解决啊
用绝对定位, 父容器的高度就塌陷了
点赞三连
三连
图片高度会抖动一下?是否是高度没有计算好?