前言
有时候我们展示一些图片布局的时候,为了能够让用户一个页面能够展示更多的内容,并且还要展示较全的图片内容,但是图片比例是不一样的,有横屏竖屏,并且同时竖屏百分比也不一样,为了能够展示的更优秀,因此有了瀑布流效果
先看一个测试案例效果,我们实现了一个类似的效果,当然要是加上图片就更好看了,这里就不加了
瀑布流
想要实现瀑布流,至少要知道瀑布流的几个特征,即需要做的一些事情
瀑布流要做的那些事
- 需要了解布局宽度、单个item的宽度,或者最大宽度,能够保证一瓶放置多少列,也可以写死列数,然后根据列数自动填充(这里就先不支持写死列数了,这个逻辑更简单)
- 可以通过,最大宽度计算出,最多可以计算出大致多少列,保持列数,适当缩小item宽度即可计算出实际的 item 宽度,当然要参考间隙 gap
- 计算出一共有多少列后,可以在屏幕上放置指定数量的,竖向排版的布局,提前计算好数据应该在哪一列,然后直接一次渲染所有列即可
实现步骤
- 为了更通用,我们编写一个 WaterFallModel,用于保存瀑布流的基础信息,包括总宽度totalWidth(布局不一定能用到,计算用),item 宽度 itemWidth, 间隙 gap,最大item宽度 maxItemWidth(通过该参数配合总宽度可以计算出比较合理的 item 宽度),列数 column 是用于保存计算的 column 结果,每列布局的高度 columnsHeight 用于计算下一个元素应该布局到哪一列
- 编一个一个 generateColumns 用于计算出实际的 columns,这个主要是针对于 maxItemWidth,顺便计算出比较合适的 itemWidth,保存下来
- 编写一个 waterFallAtIndex 用于计算出下一个元素应该放置在哪一列,再布局的时候,能够保证新元素总是插入到最矮的那一列
- 计算好之后,只需要横向同时部署多列纵向布局,将生成的数据填充渲染即可实现瀑布流效果
实现 WaterFallModel 计算模块
我们直线先提取一个用于计算的 WaterFallModel,里面保存有瀑布流 UI布局 的基础信息,还有计算逻辑
//这里我们认为是没有padding,需要外部加上margin或者套一层即可,暂时忽略
export default class WaterFallModel {
totalWidth: number;
itemWidth: number = 0;
maxItemWidth: number;
gap: number;
columns: number;
columnsHeight: number[];
//计算规则
//整体布局总宽度是必须要有的
//如果设置了单个itemWidth宽度,则会根据gap(默认为10),计算页面布局,放置不开则右侧留白
//如果没有设置itemWith,这里直接建议同时设置 maxItemWidth,则会有限根据最大 ItemWidth 伸开,如果最后一个超出整体宽度,则适当缩小,保证最后一个贴边,不留白
//有最大这里不要最小了,简化一点逻辑,另外设置最大就足够了(对于图片体验稍好),最小没啥必要
//gap间距,默认设置为 10,可以设置,间距固定显示
constructor(totalWidth: number) {
this.totalWidth = totalWidth;
this.itemWidth = totalWidth; //设置跟外部一样,默认一列,需要自己手动设置,计算后,此值会有所改变
this.maxItemWidth = 0;
this.gap = 10;
this.columns = 0;
this.columnsHeight = [];
}
generateColumns() {
if (!this.totalWidth) {
throw new Error("列数为0");
}
if (
(!this.itemWidth || this.itemWidth <= 0) &&
!(this.maxItemWidth || this.maxItemWidth <= 0)
) {
throw new Error("列数为0");
}
let columns = 0;
if (this.maxItemWidth) {
columns =
Math.ceil((this.totalWidth + this.gap) /
(this.maxItemWidth + this.gap));
//根据计算出的列数,计算出实际的 ItemWidth,结果取地板,毕竟像素不存在小数,整体可以少个一个半个像素的,但是越界可能会导致渲染出现问题
this.itemWidth = Math.floor(
(this.totalWidth + this.gap) / (columns)
) - this.gap;
console.log("maxItemWidth", columns);
} else if (this.itemWidth) {
//查找一共有几列中间有 this.gap,为了最后一个贴边,总体要加上一个gap,取地板即可
columns =
Math.floor((this.totalWidth + this.gap) /
(this.itemWidth + this.gap));
console.log("itemWidth", columns);
}
console.log('columns', columns, 'totalwidth', this.totalWidth, 'itemwidth', this.itemWidth)
this.columns = columns;
this.columnsHeight = new Array(this.columns).fill(0);
}
//传入宽高比 radio = width / height,给出应当插入的列
waterFallAtIndex(radio: number) {
if (!this.columns || this.columns === 1) return 0;
const height = this.itemWidth / radio;
let minIdx = 0;
this.columnsHeight.reduce((pre, cur, idx) => {
if (cur < pre) {
minIdx = idx;
return cur;
}
return pre;
}, this.columnsHeight[0]);
this.columnsHeight[minIdx] += height;
return minIdx;
}
}
加入 ui 实现一个简易的瀑布流
写一个简单的 react 测试案例
const waterfall = useRef<WaterFallModel | null>();
const [waterDataSource, setWaterDataSource] = useState<any[][]>([]);
const generateData = async () => {
const dom = document.querySelector("#alalala");
if (!dom) return;
const rect = dom.getBoundingClientRect();
const water = (waterfall.current! = new WaterFallModel(rect.width));
water.maxItemWidth = 200;
water.generateColumns();
const datasource: any[][] = new Array(water.columns);
for (let idx = 0; idx < 100; idx++) {
//生成100个随机数据,radio随机生成
sleep(3);
let radio = (new Date().getTime() % 10) / 8;
if (radio < 0.5) {
radio += 0.5;
}
const index = water.waterFallAtIndex(radio);
console.log(index);
if (!datasource[index]) {
datasource[index] = [];
}
datasource[index].push({
radio,
content: `我是第${idx + 1}条数据`,
});
}
console.log("datasource", datasource);
setWaterDataSource(datasource);
};
const sleep = (interval: number) => {
return new Promise((resolve) => {
setTimeout(resolve, interval);
});
};
<div
id="alalala"
style={{
width: "100%",
height: "100%",
display: "flex",
}}
>
{waterDataSource.map((list, index) => (
<div
key={index}
style={{
width: waterfall.current?.itemWidth,
marginLeft: index > 0 ? waterfall.current?.gap : 0,
display: "flex",
flexDirection: "column",
}}
>
{list.map((item, idx) => (
<div
key={idx}
style={{
width: "100%",
marginTop:
idx > 0 ? waterfall.current?.gap : 0,
height:
waterfall.current!.itemWidth /
item.radio,
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: "red",
color: "white",
}}
>
{item.content}
</div>
))}
</div>
))}
</div>
最后
瀑布流就做到这里了,就这点逻辑也花费一个小时时间,可以根据需要,将 瀑布流的 UI + 计算数据model,组合成一个通用组件,应用到项目里,这里就不多做了,仅仅作为一个思路,毕竟封装成一个组件也不难,就是稍微多花点时间
最后,祝大家新年快乐,发发发 🤣🤣🤣🤣🤣🤣