元素

List

<List> 用来展示很长或无上限的数据。只有视口附近的行才真正存在:用户滚动时,离开视口的行会被回收,用来显示新进入的行。哪些行存在、它们的位置、多列布局、吸顶标题和可见性都在原生侧计算,所以滚动一个上千项的列表,开销和二十项的列表差不多。

import { mount, Text, View } from "nsh";
import { List } from "nsh/list";

const contacts = Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `Contact ${i + 1}` }));

function App() {
  return (
    <List each={contacts} estimatedItemSize={56} style={{ flex: 1 }}>
      {(contact, index) => (
        <View style={{ height: 56, paddingHorizontal: 16, justifyContent: "center" }}>
          <Text style={{ fontSize: 16 }}>
            {index() + 1}. {contact().name}
          </Text>
        </View>
      )}
    </List>
  );
}

mount(App);

List 从 nsh/list 导入。它是泛型组件:List<T> 会检查 each 与行函数的条目类型是否一致,TypeScript 会从 each 推断出条目类型。

如果列表很短、所有行都应该存在(设置页、表单),在 <Scroll> 里用 <Each> 更简单。分组表格样式的列表见 ListView。列表指南 介绍了常见模式:无限加载、聊天、分组。

#行会被复用

行函数对每个单元格执行一次,而不是对每个条目执行一次。单元格被复用来显示另一个条目时,它的 item() 和 index() 访问器会变化,读取它们的地方随之更新:

<List each={messages()}>
  {(message) => <Text>{message().body}</Text>}
</List>

因此,每个条目自己的状态要放在数据里(按 id 区分),不要放在行函数内部创建的 signal 里:那样的 signal 会跟着单元格走,出现在别的条目上。

#属性

属性类型默认值说明
eachreadonly T[]条目。
children(item: () => T, index: () => number) => JSX.Element渲染一行。
estimatedItemSizenumber50尚未测量的条目尺寸(点)。估值越接近,滚动条越稳定。
horizontalbooleanfalse横向滚动。
numColumnsnumber1每行的条目数(纵向列表)。
invertedbooleanfalse从末端开始布局,第 0 项在最后(聊天样式):初始滚动到末端,内容变化时保持与末端的距离。
stickyHeaderIndicesnumber[]滚动经过时吸附在起始端的条目(单列、非倒序)。
contentContainerStyleListContentStyle内容的内边距和间距(点):padding、paddingHorizontal、paddingVertical、paddingTop、paddingRight、paddingBottom、paddingLeft、gap、rowGap、columnGap。
ListHeaderComponentListSlot条目之前的内容:一个组件(Header)或一个元素(<Header />)。
ListFooterComponentListSlot条目之后的内容。
ListEmptyComponentListSloteach 为空时代替条目显示。
ItemSeparatorComponentListSlot条目之间的分隔(单列)。
initialScrollIndexnumber第一帧之前先滚动到该条目。
keyExtractor(item: T, index: number) => stringitem.key、item.id 或索引条目在可见性 token 中的 key。
onEndReached() => void快到末端了:加载更多。
onEndReachedThresholdnumber1距末端多近(以视口为单位)时触发 onEndReached。
onViewableItemsChanged(info: { viewableItems: ViewToken<T>[]; changed: ViewToken<T>[] }) => void有条目变为可见或不再可见。
viewabilityConfigViewabilityConfig条目何时算作可见(见下文)。
refreshingboolean显示下拉刷新的转圈。
onRefresh() => void用户下拉刷新。
showsIndicatorbooleantrue显示滚动条。
scrollValueNativeValue接收滚动偏移的原生值,用于随滚动变化的动画。
keyboardDismissMode"none" | "on-drag" | "interactive""none"拖动时是否收起键盘。
styleStyleProp<ViewStyle>列表自身的样式(需要有确定的尺寸:flex: 1 或高度)。
refYNode | (node: YNode) => void列表的节点(见 Ref 方法)。

列表同样支持 <Scroll> 的滚动属性:pagingEnabled、snapToInterval、snapToOffsets、snapToAlignment、disableIntervalMomentum、decelerationRate、bounces、contentInset、contentOffset、keyboardShouldPersistTaps、scrollEventThrottle 和 scrollEnabled。列表不支持缩放。

ViewabilityConfig:

字段类型说明
itemVisiblePercentThresholdnumber条目可见部分须达到的百分比。
viewAreaCoveragePercentThresholdnumber条目须覆盖视口的百分比。两个阈值都不设时,露出任何一个像素都算可见。
minimumViewTimenumber条目须保持可见多少毫秒才会上报。

ViewToken<T> 为 { item, key, index, isViewable }。

#事件

事件载荷时机
onScrollScrollEvent偏移发生变化。
onScrollBeginDrag / onScrollEndDrag{ x, y }开始 / 结束拖动。
onMomentumScrollBegin / onMomentumScrollEnd{ x, y }惯性滑动开始 / 结束。
onScrollEnd{ x, y }滚动停止。
onEndReached无距末端不足 onEndReachedThreshold 个视口。
onViewableItemsChanged{ viewableItems, changed }可见性变化。
onRefresh无下拉刷新。

#Ref 方法

方法说明
scrollToIndex({ index, animated?, viewPosition?, viewOffset? })把第 index 项滚动到视口内。viewPosition 决定位置:0 在视口起始处(默认),0.5 居中,1 在末端;viewOffset 再额外偏移。在原生侧按当前(已测量或估计的)条目尺寸计算。
scrollToOffset({ offset, animated? })沿列表方向滚动到某个偏移。
scrollToEnd({ animated? })滚动到内容末端(倒序列表则是起始端)。
scrollTo({ x?, y?, animated? })滚动到某个偏移。

#示例

带标题和可见性回调的网格:

const visible = signal("");
let list!: YNode;

<List
  ref={list}
  each={photos}
  numColumns={3}
  estimatedItemSize={120}
  contentContainerStyle={{ padding: 12, gap: 8 }}
  viewabilityConfig={{ itemVisiblePercentThreshold: 50 }}
  onViewableItemsChanged={({ viewableItems }) => visible.set(`${viewableItems.length} visible`)}
  ListHeaderComponent={<Text onPress={() => list.scrollToIndex({ index: 60, viewPosition: 0.5 })}>Jump to 60</Text>}
  style={{ flex: 1 }}
>
  {(photo) => <Image src={photo().url} style={{ height: 120, borderRadius: 8 }} />}
</List>;

聊天:

<List each={messages()} inverted estimatedItemSize={64} keyboardDismissMode="interactive" style={{ flex: 1 }}>
  {(m) => <Bubble message={m()} />}
</List>

第 0 项是最新的消息,画在最底部;新消息插到 each 开头时,用户正在看的内容不会跳动。

#平台说明

  • 在可见行之上插入条目时,两个平台都会保持可见行不动。
  • 从右到左布局中的横向列表在原生侧镜像。
  • 吸顶标题在每一帧滚动时于原生侧固定。