组件

ListView

ListView 是一个由你渲染各行的滚动列表。和 List 一样,它只创建屏幕附近的行,并在滚动时复用单元格,因此一个上千项的列表,开销与可见行数大致相当。ListView 在此之上提供外观:每行的内边距、行间分隔线,以及可选的 inset 样式。

ui-list-view.tsx 在线编辑 在新标签页打开
// ListView: 1,000 recycled rows the app renders; plain or inset, with or without dividers.
import { mount, signal, Text, View } from "nsh";
import { Button, ListView } from "nsh/ui/mobile";

const people = Array.from({ length: 1000 }, (_, i) => ({
  id: i,
  name: `Person ${i + 1}`,
  role: ["Design", "Engineering", "Sales", "Support"][i % 4],
}));

function Header(props: { inset: () => boolean; dividers: () => boolean; toggleInset: () => void; toggleDividers: () => void }) {
  return (
    <View style={{ padding: 16, paddingTop: 24, gap: 8 }}>
      <Text style={{ fontSize: 22, fontWeight: "700" }}>ListView</Text>
      <View style={{ flexDirection: "row", gap: 8 }}>
        <Button class="flex-1" variant="secondary" title={props.inset() ? "Plain" : "Inset"} onPress={props.toggleInset} />
        <Button
          class="flex-1"
          variant="secondary"
          title={props.dividers() ? "No dividers" : "Dividers"}
          onPress={props.toggleDividers}
        />
      </View>
    </View>
  );
}

function App() {
  const inset = signal(false);
  const dividers = signal(true);
  return (
    <ListView
      variant={inset() ? "inset" : "plain"}
      dividers={dividers()}
      each={people}
      keyExtractor={(p) => String(p.id)}
      ListHeaderComponent={
        <Header
          inset={inset}
          dividers={dividers}
          toggleInset={() => inset.set(!inset())}
          toggleDividers={() => dividers.set(!dividers())}
        />
      }
    >
      {(person) => (
        <View style={{ flexDirection: "row", alignItems: "center" }}>
          <Text style={{ flex: 1, fontWeight: "600" }}>{person().name}</Text>
          <Text style={{ color: "#737373" }}>{person().role}</Text>
        </View>
      )}
    </ListView>
  );
}

mount(App);
import { ListView } from "nsh/ui/mobile";

#用法

和 List 一样,用 each 传入数据,用子函数渲染每一行:

import { mount, Text, View } from "nsh";
import { ListView } from "nsh/ui/mobile";

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

function App() {
  return (
    <ListView each={people} keyExtractor={(p) => String(p.id)}>
      {(person, index) => (
        <View style={{ flexDirection: "row", justifyContent: "space-between" }}>
          <Text style={{ fontWeight: "600" }}>{person().name}</Text>
          <Text style={{ color: "#737373" }}>{`#${index() + 1}`}</Text>
        </View>
      )}
    </ListView>
  );
}

mount(App);

行函数接收的是 accessor:person() 和 index()。它对每个单元格只运行一次;单元格被复用来显示另一项时,accessor 的值随之变化。每项的状态请保存在数据里,不要放在行内创建的 signal 中。回收机制的细节见列表。

ListView 会填满父视图(flex: 1),请把它放在有高度的父视图中,比如根视图或 Screen。

#行

ListView 把每一行包进一个视图:水平内边距 16 pt,垂直内边距 12 pt,最小高度 48 pt,内容垂直居中。开启 dividers(默认开启)时,除第一行外每一行顶部都有一条 1 pt 分隔线。

列表末尾会额外加上与安全区域底部 inset 等高的内边距,这样最后一行可以滚动到 Home 指示条上方。

#变体

变体外观
plain(默认)通栏行,铺在 background 颜色上。
inset行放在带 16 pt 外边距的圆角框里,页面背景为 muted.background,类似分组的设置页。第一行顶部圆角,最后一行底部圆角。
<ListView variant="inset" each={settings} dividers={false}>
  {(item) => <Text>{item().label}</Text>}
</ListView>

#页眉、页脚与列表行为

ListView 接受 List 的数据类 props。布局(预估行高、内边距)由 ListView 自己决定,所以 List 的布局类 props,如 numColumns、horizontal、estimatedItemSize 和 contentContainerStyle,在这里不可用。

<ListView
  each={messages()}
  keyExtractor={(m) => m.id}
  ListHeaderComponent={<Text style={{ padding: 16, fontSize: 22, fontWeight: "700" }}>Inbox</Text>}
  ListEmptyComponent={<Text style={{ padding: 16 }}>No messages</Text>}
  refreshing={loading()}
  onRefresh={reload}
  onEndReached={loadMore}
>
  {(m) => <Text numberOfLines={1}>{m().subject}</Text>}
</ListView>

#Props

Prop类型默认值说明
eachreadonly T[]必填列表项。
children(item: () => T, index: () => number) => JSX.Element必填渲染一行。每个回收单元格调用一次。
variant"plain" | "inset""plain"通栏行,或带外边距的圆角框内的行。
dividersbooleantrue行间细分隔线。
keyExtractor(item: T, index: number) => stringitem.key、item.id 或索引列表项的 key。
ListHeaderComponentListSlot无显示在第一行之前(组件或元素)。
ListFooterComponentListSlot无显示在最后一行之后。
ListEmptyComponentListSlot无each 为空时代替各行显示。
stickyHeaderIndicesnumber[]无滚动经过时吸附在顶部的项。
initialScrollIndexnumber无在第一帧之前滚动到该项。
refreshingbooleanfalse显示下拉刷新指示器。
onRefresh() => void无用户下拉刷新。加载期间请设置 refreshing。
onEndReached() => void无接近列表末尾(用于加载更多)。
onEndReachedThresholdnumber1距末尾多近(以视口高度计)时触发 onEndReached。
onViewableItemsChanged(info) => void无有项变为可见或不再可见。
viewabilityConfigViewabilityConfig无何时算作可见。
class、classList、style、testID、onLayout、ref作用于列表的根视图。

ListView 没有自己的事件,上面的列表 props 就是它的回调。

#主题

部位主题键
页面背景background(plain)、muted.background(inset)
行背景(inset)background
文字颜色foreground
分隔线border
inset 圆角框的圆角radius.lg

参见主题。

#无障碍

ListView 不给行添加任何角色或标签。如果某一行可按,请给它的视图设置 accessibilityRole,必要时再加上 accessibilityLabel。

#相关内容

  • List:ListView 所基于的虚拟化列表,可完全控制布局。
  • 列表指南:回收、key、分页。
  • SelectList:带勾号的选项列表。
  • Group:圆角框内的少量行,不做虚拟化。