「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战」
一、什么是瀑布流
案例
下两张图分别是京东
首页、淘宝
首页
可以看到都使用了瀑布流式布局
,图片等宽不等高
定义
瀑布流
又称瀑布流式布局,是一种比较流行的页面布局方式,专业的英文名称为[Masonry Layouts]。与传统的分页显示不同,视觉表现为参差不齐的多栏布局
特点
技术角度
- 多栏或者说多列(为了保持统一,下面都说
栏
) 图片等宽不等高
,根据图片原比例缩放直至宽度达到固定的要求- 图片依次放入高度最小的栏,比如,分为两栏,刚开始,两栏的高度都是0,第一张图片进来,高度400,放在第一栏,第二张图片(高度300),放在第二栏,第三张图片(高度50),还是放在第二栏
体验角度
- 节省空间,外表美观,更有艺术性
- 用户浏览时的观赏和思维不容易被打断,留存更容易
适用场景
内容以图片为主
的时候,瀑布流是更好的选择。图片占用空间比较大,并且大脑理解的速度相比理解文字要快,短时间内可以扫过的内容很多,所以如果用分页显示的话用户务必会频繁的翻页,影响沉浸式的体验,而瀑布流可以解决这个问题用户目的性不强
的时候,瀑布流是更好的选择。如果用户有特定需要查找的信息,分页查找定位更方便,而当目的性较弱的时候,瀑布流可以增加用户停留的时间和意想不到的收获
二、常用解决方案
主要总结在技术选型时,看到的各个方案的缺陷
1、多栏布局
multi-column实现瀑布流主要依赖以下几个属性:
column-count
: 设置共有几列column-width
: 设置每列宽度,列数由总宽度与每列宽度计算得出column-gap
: 设置列与列之间的间距break-inside
: avoid; 设置元素不能中断
.container{
column-count: 3;
column-gap: 10px;
&-item {
break-inside: avoid;
}
}
复制代码
优点是纯css,不需要知道图片的高度
,但是有一个致命的缺陷,multi-column
布局中子元素的排列顺序是先从上往下再从左至右
,如下图
所以如果你的数据是需要先从左到右再从上到下
展示,或者说有分页
,那是不满足的
2、grid布局
缺点:需要知道图片的高度
,通过图片的高度动态修改样式属性
具体实现可见博客
3、flex布局
一种是直接用flex布局,从上往下再从左至右
,这种需要提前给容器设置一个高度,而这个高度是多少,很难计算
<View className="container">
{list.map(item => (<View className="container-item" key={item.id}></View>))}
</View>
复制代码
还有一种是,容器里面分列,比如分为两列,这种的话与grid布局
一致,需要需要知道图片的高度
,通过图片的高度动态修改样式属性
<View className="container">
<!-- 第一列 -->
<div className="container-column">
<div className="container-column__item"></div>
<!-- more items-->
</div>
<!-- 第二列 -->
<div className="container-column">
<div className="container-column__item"></div>
<!-- more items-->
</div>
<!-- 第三列 -->
<div className="container-column">
<div className="container-column__item"></div>
<!-- more items-->
</div>
</View>
复制代码
4、position:absolute
绝对定位,把子元素全部设置成绝对定位
,监听图片加载,如果加载完就把子元素设置其对应的位置,逐个塞到父容器中,需要需要知道图片的高度
综上,因为场景是有分页,且顺序需要按照服务端返回的顺序展示,所以
多栏布局
满足不了,需要js计算,考虑到flex布局
相对于grid布局
,浏览器支持度、以及属性更简单,小程序商城就两列
,相较position
,flex布局
计算更友好
三、Taro多端商城瀑布流实践
服务端返回的数据里面没有图片的宽高,首先想办法拿到图片的宽高,然后分为两列形成瀑布流
1、拿图片的高度
微信小程序图片Image上有一个bindload事件,如下图
对应到Taro的Image上是onLoad事件,如下图
需要注意的是,这个事件是在图片载入完毕
的时候得到参数的,因此当我们从服务端拿到列表数据之后,这个列表并不是我们真正想呈现的效果,因此节点需要设置display: none
先解释下,存在分页,showList
是当前页的数据,比如10条
在事件的e.detail
上可以拿到图片的宽width
、高height
;然后根据定宽,获得图片的缩放比例,算出展示的高度imgHeight
const onImageLoad = (index) => {
return (e) => {
console.log(index)
// 获取图片宽高
const oImgW = e.detail.width; //图片原始宽度
const oImgH = e.detail.height; //图片原始高度
const imgWidth = 340; //图片设置的宽度
const scale = imgWidth / oImgW; //比例计算
const imgHeight = Math.round(oImgH * scale); //自适应高度
...
};
};
<View className="buyer-show__container-v">
{
showList.map((item, index) => {
return <Image
key={ showAllList.length + index }
className="buyer-show__item-img"
src={ item.picUrl }
onLoad={ onImageLoad(showAllList.length - showList.length + index) }
/>;
}
)
}
</View>
复制代码
.buyer-show__container-v{
display: none
}
复制代码
2、图片调整排序
上面onImageLoad
的执行顺序
需要注意下,我们想要的结果是按照数组的顺序
执行,但真正在渲染的时候,规律是小图片会先执行、先载入完成,不会按照顺序
const onImageLoad = (index) => {
return (e) => {
console.log(index)
// 获取图片宽高
const oImgW = e.detail.width; //图片原始宽度
const oImgH = e.detail.height; //图片原始高度
const imgWidth = 340; //图片设置的宽度
const scale = imgWidth / oImgW; //比例计算
const imgHeight = Math.round(oImgH * scale); //自适应高度
const newShowLoadList = [...showLoadList, {
id: Number(index),
height: imgHeight,
}];
setShowLoadList(newShowLoadList);
// 载入全部的图片进入showLoadList数组,若数量和showList中相同,进入图片排序函数
if (newShowLoadList.length === showList.length) {
handleImageLoad(newShowLoadList);
}
};
};
复制代码
如上代码,输出console.log(index)
,可以看到顺序如下
所以需要引入变量showLoadList
,表示当前页(比如10条)已经载入完的数据数组,并且加上id属性
代表索引
,并将获得height
后保存的数据放到showLoadList
当这页全部载入完,执行如下handleImageLoad
,利用sort
根据id属性
排序
const handleImageLoad = (newShowLoadList) => {
// 调整顺序
const imageLoadList = newShowLoadList.sort((a, b) => a.id - b.id);
...
}
复制代码
3、形成瀑布流数据
编译数据,将height与初始数据合并到imageLoadList
定义左右已渲染数据高度变量,leftHeight
、rightHeight
,将数据分为左右两个数组leftShowList
、rightShowList
遍历imageLoadList
根据leftHeight
、rightHeight
高度依次放入高度矮
的那边
const [rightShowList, setRightShowList] = useState<showItemType[]>(rightList);
const [leftShowList, setLeftShowList] = useState<showItemType[]>(leftList);
const [leftHeight, setLeftHeight] = useState<number>(0);
const [rightHeight, setRightHeight] = useState<number>(0);
const handleImageLoad = (newShowLoadList) => {
// 调整顺序
const imageLoadList = newShowLoadList.sort((a, b) => a.id - b.id);
for (let i = 0; i < showList.length; i++) {
// 把原数组中的属性赋予imageLoadList数组
imageLoadList[i] = {
...imageLoadList[i],
...showList[i],
imgStyle: {
height: pxTransform(imageLoadList[i].height)
}
};
}
// 对现在的列表进行操作
let leftHeightCur = leftHeight; // 左边列表的高度
let rightHeightCur = rightHeight;
const left: showItemType[] = leftShowList; // 左边列表的数组
const right: showItemType[] = rightShowList;
// 遍历数组
for (let i = 0; i < imageLoadList.length; i++) {
if (leftHeightCur <= rightHeightCur) {
left.push(imageLoadList[i]);
leftHeightCur = leftHeightCur + imageLoadList[i].height + 322;
} else {
right.push(imageLoadList[i]);
rightHeightCur = rightHeightCur + imageLoadList[i].height + 322;
}
}
setRightShowList(right);
setLeftShowList(left);
setRightHeight(rightHeightCur);
setLeftHeight(leftHeightCur);
saveShowLoadList(left, right);
setShowLoadList([]);
};
复制代码
4、render及页面分页
利用flex布局
,左右分为两列
说下分页,因为有分页,数据会比比较多,因此每次在页面不显示的buyer-show__container-v
节点,每次都渲染当前页即可,也就是showList
,showAllList
是整体数据
而想要所有数据的索引值是对的,可以将传入onImageLoad的ID
设置为showAllList.length - showList.length + index
<View className="buyer-show">
<View className="buyer-show__container-v">
{
showList.map((item, index) => (
<Image
key={ showAllList.length - showList.length + index }
className="buyer-show__item-img"
src={ item.picUrl }
onLoad={ onImageLoad(showAllList.length - showList.length + index) }
/>
))
}
</View>
<View className="buyer-show__container">
<View className="buyer-show__container-left">
{
leftShowList.map(item => (
<View key={ item.id }>
<CartItem item={ item } />
</View>
))
}
</View>
<View className="buyer-show__container-right">
{
rightShowList.map(item => (
<View key={ item.id }>
<CartItem item={ item } />
</View>
))
}
</View>
</View>
</View>
复制代码