响应式瀑布流布局

Jan 07, 2024· 5 min read

响应式瀑布流布局Waterfall Flow Layout

瀑布流是一种流行的网页布局。这种效果的名称取自瀑布,悬挂在高处并自顶向下流淌,就像这种布局的视觉效果一样。瀑布流布局将元素按照垂直方向排列,以达到优化页面空间的效果,通常很受图片-heavy 网站的欢迎。

原理:对每个 item 都使用绝对定位,left 和 top 都是 0, 最后根据容器大小、item 的高度通过计算来得到 item 的 transform 值

初始化结构

我们先搭建一个简单的结构,然后为 container 设置一个 ref,在 useEffect 中获取所有 item 元素,并将其保存到 items 数组中。

TSX
import { useRef } from'react'; function App() { const list = [ '/photo-1.avif', '/photo-2.avif', '/photo-3.avif', '/photo-4.avif', '/photo-5.avif', '/photo-6.avif', '/photo-7.avif', '/photo-8.avif', '/photo-9.avif', '/photo-10.avif', '/photo-11.avif', '/photo-12.avif' ]; const container = useRef<HTMLDivElement | null>((null); const [items, setItems] = useState<HTMLDivElement[]>([]); // 保存所有 items 元素 useEffect(() => { if (container.current) { setItems(Array.from(container.current.querySelectorAll('.item'))); } }, []) return ( <div ref={container} className="container" > {list.map((item) => ( <div className="item" key={item} > <img src={item} /> </div> ))} </div> ) }
CSS
html, body { width: 100%; height: 100%; } * { margin: 0; padding: 0; box-sizing: border-box; } #root { width: 100%; height: 100%; --item-width: 0px; } .container { position: relative; width: 100%; } .item { position: absolute; width: var(--item-width); top: 0; left: 0; padding: 10px; border: 1px solid #eee; border-radius: 5px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); background-color: #fff; } .item img { width: 100%; object-fit: cover; }

监听窗口大小变化

由于我们需要监听窗口大小变化,所以我们可以使用 ResizeObserver 监听容器的大小变化。 主要逻辑在 handleResize 函数中。

TSX
useEffect(() => { const observer = new ResizeObserver(() => handleResize()); if (container.current) { observer.observe(container.current); } return () => { observer.disconnect(); }; }, []);

计算 item 的 transform 值 和 item 的宽度

  1. 首先我们可以获取容器的宽度,然后根据宽度来计算每一行放置多少 item,并计算每一个 item 的宽度。
  2. 创建一个 columnsHeights 数组用来保存每一列的高度,初始化为 0。
  3. 遍历所有 item,根据 item 的宽度来计算 item 的 transform 值,同时更新 columnHeights 的值。
TSX
function getColumns(containerWidth: number) { if (containerWidth > 1200) { return 6; } else if (containerWidth > 768 && containerWidth <= 1200) { return 4; } else { return 3; } } const handleResize = () => { if (container.current) { const containerWidth = container.current.offsetWidth; const columns = getColumns(containerWidth); const columnWidth = containerWidth / columns; container.current.style.setProperty('--item-width', columnWidth + 'px'); const columnHeights = new Array(columns).fill(0); // 初始化高度为 0 // 计算每个 item 的 transform 值 items.forEach((item) => { const shortestColumn = columnHeights.indexOf(Math.min(...columnHeights)); item.style.transform = `translate(${columnWidth * shortestColumn}px, ${ columnHeights[shortestColumn] }px)`; // 更新 columnHeights 的值 columnHeights[shortestColumn] += item.offsetHeight; }); } };

完整代码

TSX
import { useCallback, useEffect, useRef, useState } from 'react'; import './App.css'; function getColumns(containerWidth: number) { if (containerWidth > 1200) { return 6; } else if (containerWidth > 768 && containerWidth <= 1200) { return 4; } else { return 3; } } function App() { const list = [ '/photo-1.avif', '/photo-2.avif', '/photo-3.avif', '/photo-4.avif', '/photo-5.avif', '/photo-6.avif', '/photo-7.avif', '/photo-8.avif', '/photo-9.avif', '/photo-10.avif', '/photo-11.avif', '/photo-12.avif' ]; const container = useRef<HTMLDivElement | null>(null); const [items, setItems] = useState<HTMLDivElement[]>([]); useEffect(() => { if (container.current) { const items = Array.from( document.querySelectorAll('.item') ) as HTMLDivElement[]; setItems(items); } }, []); const handleResize = useCallback(() => { if (container.current) { const containerWidth = container.current.offsetWidth; const columns = getColumns(containerWidth); const columnWidth = containerWidth / columns; container.current.style.setProperty('--item-width', columnWidth + 'px'); const columnHeights = new Array(columns).fill(0); items.forEach((item) => { const shortestColumn = columnHeights.indexOf( Math.min(...columnHeights) ); item.style.transform = `translate(${columnWidth * shortestColumn}px, ${ columnHeights[shortestColumn] }px)`; columnHeights[shortestColumn] += item.offsetHeight; }); } }, [items]); useEffect(() => { const observer = new ResizeObserver(() => handleResize()); if (container.current) { observer.observe(container.current); } return () => { if (observer) { observer.disconnect(); } }; }, [handleResize]); return ( <div ref={container} className="container" > {list.map((item) => ( <div className="item" key={item} > <img src={item} /> </div> ))} </div> ); } export default App;

总结

瀑布流布局主要就是需要计算每行最小高度,然后根据最小高度来计算每个 item 的位置。 使用 transform 控制每个 item 的位置可以避免不必要的重排。