最近在尝试实现真正的瀑布流布局时发现,大部分基于纯CSS的解决方案都是伪瀑布流,无法满足子元素不定高度的需求。后来想到一种flex布局结合计算的方法,完美实现瀑布流布局。接下来,我将分享一下实现的基本原理以及一些关键技术细节。
一、Columns
最简单的方法,但是排布顺序从上到下
<div className="columns-3 gap-4">
{urls.map((url, index) => (
<img src={url} key={index} className="w-full mb-4" />
))}
</div>
二、Grid
Grid 布局一行顺序是从左到右,但是每一行会被最长的一项撑开
<div className="grid grid-cols-3 gap-4">
{urls.map((url, index) => (
<img src={url} key={index} className="w-full mb-4" />
))}
</div>
三、Flex 布局
Flex布局:按照图片顺序依次放入各列队列中进行展示,但空间利用率不够,会出现长短列问题。
const col = 3;
const list = Array.from({ length: col }, () => new Array()); // 图片队列
urls.forEach((item, index) => {
list[index % col].push(item); // 将图片分组
});
return (
<>
<div className="flex gap-4 flex-wrap">
{list?.map((vals, index) => (
<div className="flex flex-col gap-4 w-0 flex-1" key={index}>
{vals?.map((url) => (
<img src={url} key={index} className="w-full" />
))}
</div>
))}
</div>
</>
);
四、Flex 计算布局
按照 flex 布局,每次渲染图片前,先找到高度最短列,将图片放入。
export function CardItem(props: IProps) {
const { children, renderNext } = props;
// 包裹每个内容,挂载后触发下一次渲染
useEffect(() => {
renderNext();
}, []);
return children;
}
export default function WaterFull() {
const col = 3;
const initList = useRef<React.ReactNode[]>(cloneDeep(urls));
// 放置分配后的渲染元素队列
const [renderList, setRenderList] = useState(Array.from({ length: col }, () => new Array()));
// 找到最短列,渲染下一个
const renderNext = () => {
if (initList?.current?.length > 0) {
const data = cloneDeep(initList?.current);
// 获取当前元素
const item = data.shift();
initList.current = data;
if (!!item) {
const heightArr = [];
for (let i = 0; i < col; i++) {
// 获取每列高度
heightArr.push(document.getElementById(`list${i}`)?.clientHeight ?? Infinity);
}
// 找到最短列下标
let minH = Infinity,
minIndex = 0;
heightArr.forEach((h, index) => {
if (h < minH) {
minH = h;
minIndex = index;
}
});
// 将当前元素放入最短列
setRenderList((pre) => {
const res = cloneDeep(pre);
res[minIndex].push(item);
return res;
});
}
}
};
// 开始触发第一个元素渲染
useLayoutEffect(() => {
renderNext();
}, []);
return (
<div className="w-full">
<div className="flex gap-5 max-w-full items-start">
{renderList?.map((list, listIndex) => (
<div className="flex gap-5 flex-col flex-1 w-0" id={`list${listIndex}`} key={listIndex}>
{list?.map((url: string, index) => (
<CardItem renderNext={renderNext} key={index}>
<img src={url} className="w-full" />
</CardItem>
))}
</div>
))}
</div>
</div>
);
}
可以看到效果还是不够完美,因为放置下一个元素之前图片还未请求到,因此内部高度还为0。修改一下,将下一次渲染时间放置图片信息返回之后,达到最完美展示效果。
// 有改动部分代码
return (
<div className="w-full">
<div className="flex gap-5 max-w-full items-start">
{renderList?.map((list, listIndex) => (
<div className="flex gap-5 flex-col flex-1 w-0" id={`list${listIndex}`} key={listIndex}>
{list?.map((url: string, index) => (
<img src={url} className="w-full" onLoad={renderNext} key={index} />
))}
</div>
))}
</div>
</div>
);
如果不是单一图片形式,则可用useEffect触发,也可实现完美展示效果,但效果不够稳定(偶现)。
export function CardItem(props: IProps) {
const { children, renderNext } = props;
const timerId = useRef<any>(); // 定时器id
// 包裹每个内容,挂载后触发下一次渲染
useEffect(() => {
timerId.current = setTimeout(() => {
renderNext();
if (timerId.current) {
// 清除定时器
clearTimeout(timerId.current);
timerId.current = null;
}
}, 100);
}, []);
return children;
}
// 渲染卡片代码
<div className="flex gap-5 max-w-full items-start">
{renderList?.map((list, listIndex) => (
<div className="flex gap-5 flex-col flex-1 w-0" id={`list${listIndex}`} key={listIndex}>
{list?.map((item: React.ReactNode, index) => (
<CardItem key={index} renderNext={renderNext}>
{item}
</CardItem>
))}
</div>
))}
</div>