卡片流布局

49 阅读2分钟

最近在尝试实现真正的瀑布流布局时发现,大部分基于纯CSS的解决方案都是伪瀑布流,无法满足子元素不定高度的需求。后来想到一种flex布局结合计算的方法,完美实现瀑布流布局。接下来,我将分享一下实现的基本原理以及一些关键技术细节。

一、Columns

最简单的方法,但是排布顺序从上到下

<div className="columns-3 gap-4">
  {urls.map((url, index) => (
    <img src={url} key={index} className="w-full mb-4" />
  ))}
</div>

POPO-20240111-111926.png

二、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>

POPO-20240111-111837.png

三、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>
  </>
);

POPO-20240111-111723.png

四、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>
  );
}

POPO-20240111-111520.png

可以看到效果还是不够完美,因为放置下一个元素之前图片还未请求到,因此内部高度还为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>
  );

POPO-20240111-111059.png

如果不是单一图片形式,则可用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>

POPO-20240111-111222.png