FlatList 虚拟列表

注:

  • 需要给 FlatList 组件一个固定高度,通过 CSS 设置 height。
  • 如果列表内容存在不固定宽高的图片,由于图片加载的机制,每个 item 的 DOM 渲染了,图片可能还未加载,导致每个 item 的位置计算错误,所以作为调用方,需要将未加载的图片用样式固定住。在已知宽高比(比如常见的正方形商品图),可以使用 Image 组件内置 aspectRatio 来固定图片宽高。
  • 该组件使用的是 Virtual List 虚拟列表技术,将渲染的数据控制在一定范围内,在海量数据滚动中达到更好的性能和体验。
  • 如果数据量过大(大几百以上),强烈建议设置 itemSize,未设置时使用动态计算高度会带来渲染性能的下降。
import {
  TaFlatList,
  TaCell,
  TaGroup,
  TaEmpty,
  ViewPosition,
  FlatListOnRefreshing,
  showToast,
  FlatListRef,
  FlatListOnEndReached,
  FlatListOnVisibleItemsChange
} from '@/index'
import { useLayoutEffect, useRef, useState } from 'react'

interface ExpList {
  id: number
  text: string
}

// 数据初始化
const list: ExpList[] = []
const largeList: ExpList[] = []
for (let i = 0; i < 100; i++) {
  list.push({
    id: i + 1,
    text: `${i + 1} 个列表`
  })
}
for (let i = 0; i < 100000; i++) {
  largeList.push({
    id: i + 1,
    text: `${i + 1} 个列表`
  })
}

