git仓库地址:https://github.com/kapeter/mpMasonry
前段时间,接到一个需求,要在小程序中实现不定高度的瀑布流布局。我首先去万能的百度上搜索了一波,确实有很多方案,但都是固定高度的,这和需求不符。于是决定自己写一个,考虑到后面也会有类似的需求,干脆做成一个通用组件,方便使用。
瀑布流是比较流行的一种网站页面布局,尤其在mobile端经常被用来展示信息流。前段时间,接到一个需求,要在小程序中实现不定高度的瀑布流布局。我首先去万能的百度上搜索了一波,确实有很多方案,但都是固定高度的,这和需求不符。于是决定自己写一个,考虑到后面也会有类似的需求,干脆做成一个通用组件,方便使用。
本套方案主要使用flex模型,结合小程序的特性(boundingClientRect、抽象节点),实现瀑布流布局和组件化。
由于手机宽度的限制,一般移动端的瀑布流只有两列,不需要考虑多列的情况,因此我们的布局完全可以通过CSS3的flex模型完成。
// masonry.wxss
.masonry-list {
width: 100%;
display: flex;
box-sizing: border-box;
}
.masonry-list-left, .masonry-list-right {
flex: 1;
}
flex为1,给予左右两列相同的宽度,通过设置properties中的intervalWidth来控制两者间的间距。
有人可能会奇怪,为什么要在class="masonry-list-left"
的view之后再加一个层级?同一层级会怎么样呢?
来看两个写法的对比图,我们统一给左边加上高度150px,右边高度设为auto。 很明显,在同一层级情况下,右边高度也变成了150px,与左边一致,这会导致我们后面获取两边高度的时候拿到一样的数值,就无法判断该把元素放在哪边。因此,我们要多增加一个层级。
出现这种情况的原因是:flex的column会进行高度补全,和父容器保持一致。
基本布局已经完成,接下来就是要让布局“流”起来。
先来看一下传统瀑布流的原理:先通过计算出一排能够容纳几列元素,然后寻找各列之中所有元素高度之和的最小者,并将新的元素添加到该列上,如此循环下去,直至所有元素均能够按要求排列为止。
根据上述原理,渲染流程如下:
/**
* 渲染函数
*
* @param {Array} items - 正在渲染的数组
* @param {Number} i - 当前渲染元素的下标
* @param {Function} onComplete - 完成后的回调函数
*/
_render (items, i, onComplete) {
if (items.length > i && !this.data.stopMasonry) {
this.columnNodes.boundingClientRect().exec(arr => {
const item = items[i]
const rects = arr[0]
const leftColHeight = rects[0].height
const rightColHeight = rects[1].height
this.setData({
items: [...this.data.items, {
...item,
columnPosition: leftColHeight <= rightColHeight ? 'left' : 'right'
}]
}, () => {
this._render(items, ++i, onComplete)
})
})
} else {
onComplete && onComplete()
}
}
为了满足item高度是动态的场景,需要将渲染函数设置为递归函数。以下是渲染函数的执行流程:
boundingClientRect()
调取两边的高度;columnPosition
字段;setData()
将新的元素渲染到dom上,新的元素位置基于columnPosition
字段的值;setData()
的回调函数中执行下一层递归,确保下一次boundingClientRect()
能获取到最新的高度。
setData()
为异步渲染,详细说明请见小程序指南-双线程下的界面渲染- 由于每渲染一个元素,需要执行一次
boundingClientRect()
和setData()
,渲染时间较长。exec()
返回的是按请求次序构成的结果数组,即使只执行了一次请求,结果也位于res[0]而不是res。- boundingClientRect的详细用法可查看小程序文档-WXML节点信息API。
有了核心的渲染函数,我们还要进行一些处理。
/**
* 刷新瀑布流
*
* @param {Array} items - 参与渲染的元素数组
*/
_refresh(items) {
const query = wx.createSelectorQuery().in(this)
this.columnNodes = query.selectAll('#left-col-inner, #right-col-inner')
return new Promise((resolve, reject) => {
this._render(items, 0, () => {
resolve()
})
})
}
_refresh
函数包括两部分:
_render
函数包起来,并在_render
的完成回调函数中触发resolve()
,这样就能在渲染结束后执行其他操作。当前存在一个问题,masonry-item组件是用来承载元素的业务逻辑,如果项目存在多处需要瀑布流,并且业务逻辑不一样,那就需要修改masonry组件,添加判断条件,这就产生了耦合,不符合通用组件的规范。因此,我们需要进行解耦。
这里需要用到“抽象节点”。以下是定义:自定义组件模版中的一些节点,其对应的自定义组件不是由自定义组件本身确定的,而是自定义组件的调用者确定的。这时可以把这个节点声明为“抽象节点”。
简单来说,就是在masonry组件内部定义抽象节点masonry-item,这个节点可以代表任何组件,只有当页面调用masonry组件时,这个组件才被确定,这样就能将业务逻辑组件剥离出来了。
具体实现很简单,在masonry组件声明抽象节点。
// masonry.json
"componentGenerics": {
"masonry-item": true
}
在页面调用时,指定该抽象节点为哪个组件。
注意点:节点的 generic 引用 generic:xxx="yyy" 中,值 yyy 只能是静态值,不能包含数据绑定。因而抽象节点特性并不适用于动态决定节点名的场景。
1、将components目录下中masonry文件夹复制到自己项目中。
2、添加业务组件,并在业务组件中添加property,用于承载数据
// property名必须为item
properties: {
item: {
type: Object
}
}
3、引入masonry组件和所需的业务组件
// index.json
"usingComponents": {
"masonry": "../../components/masonry/masonry",
"img-box": "../../components/img-box/img-box"
}
4、在wxml加入masonry节点
generic:masonry-item
用于指定业务组件,interval-width
为左右两列空隙宽度。
5、调用函数,渲染瀑布流
_doStartMasonry(items) {
// 通过ID,获取组件实例
this.masonryListComponent = this.selectComponent('#masonry');
// 调用组件的start函数,渲染瀑布流
this.masonryListComponent.start(items).then(() => {
console.log('render completed')
})
}
为保证页面显示效果,建议一次渲染不超过100个元素。
函数名 | 函数功能 | 参数说明 | 返回值 |
---|---|---|---|
append | 批量添加元素 | {Array} items - 新增的元素数组 | Promise |
delete | 批量删除瀑布流中的元素 | {Number} start - 开始下标 {Number} end - 结束下标 |
无 |
deleteItem | 删除瀑布流中的某个元素 | {Number} index - 数组下标 | 无 |
start | 启动组件,开始渲染瀑布流 | {Array} items - 参与渲染的元素数组 | Promise |
stop | 停止渲染瀑布流,清空数据 | 无 | 无 |
updateItem | 更新渲染数组中的某个元素 | {Object} newItem - 修改后的元素 {Number} index - 需要更新的数组下标 |
无 |
原文https://www.kapeter.com/post/64