灏天阁

基于 React 的瀑布流组件

· Yin灏

瀑布流布局的定义

一般来说,把多列等宽、元素高度不一的页面布局认为是瀑布流布局.

Screen Shot 2022-10-06 at 5.57.36 PM.png

瀑布流的特点

瀑布流的特点通常包括以下:

  • 多列布局
  • 每列等宽
  • 高度参差不齐(这是和常规列表不一样的地方,造成不规则的效果)
  • 大量元素展示(通常是图片或图文并存)

因此,当我们根据获得的列表进行展示的时候,引申出排版的规律:

  • 优先插入高度小的那一列
  • 同等高度,优先左侧

动手开工

组件属性

根据上面的归纳,可以为Waterfall组件得到以下属性

  • list: 传入需要进行瀑布流布局的数据列表,每个元素必须包含 height 和 width 字段
  • cols: 瀑布流布局展示的列数
  • width: 瀑布流布局的总宽度,单位为 px
  • margin: 列与列之间的间隔距离,默认为 20,单位为 px
<Waterfall list={waterfallList} cols={4} width={450} margin={10}></Waterfall>;

计算每列的宽度

根据colswidthmargin,计算出每列的宽度。

const defaultColWidth = (width - (cols - 1) * margin) / cols;

分配&插入队列

接下来,要实现一个插入分配的函数。

  1. 依据cols的数目,创建一个用于存储元素的二维数组arr
  2. 创建一个用于记录每列高度的数组
  3. 遍历传入的list
    1. 获取元素的heightwidth字段
    2. 根据之前得到的每列宽度,计算出元素实际的显示高度
    3. 获取所有列的当前高度,把元素插入到高度最小列,并更新该列的宽度
  4. 返回所有元素都插入完成arr

显示瀑布流

根据上一步得到的数组进行显示,大功告成。

Screen Shot 2022-10-06 at 11.47.00 PM.png

代码展示

特别说明一下,这里有一个字段fields,是作为业务展示数据传入,这里可以针对业务场景,做定制化的展示,这里就不再赘述。

function allocateItems(ls, len, colWidth) {
  /** 各个瀑布流的内容列表 */
  const arr = [];
  /** 各个瀑布流的高度列表 */
  const heightArr = [];

  // 初始化瀑布流的内容列表和高度列表
  for (let i = 0; i < len; i++) {
    arr.push([]);
    heightArr.push(0);
  }

  /** 获取高度最小的流的索引值 */
  function getIndexOfMinHeightFlow() {
    let minH = Number.MAX_SAFE_INTEGER;
    let minIndex = 0;
    heightArr.forEach((h, index) => {
      if (h < minH) {
        minH = h;
        minIndex = index;
      }
    });
    return minIndex;
  }

  // 通过计算展示高度,设置瀑布流的内容列表
  ls.forEach((item, idx) => {
    const index = getIndexOfMinHeightFlow();
    item.displayHeight = (item.height * colWidth) / item.width;
    arr[index].push(item);
    heightArr[index] += item.displayHeight;
  });
  return arr;
}

const Waterfall = (props) => {
  const { list, cols = 1, width, margin = 20, fields } = props;
  const defaultColWidth = (width - (cols - 1) * margin) / cols;
  const [colList, setColList] = useState(
    allocateItems(list, cols, defaultColWidth)
  );

  useEffect(() => {
    setColList(allocateItems(list, cols, defaultColWidth));
  }, [list, cols, defaultColWidth]);

  return (
    <div className="waterfall" style={{ width: width + "px" }}>
      {colList.map((col, fIndex) => (
        <ul
          className="waterfall-list"
          style={{ width: defaultColWidth + "px" }}
          key={"flow_" + fIndex}
        >
          {col.map((item, iIndex) => (
            <li
              className="waterfall-item"
              key={
                "flow_" + fIndex + "_item_" + iIndex + "_" + item.displayHeight
              }
              style={{ height: item.displayHeight + "px" }}
              title={item.text || ""}
            >
              {/* 业务展示UI */}
            </li>
          ))}
        </ul>
      ))}
    </div>
  );
};

最后补充

上面的代码逻辑假定了已知元素的高宽,但是如果后端接口没有给我们图片大小的话,我们要怎样做呢?

可以是在allocateItems()里面,添加new Image请求图片并监听onLoad事件,获取图片的实际大小再进行计算。

- Book Lists -