export default function ExpFlatList() {
  // 瀑布流
  function getItemSize(index: number) {
    return 50 + (index % 10) * 2
  }

  // 下拉刷新
  const onRefreshing: FlatListOnRefreshing = (res, done) => {
    setTimeout(() => {
      showToast({
        title: `刷新成功`,
        type: 'success'
      })
      done()
    }, 2000)
  }

  // 加载更多
  const [lowerLoading, setLowerLoading] = useState(true)
  const [loadList, setLoadList] = useState<ExpList[]>([])

  function getLoadList() {
    const newLoadingList = [...loadList]

    for (
      let i = newLoadingList.length, len = newLoadingList.length + 10;
      i < len;
      i++
    ) {
      newLoadingList.push({
        id: i + 1,
        text: `${i + 1} 个列表`
      })
    }

    setLoadList(newLoadingList)

    return newLoadingList.length
  }

  useLayoutEffect(() => {
    getLoadList()
  }, [])

  const onLoadMore: FlatListOnEndReached = res => {
    console.log('end-reached', res)

    const max = 100

    if (loadList.length >= max) {
      return
    }

    setTimeout(() => {
      showToast({
        title: `加载成功`,
        type: 'success'
      })

      if (getLoadList() >= max) {
        setLowerLoading(false)
      }
    }, 500)
  }

  // 事件监听
  const onVisibleItemsChange: FlatListOnVisibleItemsChange = ({ items }) => {
    console.log('visible-items-change', items)

    items.forEach(({ index, visible }) => {
      index === 49 && showToast(`index: ${index}, visable: ${visible}`)
    })
  }
  const onEndReached: FlatListOnEndReached = res => {
    console.log('end-reached', res)
    showToast(`到底了`)
  }

  // 方法调用
  const methodList = useRef<FlatListRef>(null)

  function scrollToIndex(index: number, viewPosition: ViewPosition = 0) {
    methodList.current?.scrollToIndex({ index, viewPosition })
  }
  function scrollTo(offset: number) {
    methodList.current?.scrollTo({ offset })
  }
  function scrollToEnd(animated: boolean) {
    methodList.current?.scrollToEnd(animated)
  }

  return (
    <>
      <TaGroup title="基础用法">
        <TaFlatList
          className="exp-flatList-box"
          ids={list.map(v => v.id)}
          render={({ index }) => (
            <div className="exp-flatList-item">{list[index].text}</div>
          )}
        />
      </TaGroup>
      <TaGroup title="水平列表">
        <TaFlatList
          className="exp-flatList-box"
          itemSize={140}
          initialHorizontal
          ids={list.map(v => v.id)}
          render={({ index }) => (
            <div className="exp-flatList-item">{list[index].text}</div>
          )}
        />
      </TaGroup>
      <TaGroup title="开启下拉刷新">
        <TaFlatList
          className="exp-flatList-box"
          itemSize={50}
          enablePullRefresh
          onRefreshing={onRefreshing}
          ids={list.map(v => v.id)}
          render={({ index }) => (
            <div className="exp-flatList-item">{list[index].text}</div>
          )}
        />
      </TaGroup>
      <TaGroup title="展示底部加载更多提示">
        <TaFlatList
          className="exp-flatList-box"
          lowerLoading={lowerLoading}
          ids={loadList.map(v => v.id)}
          render={({ index }) => (
            <div className="exp-flatList-item">{loadList[index].text}</div>
          )}
          onEndReached={onLoadMore}
        />
      </TaGroup>
      <TaGroup title="分割线(#separator)">
        <TaFlatList
          className="exp-flatList-box"
          ids={list.map(v => v.id)}
          render={({ index }) => (
            <div className="exp-flatList-item">{list[index].text}</div>
          )}
          renderSeparator={({ index }) =>
            index < list.length - 1 ? (
              <div className="exp-flatList-item-separator"></div>
            ) : (
              <></>
            )
          }
        />
      </TaGroup>
      <TaGroup title="瀑布流">
        <TaFlatList
          className="exp-flatList-box"
          itemSize={getItemSize}
          initialWaterfallCount={3}
          ids={list.map(v => v.id)}
          render={({ index }) => (
            <div className={`exp-flatList-item color-${index % 10}`}>
              {list[index].text}
            </div>
          )}
        />
      </TaGroup>
      <TaGroup title="事件监听(end-reached/visible-items-change)">
        <TaFlatList
          className="exp-flatList-box"
          itemSize={50}
          ids={list.map(v => v.id)}
          render={({ index }) => (
            <div className="exp-flatList-item">{list[index].text}</div>
          )}
          onEndReached={onEndReached}
          onVisibleItemsChange={onVisibleItemsChange}
        />
      </TaGroup>
      <TaGroup title="Slot empty">
        <TaFlatList
          className="exp-flatList-box"
          ids={[]}
          renderEmpty={() => <TaEmpty description="暂无列表" />}
          render={() => <></>}
        />
      </TaGroup>
      <TaGroup title="Method">
        <TaFlatList
          className="exp-flatList-box"
          ids={largeList.map(v => v.id)}
          ref={methodList}
          itemSize={50}
          render={({ index }) => (
            <div className={`exp-flatList-item color-${index % 10}`}>
              {largeList[index].text}
            </div>
          )}
        />
        <TaCell
          label="scrollToIndex({ index: 49999 })"
          isLink
          onClick={() => scrollToIndex(49999)}
        ></TaCell>
        <TaCell
          label="同上加 viewPosition=0.5"
          isLink
          onClick={() => scrollToIndex(49999, 0.5)}
        ></TaCell>
        <TaCell
          label="同上加 viewPosition=1"
          isLink
          onClick={() => scrollToIndex(49999, 1)}
        ></TaCell>
        <TaCell
          label="scrollTo({ offset: 200 })"
          isLink
          onClick={() => scrollTo(200)}
        ></TaCell>
        <TaCell
          label="scrollToEnd(true)"
          isLink
          onClick={() => scrollToEnd(true)}
        ></TaCell>
      </TaGroup>
    </>
  )
}

Import

import { TaFlatList } from 'tantalum-ui-mobile-react'

具体的引入方式可以参考引入组件

Import Type

组件导出的类型定义:

import type {
  FlatListOnRefreshing,
  FlatListOnScroll,
  FlatListOnEndReached,
  FlatListOnVisibleItemsChange,
  FlatListRef
} from 'tantalum-ui-mobile-react'

Props

