使用react写一个瀑布流

332 阅读5分钟

前言

有时候我们展示一些图片布局的时候,为了能够让用户一个页面能够展示更多的内容,并且还要展示较全的图片内容,但是图片比例是不一样的,有横屏竖屏,并且同时竖屏百分比也不一样,为了能够展示的更优秀,因此有了瀑布流效果

先看一个测试案例效果,我们实现了一个类似的效果,当然要是加上图片就更好看了,这里就不加了

image.png

瀑布流

想要实现瀑布流,至少要知道瀑布流的几个特征,即需要做的一些事情

瀑布流要做的那些事

  • 需要了解布局宽度、单个item的宽度,或者最大宽度,能够保证一瓶放置多少列,也可以写死列数,然后根据列数自动填充(这里就先不支持写死列数了,这个逻辑更简单)
  • 可以通过,最大宽度计算出,最多可以计算出大致多少列,保持列数,适当缩小item宽度即可计算出实际的 item 宽度,当然要参考间隙 gap
  • 计算出一共有多少列后,可以在屏幕上放置指定数量的,竖向排版的布局,提前计算好数据应该在哪一列,然后直接一次渲染所有列即可

实现步骤

  1. 为了更通用,我们编写一个 WaterFallModel,用于保存瀑布流的基础信息,包括总宽度totalWidth(布局不一定能用到,计算用),item 宽度 itemWidth, 间隙 gap,最大item宽度 maxItemWidth(通过该参数配合总宽度可以计算出比较合理的 item 宽度),列数 column 是用于保存计算的 column 结果,每列布局的高度 columnsHeight 用于计算下一个元素应该布局到哪一列
  2. 编一个一个 generateColumns 用于计算出实际的 columns,这个主要是针对于 maxItemWidth,顺便计算出比较合适的 itemWidth,保存下来
  3. 编写一个 waterFallAtIndex 用于计算出下一个元素应该放置在哪一列,再布局的时候,能够保证新元素总是插入到最矮的那一列
  4. 计算好之后,只需要横向同时部署多列纵向布局,将生成的数据填充渲染即可实现瀑布流效果

实现 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,组合成一个通用组件,应用到项目里,这里就不多做了,仅仅作为一个思路,毕竟封装成一个组件也不难,就是稍微多花点时间

最后,祝大家新年快乐,发发发 🤣🤣🤣🤣🤣🤣