API
List
nsh 匯出 List:一個建基於原生捲動視圖的虛擬化清單。只有靠近可視範圍的列才會存在,而它們的儲存格會在捲動時重用。哪些列存在、位置在哪、欄數、黏性標題和可視狀態,全部由原生層計算,因此捲動一個有一萬個項目的清單時,除了儲存格重用時會變動的列綁定外,不會執行任何 JS。
import { List, type ListProps, type ViewToken, type ViewabilityConfig } from "nsh";
#List
function List<T>(props: ListProps<T>): JSX.Element;
import { List, mount, Text, View } from "nsh";
const contacts = Array.from({ length: 1000 }, (_, i) => ({ id: String(i), name: `Contact ${i}` }));
function App() {
return (
<List each={contacts} estimatedItemSize={48} style={{ flex: 1 }}>
{(item, index) => (
<View style={{ height: 48, paddingHorizontal: 16, justifyContent: "center", backgroundColor: index() % 2 ? "#f8fafc" : "#fff" }}>
<Text>{item().name}</Text>
</View>
)}
</List>
);
}
mount(App);
#列會被回收
children 是一個渲染單一儲存格的函式。它是每個儲存格執行一次,而不是每個項目執行一次:當儲存格捲出可視範圍後,會被重用於另一個項目,而 item() 和 index()(兩者都是 accessor)會隨之改變。請在 JSX 表達式({item().name})內讀取它們,這樣儲存格在重用時才會更新。
#屬性
ListProps<T> 結合了清單本身的屬性、與 <Scroll> 共用的捲動屬性,以及所有元素都接受的屬性,這些屬性會傳給清單的捲動視圖。
#資料與渲染
| 屬性 | 類型 | 預設值 | 說明 |
|---|---|---|---|
each |
readonly T[] |
(必填) | 項目。傳入 signal 的值(each={items()})以跟隨變化;在可視列上方插入項目時,那些列會保持原位。 |
children |
(item: () => T, index: () => number) => JSX.Element |
(必填) | 渲染一列(見上文)。 |
estimatedItemSize |
number |
50 |
尚未量度的列沿清單軸向的大小(以 point 為單位)。估算越接近,捲動指示器和 scrollToIndex 就越準確。 |
numColumns |
number |
1 |
每列的項目數(垂直清單):即網格。 |
horizontal |
boolean |
false |
橫向捲動。 |
inverted |
boolean |
false |
從末端開始排版,項目 0 在最後(聊天風格);一開始會捲動到末端,並在內容變化時保持與末端的距離。 |
stickyHeaderIndices |
number[] |
捲過之後會黏附在起點的項目(單欄、非反向)。 | |
header |
ListSlot |
在項目之前渲染。 | |
footer |
ListSlot |
在項目之後渲染。 | |
empty |
ListSlot |
當 each 為空時,顯示此內容以取代項目。 |
|
separator |
ListSlot |
在項目之間渲染(單欄)。 | |
keyExtractor |
(item: T, index: number) => string |
item.key、item.id或索引 |
項目在可視狀態 token 中的鍵。 |
initialScrollIndex |
number |
在第一個影格之前捲動到此項目。 | |
contentStyle |
ListContentStyle |
內容的內距和間距:padding、paddingHorizontal、paddingVertical、paddingTop、paddingRight、paddingBottom、paddingLeft、gap、rowGap、columnGap(數字)。 |
|
contentClass |
string |
以 class 指定的內容內距和間距(p-*、gap-*);contentStyle 的優先級高於它們。 |
|
style |
StyleProp<ViewStyle> |
清單本身的樣式(請為它指定大小:flex: 1 或高度)。 |
|
testID |
string |
UI 測試用來找到清單捲動視圖的 id(nsh test 的 id:、XCUITest、UI Automator)。 |
|
ref |
ListRef | ((node: ListRef) => void) |
請參閱 ref 方法。 |
ListSlot 是一個元件(Header)或一個元素(<Header />)。
#載入、重新整理與可視狀態
| 屬性 | 類型 | 預設值 | 說明 |
|---|---|---|---|
onLoadMore |
() => void |
當末端進入 loadMoreThreshold 個可視範圍長度之內時呼叫:載入更多。 |
|
loadMoreThreshold |
number |
1 |
距離末端多近(以可視範圍長度計)時觸發 onLoadMore。 |
onLoadMoreAtStart |
() => void |
在使用者或捲動指令移動清單後,當起點進入 loadMoreAtStartThreshold 個可視範圍長度之內時呼叫:載入第一個項目之前的內容。 |
|
loadMoreAtStartThreshold |
number |
1 |
距離起點多近(以可視範圍長度計)時觸發 onLoadMoreAtStart。 |
refreshing |
boolean |
顯示下拉重新整理的轉圈圖示。 | |
onRefresh |
() => void |
啟用下拉重新整理;載入期間請設定 refreshing。 |
|
refreshIndicator |
{ height: number } |
應用程式自行繪製下拉重新整理指示器,高度為 height,以取代轉圈圖示。 |
|
pullValue |
AnimatedValue |
接收清單被下拉超過頂部的距離,供該指示器使用。 | |
onViewableItemsChange |
(info: { viewableItems: ViewToken<T>[]; changed: ViewToken<T>[] }) => void |
變為可視或不再可視的項目。 | |
viewabilityConfig |
ViewabilityConfig |
項目何時算作可視。 |
interface ViewToken<T> {
item: T;
key: string;
index: number;
isViewable: boolean;
}
interface ViewabilityConfig {
/** How much of the viewport an item must cover to count as viewable, from 0 to 1. */
viewAreaCoverageThreshold?: number;
/** How much of an item must be visible, from 0 to 1. Without either threshold, any visible pixel counts. */
itemVisibleThreshold?: number;
/** Milliseconds items must stay viewable before they are reported. */
minimumViewTime?: number;
}
#捲動
| 屬性 | 類型 | 預設值 | 說明 |
|---|---|---|---|
scrollbar |
boolean | ScrollbarTheme |
true |
顯示捲動指示器:使用平台的指示器,或應用程式的內建捲動條;物件則是視圖自身的主題。 |
scrollable |
boolean |
true |
使用者可以捲動。 |
bounces |
boolean |
true |
超出邊緣的過度捲動。 |
pagingEnabled |
boolean |
false |
停在可視範圍的倍數位置。 |
snapToInterval |
number |
0 |
停在此長度的倍數位置(0:關閉)。 |
snapToOffsets |
number[] |
停在這些偏移位置。 | |
snapToAlignment |
"start" | "center" | "end" |
"start" |
配合 snapToInterval 時,項目的哪個點與可視範圍對齊。 |
disableIntervalMomentum |
boolean |
false |
一次快速滑動最多只移動一個吸附點。 |
decelerationRate |
"normal" | "fast" | number |
平台預設 | 快速滑動減速的快慢(0.998 / 0.99,或每毫秒保留的速度)。 |
contentInset |
Partial<Edges> |
內容四周的空間。 | |
contentOffset |
{ x?, y? } |
起始位置。 | |
keyboardShouldPersistTaps |
"never" | "always" | "handled" | boolean |
"never" |
點按是否會收起鍵盤("never":點按只會收起鍵盤)。 |
keyboardDismissMode |
"none" | "on-drag" | "interactive" |
"none" |
拖曳是否會收起鍵盤。 |
scrollEventThrottle |
number |
onScroll 事件之間的最短間隔(毫秒)(無論如何每個影格最多一次)。 |
|
scrollValue |
AnimatedValue |
一個動畫值,在 UI 執行緒上接收捲動偏移(主軸),用於與捲動連動的效果。 |
#捲動事件
| 事件 | 載荷 | 時機 |
|---|---|---|
onScroll |
ScrollEvent:{ offset, contentSize, size, zoomScale } |
捲動期間。 |
onScrollDragStart |
{ offset } |
開始拖曳。 |
onScrollDragEnd |
{ offset } |
手指離開。 |
onMomentumStart |
{ offset } |
拖曳之後開始滑行。 |
onMomentumEnd |
{ offset } |
滑行結束(新的拖曳接住它時也會觸發)。 |
onScrollEnd |
{ offset } |
捲動停止。 |
清單也接受所有元素都接受的屬性(通用屬性)。
#Ref 方法
清單的 ref 會接收一個 ListRef,除了 ViewRef 的方法外,還有以下方法:
| 方法 | 說明 |
|---|---|
scrollToIndex({ index, animated?, viewPosition?, viewOffset? }) |
將項目 index 捲動到可視範圍內。viewPosition 決定放置位置:0 在可視範圍起點(預設)、0.5 在中間、1 在末端;viewOffset 可進一步偏移。以已量度(或估算)的列大小在原生層解析。 |
scrollToOffset({ offset, animated? }) |
沿清單軸向捲動到某個內容偏移位置。 |
scrollToEnd({ animated? }) |
捲動到內容末端(反向清單則是起點)。 |
scrollTo({ x?, y?, animated? }) |
捲動到某個偏移位置。 |
import { List, type ListRef, Text, View } from "nsh";
let list!: ListRef;
<View style={{ flex: 1 }}>
<Text onPress={() => list.scrollToIndex({ index: 60, viewPosition: 0.5 })}>Jump to 60</Text>
<List ref={list} each={items} numColumns={3} estimatedItemSize={90} style={{ flex: 1 }}
contentStyle={{ padding: 12, gap: 8 }}>
{(item) => <View style={{ height: 90, backgroundColor: "#e0e7ff" }}><Text>{item().title}</Text></View>}
</List>
</View>
#範例
聊天:最新訊息在底部,向上捲動時載入較舊的訊息。
<List
each={messages()}
inverted
estimatedItemSize={44}
style={{ flex: 1 }}
contentStyle={{ padding: 12, gap: 6 }}
keyboardDismissMode="interactive"
onLoadMore={loadOlder}
loadMoreThreshold={0.5}
>
{(m) => (
<View style={{ alignSelf: m().mine ? "flex-end" : "flex-start", maxWidth: "75%", padding: 10, borderRadius: 14, backgroundColor: m().mine ? "#6366f1" : "#e2e8f0" }}>
<Text style={{ color: m().mine ? "#fff" : "#0f172a" }}>{m().text}</Text>
</View>
)}
</List>
下拉重新整理:
const refreshing = signal(false);
const refresh = async () => {
refreshing.set(true);
items.set(await load());
refreshing.set(false);
};
<List each={items()} refreshing={refreshing()} onRefresh={refresh} style={{ flex: 1 }}>
{(item) => <Text style={{ padding: 16 }}>{item().title}</Text>}
</List>
#ListItemsProps
type ListItemsProps<T> = Pick<ListProps<T>,
"each" | "children" | "keyExtractor" | "stickyHeaderIndices" | "initialScrollIndex"
| "onViewableItemsChange" | "viewabilityConfig" | "refreshing" | "onRefresh"
| "onLoadMore" | "loadMoreThreshold"
| "header" | "footer" | "empty">;
ListProps 中負責資料與列的一半:當你有自己的元件,並由它自行排版清單時(例如一個以自訂內距和分隔線包裝 List 的設定清單),就是傳入這些屬性。