属性类型默认值必填说明
ids(string | number)[]id 列表,会在渲染的时候作为参数回调
horizontalbooleanfalse设置为 true 则变为水平布局模式
itemSizenumber | (index: number) => number设置列表项尺寸(垂直布局下指高度,水平布局下指宽度),推荐设置,提升性能
endReachedThresholdnumber0.5决定当距离内容最底部还有多远时触发 onEndReached 回调。注意此参数是一个比值而非像素单位。比如,0.5 表示距离内容最底部的距离为当前列表可见长度的 50%时触发
enablePullRefreshbooleanfalse是否开启下拉刷,如果时水平列表则为左拉刷新,搭配 onRefreshing 事件使用新
lowerLoadingbooleanfalse开启后会在列表最后展示加载更多效果,配合 onEndReached 事件加载无限列,当多次加载完所有数据后设置为 false 关闭
initialWaterfallCountnumber1开启瀑布流展示并设置瀑布流列数,支持 1 ~ 5 列,1 就是没开
approvedItemVisibleScalenumber0.5可选 0-1, 主要是配合 onVisibleItemsChange 事件使用,0.5 表示每项需要展示超过自身 50%才认为是可视 visible=true,在一些数数据统计和展示触发行为常用
preLoadnumber1.5预加载屏数,1.5 标识在在可视窗口之外再预加载大于 1.5 个窗口的数据,防止滚动过程中产生的白屏。预加载的屏数设置越高,加载的数据越多,高速滚动产生白屏的几率越小,但是相应性能消耗更大

Events

事件描述回调函数参数TypeScript 函数
onVisibleItemsChange列表项可视情况变化时触发payload: { items: { index: number, visible: boolean }[] }FlatListOnVisibleItemsChange
onEndReached滚动到末尾时触发payload: { distanceFromEnd: number } 其中 distanceFromEnd 为距离末尾的距离,单位 pxFlatListOnEndReached
onScroll滚动时触发payload: { scrollLeft: number, scrollTop: number, scrollWidth: number, scrollHeight: number }FlatListOnScroll
onRefreshing下拉刷新时触发payload: ({ pullDirection: 'up' | 'right' | 'down' | 'left' }, done: () =>void) 其中 pullDirection 指下拉的方向,done 指刷新完毕回调的函数FlatListOnRefreshing

onVisibleItemsChange 的 items 参数

item类型说明
visibleboolean这里的可视情况受到 approvedItemVisibleScale 字段影响,可以配置它来达到你认可的可视情况
indexnumber第 index 项

Slots

列表项(render)

<TaFlatList
  className="exp-flatList-box"
  ids={list.map(v => v.id)}
  render={({ index }) => (
    <div className="exp-flatList-item">{list[index].text}</div>
  )}
/>

列表为空(renderEmpty)

<TaFlatList
  className="exp-flatList-box"
  ids={[]}
  renderEmpty={() => <TaEmpty description="暂无列表" />}
  render={() => <></>}
/>

分割线(renderSeparator)

<TaFlatList
  className="exp-flatList-box"
  ids={list.map(v => v.id)}
  render={({ index }) => (
    <div className="exp-flatList-item">{list[index].text}</div>
  )}
  renderSeparator={({ index }) =>
    index < list.length - 1 ? (
      <div className="exp-flatList-item-separator"></div>
    ) : (
      <></>
    )
  }
/>

注: itemSize 设定值需要把分割线也考虑进去。

Methods

方法名说明参数
scrollToIndex将位于指定位置的元素滚动到可视区的指定位置({ index: number, animated: boolean, viewPosition: ViewPosition }) => void
scrollTo滚动列表到指定的偏移,单位 px({ offset: number, animated: boolean }) => void
scrollToEnd滚动到底部( animated: boolean ) => void
recordInteraction主动通知列表发生了一个事件,以使列表重新计算可视区域() => void

scrollToIndex 的参数

属性类型默认值必填说明
indexnumber列表第 index 项滚动到可视区的指定位置
animatedbooleantrue滚动过程中是否使用过度动画
viewPositionstring'start''start'/'center'/'end'/0/0.5/1 显示在屏幕的头部/中间/末尾位置