基于react的瀑布流组件

4,480 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第4天,点击查看活动详情

在日常工作的过程中,搭建瀑布流式的页面布局是一个经常出现的业务场景。

背景

瀑布流布局的定义

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

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事件,获取图片的实际大小再进行计算。