瀑布流梳理
js 实现思路
- 先排列好第一行的内容(
i<this.cols或者heightsArr.length<this.cols)
,并用数组「heightsArr
」记录每一列的高度; - 接下来排列剩余行时,找到高度和最低的那一列,并记录列的索引「minHeightColIndex」;
- 将新的那一个数据项「
newItem
」放置在「minHeightColIndex
」那一列的下方(通过style.left
,style.top
); - 然后更新「
minHeightColIndex
」列的高度(heightsArr[minHeightColIndex]+newItem.height+rowGap
); - 直到所有项遍历结束。
js 优化思路
本地获取图片再得到图片宽高信息,效率较低,会出现白屏,体验较差。
优化思路 1:服务端直接返回图片宽高信息;
优化思路 2:[web 端]从云存储服务器获取图片时加上?imageInfo,可直接返回图片信息,速度很快。例如七牛:https://img.fatiaoya.com/bdperson/1670316442405_余姚路169号.png?imageInfo
列表渲染时,指定图片的样式宽高(瀑布流计算后的宽高),否则容易出现抖动!
完整 JS 代码
js
/**
* 方案思路
*
* 基本实现:
* 1.先排列好第一行的内容,并用数组「heightsArr」记录每一列的高度;
* 2.接下来排列剩余行时,找到高度最低的那一列,并记录列的索引「minHeightColIndex」;
* 3.将新的那一个数据项「newItem」放置在「minHeightColIndex」那一列(通过style.left),
* 并更新「minHeightColIndex」列的高度(heightsArr[minHeightColIndex]+newItem.height+rowGap);
*
* 参考链接:https://zhuanlan.zhihu.com/p/55575862
*
* 优化:
* 本地获取图片再得到图片宽高信息,效率较低,会出现白屏,体验较差。
* 优化思路1:服务端直接返回图片宽高信息;
* 优化思路2:[web端]从云存储服务器获取图片时加上?imageInfo,可直接返回图片信息,速度很快。
* 例如七牛:https://img.fatiaoya.com/bdperson/1670316442405_%E4%BD%99%E5%A7%9A%E8%B7%AF169%E5%8F%B7.png?imageInfo
*
* 参考链接:https://juejin.cn/post/6844903998625972238
*/
/**
* 「瀑布流」主要方法如下:
* mutilLoader 用于批量处理
*
* singleLoader 用于单个处理
* return {
* width(图片原始宽),
* height(图片原始高),
* imgRatioWidth(图片等比缩放宽),
* imgRatioHeight(图片等比缩放高),
* blockWidth(整块宽),
* blockHeight(整块高),
* primaryColor(图片主色调,默认#f5f5f5),
* textColor(根据主色调的「亮度」,获取文字颜色[黑|白])
* }
*
* getMaxHeight 返回最大列高,可用于动态修改页面瀑布流外层容器的高度,给loading预留位置
*
* reset 用于重置瀑布没列的高度信息
*/
export class Waterfall {
cols;
rowGap;
colGap;
colWidth;
heightsArr = []; //每一列所有项的高度和
minHeightColIndex = 0; // 高度最低的那一列的索引
hasServerImageInfo = false;
isWXMP = true;
/**
* 参数说明
* @param options
* @param options.pageWidth 页面宽度
* @param options.cols 需要展示多少列
* @param options.colGap 列间隙
* @param options.rowGap 行间隙
* @param options.addonHeight 附加高度(比如有文字介绍)
* @param options.hasServerImageInfo 服务端是否返回图片信息,若是,则本地无需再通过其他方式加载图片获取图片的宽高信息了
*/
constructor({
pageWidth,
cols,
colGap,
rowGap,
addonHeight,
hasServerImageInfo = false,
isWXMP = true,
}) {
this.cols = cols;
this.rowGap = rowGap;
this.colGap = colGap;
this.addonHeight = addonHeight;
this.hasServerImageInfo = hasServerImageInfo;
this.isWXMP = isWXMP;
// colWidth => [screenWidth = cols * colwidth + 3 * colGap] = (screenWidth - 3*colGap)/cols
this.colWidth = (pageWidth - (cols + 1) * colGap) / cols;
console.log("列数,列宽: ", this.cols, this.colWidth);
}
/**
* 多项加载
* @param {*} list
*/
async mutilLoader(list = []) {
for (let i = 0; i < list.length; i++) {
let info = await this.singleLoader(list[i]);
list[i] = {
...info,
};
}
return [...list];
}
/**
* 单项加载
* @param {*} data
*/
async singleLoader(data) {
let item = {
...data,
};
// step1:处理基本信息
let imgInfo = {
width: 0,
height: 0,
primaryColor: "#f5f5f5",
};
if (this.hasServerImageInfo) {
imgInfo = {
width: item.width || 260,
height: item.height || 320,
primaryColor: item.main_color,
};
} else {
let res = {};
if (this.isWXMP) {
res = await this.wxGetImageInfo(item.thumb_img);
} else {
res = await this.webGetImageInfo(item.thumb_img);
}
imgInfo = {
...imgInfo,
...res,
};
}
const { width, height, primaryColor } = imgInfo;
let ratioHeight = this.colWidth * (height / width); //等比缩放的高度 height/width = h2/colWidth
item.imgRatioHeight = ratioHeight; // 等比例缩放后的图片高度
item.blockHeight = ratioHeight + this.addonHeight; //实际高度=等比例缩放的高度+附加高度(如展示title)
item.imgRatioWidth = this.colWidth;
item.blockWidth = this.colWidth;
item.primaryColor = primaryColor; //图片主色调
item.textColor = this.getContrastColor(primaryColor); //根据主色调的亮度,设置文字颜色[黑|白]
// step2:装载入列,计算位置
let i = this.heightsArr.length;
if (i < this.cols) {
// 第一行,记录列高
this.heightsArr.push(item.blockHeight);
item.left = i * this.colWidth + this.colGap * (i + 1);
item.top = 0;
} else {
// 剩余行,寻找整列高最小的列加入
let minHeight = Math.min(...this.heightsArr);
let minHeightIndex = this.heightsArr.indexOf(minHeight);
// console.log(this.heightsArr, minHeightIndex, minHeight, item.id)
item.left =
minHeightIndex * this.colWidth + this.colGap * (minHeightIndex + 1);
item.top = this.heightsArr[minHeightIndex] + this.rowGap;
// 更新这里列的高度
this.heightsArr[minHeightIndex] = item.top + item.blockHeight;
}
return item;
}
/**
* 获取最大列高
* 可用于动态修改页面瀑布流外层容器的高度,给loading预留位置
*/
getMaxHeight() {
return Math.max(...this.heightsArr);
}
/**
* 重置高度记录,适用于列表重载
*/
reset() {
this.heightsArr = [];
}
/**小程序端 */
wxGetImageInfo(src) {
return new Promise((resolve) => {
wx.getImageInfo({
src: src,
success: (res) => {
resolve(res);
},
fail(err) {
console.log(err);
},
});
});
}
/**web端 */
webGetImageInfo(src) {
return new Promise((resolve, reject) => {
let img = new Image();
img.crossOrigin = "Anonymous"; //解决跨域图片问题
img.onload = (e) => {
resolve({
img,
width: img.naturalWidth,
height: img.naturalHeight,
whRatio: (img.naturalWidth / img.naturalHeight).toFixed(2) * 1,
hwRatio: (img.naturalHeight / img.naturalWidth).toFixed(2) * 1,
});
};
img.onerror = (err) => {
reject(err);
};
img.src = src;
});
}
/**获取相对色 */
getContrastColor(rgbstr) {
if (!rgbstr || !rgbstr.length) {
return "";
}
let rgb = "";
if (rgbstr.includes("rgb")) {
rgb = rgbstr.replace(/rgb|\(|\)/g, "");
}
rgb = rgb.split(",");
let yuv = this.rgb2yuv(rgb[0], rgb[1], rgb[2]);
return yuv >= 128 ? "rgb(0,0,0)" : "rgb(255,255,255)";
}
/**rgb转亮度yuv */
rgb2yuv(r = 0, g = 0, b = 0) {
return r * 0.299 + g * 0.578 + b * 0.114;
}
}
在微信小程序中使用
html
<import src="/templates/listLoadingIcon" />
<nav-menus menus="{{menus}}" bind:change="onMenuChange"></nav-menus>
<!-- 设置外层容器高度,给loading预留位置 -->
<view class="wrap" style="height: {{pageHeight}}px;">
<view
class="item"
wx:for="{{listFetchConfig.data}}"
style="left:{{item.left}}px;top:{{item.top}}px;width:{{item.blockWidth}}px;height:{{item.blockHeight}}px;background-color: {{item.primaryColor}};"
key="index"
bindtap="goMaterialDetail"
data-video="{{item}}"
>
<!-- !!!这里需要手动设置图片的宽高,如果是mode='xxx'自适应的写法,会出现加载新数据时页面抖动问题。 -->
<image
src="{{item.thumb_img}}"
style="width:{{item.imgRatioWidth}}px;height:{{item.imgRatioHeight}}px;background-color: {{item.primaryColor}};"
>
</image>
<view class="title t13 t-2" style="color:{{item.textColor}};"
>{{item.title}}</view
>
<view wx:if="{{item.is_recommend}}" class="recommend">荐</view>
</view>
<!-- loading位置计算 -->
<view class="f-r-c-c" style="position: absolute; top: {{pageHeight-60}}px;">
<template
is="list_loading_icon"
data="{{loading:listFetchConfig.isLoading}}"
></template>
</view>
</view>
js
const { screenWidth } = wx.getSystemInfoSync();
let wf = new Waterfall({
pageWidth: screenWidth,
cols: 2,
colGap: 7.5,
rowGap: 7.5,
addonHeight: 60,
hasServerImageInfo: true
});
// 单个计算加载
let info = await wf.singleLoader(item);
// 获取最大列高
this.setData({
pageHeight: wf.getMaxHeight(),
});
// 重置瀑布流列高信息
wf.reset();
css 实现
flex 实现
html
<div class="wrap">
<div class="item" style="height:400px;">
<h4>1</h4>
<img />
</div>
<div class="item" style="height:200px;">
<h4>2</h4>
<img />
</div>
<div class="item" style="height:300px;">
<h4>3</h4>
<img />
</div>
</div>
css
.wrap {
column-count: 2;
column-gap: 20rpx;
position: relative;
}
.item {
position: absolute;
box-sizing: border-box;
width: calc(50vw - 10rpx);
border: 1px solid #999;
margin-bottom: 10px;
/* 元素是否可以被中断:渲染完再换行 */
break-inside: avoid;
background-color: cadetblue;
border-radius: 30rpx;
}
image {
width: 100%;
border-radius: 30rpx;
}