@legendapp/list 2.0.0-next.1 → 2.0.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/.cursor/rules/changelog.mdc +60 -0
  3. package/.github/FUNDING.yml +15 -0
  4. package/.gitignore +5 -0
  5. package/.prettierrc.json +5 -0
  6. package/.vscode/settings.json +14 -0
  7. package/CLAUDE.md +126 -0
  8. package/biome.json +46 -0
  9. package/bun.lock +1289 -0
  10. package/bunfig.toml +2 -0
  11. package/dist/CHANGELOG.md +119 -0
  12. package/dist/LICENSE +21 -0
  13. package/dist/README.md +139 -0
  14. package/{animated.d.mts → dist/animated.d.mts} +1 -1
  15. package/{animated.d.ts → dist/animated.d.ts} +1 -1
  16. package/{index.d.mts → dist/index.d.mts} +16 -10
  17. package/{index.d.ts → dist/index.d.ts} +16 -10
  18. package/{index.js → dist/index.js} +52 -32
  19. package/{index.mjs → dist/index.mjs} +52 -32
  20. package/{keyboard-controller.d.mts → dist/keyboard-controller.d.mts} +4 -4
  21. package/{keyboard-controller.d.ts → dist/keyboard-controller.d.ts} +4 -4
  22. package/dist/package.json +35 -0
  23. package/example/README.md +40 -0
  24. package/example/api/data/genres.json +23 -0
  25. package/example/api/data/playlist/10402-10749.json +1 -0
  26. package/example/api/data/playlist/10402-10770.json +1 -0
  27. package/example/api/data/playlist/10402-37.json +1 -0
  28. package/example/api/data/playlist/10749-10752.json +1 -0
  29. package/example/api/data/playlist/10749-10770.json +1 -0
  30. package/example/api/data/playlist/10749-37.json +1 -0
  31. package/example/api/data/playlist/10749-878.json +1 -0
  32. package/example/api/data/playlist/10751-10402.json +1 -0
  33. package/example/api/data/playlist/10751-10752.json +1 -0
  34. package/example/api/data/playlist/10751-37.json +1 -0
  35. package/example/api/data/playlist/10751-53.json +1 -0
  36. package/example/api/data/playlist/10751-878.json +1 -0
  37. package/example/api/data/playlist/10751-9648.json +1 -0
  38. package/example/api/data/playlist/10752-37.json +1 -0
  39. package/example/api/data/playlist/12-10402.json +1 -0
  40. package/example/api/data/playlist/12-10749.json +1 -0
  41. package/example/api/data/playlist/12-18.json +1 -0
  42. package/example/api/data/playlist/12-27.json +1 -0
  43. package/example/api/data/playlist/12-35.json +1 -0
  44. package/example/api/data/playlist/14-36.json +1 -0
  45. package/example/api/data/playlist/14-878.json +1 -0
  46. package/example/api/data/playlist/16-10751.json +1 -0
  47. package/example/api/data/playlist/16-10770.json +1 -0
  48. package/example/api/data/playlist/16-35.json +1 -0
  49. package/example/api/data/playlist/16-36.json +1 -0
  50. package/example/api/data/playlist/16-53.json +1 -0
  51. package/example/api/data/playlist/18-10751.json +1 -0
  52. package/example/api/data/playlist/18-10752.json +1 -0
  53. package/example/api/data/playlist/18-37.json +1 -0
  54. package/example/api/data/playlist/18-53.json +1 -0
  55. package/example/api/data/playlist/18-878.json +1 -0
  56. package/example/api/data/playlist/27-10749.json +1 -0
  57. package/example/api/data/playlist/27-10770.json +1 -0
  58. package/example/api/data/playlist/28-10749.json +1 -0
  59. package/example/api/data/playlist/28-10751.json +1 -0
  60. package/example/api/data/playlist/28-10770.json +1 -0
  61. package/example/api/data/playlist/28-16.json +1 -0
  62. package/example/api/data/playlist/28-18.json +1 -0
  63. package/example/api/data/playlist/28-36.json +1 -0
  64. package/example/api/data/playlist/28-37.json +1 -0
  65. package/example/api/data/playlist/28-53.json +1 -0
  66. package/example/api/data/playlist/28-80.json +1 -0
  67. package/example/api/data/playlist/28-99.json +1 -0
  68. package/example/api/data/playlist/35-10749.json +1 -0
  69. package/example/api/data/playlist/35-10751.json +1 -0
  70. package/example/api/data/playlist/35-10752.json +1 -0
  71. package/example/api/data/playlist/35-27.json +1 -0
  72. package/example/api/data/playlist/35-36.json +1 -0
  73. package/example/api/data/playlist/35-53.json +1 -0
  74. package/example/api/data/playlist/35-80.json +1 -0
  75. package/example/api/data/playlist/36-37.json +1 -0
  76. package/example/api/data/playlist/36-878.json +1 -0
  77. package/example/api/data/playlist/36-9648.json +1 -0
  78. package/example/api/data/playlist/53-10752.json +1 -0
  79. package/example/api/data/playlist/80-10770.json +1 -0
  80. package/example/api/data/playlist/80-14.json +1 -0
  81. package/example/api/data/playlist/80-18.json +1 -0
  82. package/example/api/data/playlist/80-37.json +1 -0
  83. package/example/api/data/playlist/878-37.json +1 -0
  84. package/example/api/data/playlist/9648-10770.json +1 -0
  85. package/example/api/data/playlist/9648-37.json +1 -0
  86. package/example/api/data/playlist/9648-53.json +1 -0
  87. package/example/api/data/playlist/9648-878.json +1 -0
  88. package/example/api/data/playlist/99-10749.json +1 -0
  89. package/example/api/data/playlist/99-14.json +1 -0
  90. package/example/api/data/playlist/99-18.json +1 -0
  91. package/example/api/data/playlist/99-27.json +1 -0
  92. package/example/api/data/playlist/99-53.json +1 -0
  93. package/example/api/data/playlist/99-9648.json +1 -0
  94. package/example/api/data/playlist/index.ts +73 -0
  95. package/example/api/data/rows.json +1 -0
  96. package/example/api/index.ts +36 -0
  97. package/example/app/(tabs)/_layout.tsx +60 -0
  98. package/example/app/(tabs)/cards.tsx +81 -0
  99. package/example/app/(tabs)/index.tsx +205 -0
  100. package/example/app/(tabs)/moviesL.tsx +7 -0
  101. package/example/app/(tabs)/moviesLR.tsx +7 -0
  102. package/example/app/+not-found.tsx +32 -0
  103. package/example/app/_layout.tsx +34 -0
  104. package/example/app/accurate-scrollto/index.tsx +125 -0
  105. package/example/app/accurate-scrollto-2/index.tsx +52 -0
  106. package/example/app/accurate-scrollto-huge/index.tsx +128 -0
  107. package/example/app/add-to-end/index.tsx +82 -0
  108. package/example/app/ai-chat/index.tsx +236 -0
  109. package/example/app/bidirectional-infinite-list/index.tsx +133 -0
  110. package/example/app/cards-columns/index.tsx +37 -0
  111. package/example/app/cards-flashlist/index.tsx +122 -0
  112. package/example/app/cards-flatlist/index.tsx +94 -0
  113. package/example/app/cards-no-recycle/index.tsx +110 -0
  114. package/example/app/cards-renderItem.tsx +354 -0
  115. package/example/app/chat-example/index.tsx +167 -0
  116. package/example/app/chat-infinite/index.tsx +239 -0
  117. package/example/app/chat-keyboard/index.tsx +248 -0
  118. package/example/app/chat-resize-outer/index.tsx +247 -0
  119. package/example/app/columns/index.tsx +78 -0
  120. package/example/app/countries/index.tsx +182 -0
  121. package/example/app/countries-flashlist/index.tsx +163 -0
  122. package/example/app/countries-reorder/index.tsx +187 -0
  123. package/example/app/extra-data/index.tsx +86 -0
  124. package/example/app/filter-elements/filter-data-provider.tsx +55 -0
  125. package/example/app/filter-elements/index.tsx +118 -0
  126. package/example/app/initial-scroll-index/index.tsx +106 -0
  127. package/example/app/initial-scroll-index/renderFixedItem.tsx +215 -0
  128. package/example/app/initial-scroll-index-free-height/index.tsx +70 -0
  129. package/example/app/initial-scroll-index-keyed/index.tsx +62 -0
  130. package/example/app/lazy-list/index.tsx +123 -0
  131. package/example/app/movies-flashlist/index.tsx +7 -0
  132. package/example/app/mutable-cells/index.tsx +104 -0
  133. package/example/app/video-feed/index.tsx +119 -0
  134. package/example/app.config.js +22 -0
  135. package/example/app.json +45 -0
  136. package/example/assets/fonts/SpaceMono-Regular.ttf +0 -0
  137. package/example/assets/images/adaptive-icon.png +0 -0
  138. package/example/assets/images/favicon.png +0 -0
  139. package/example/assets/images/icon.png +0 -0
  140. package/example/assets/images/partial-react-logo.png +0 -0
  141. package/example/assets/images/react-logo.png +0 -0
  142. package/example/assets/images/react-logo@2x.png +0 -0
  143. package/example/assets/images/react-logo@3x.png +0 -0
  144. package/example/assets/images/splash-icon.png +0 -0
  145. package/example/autoscroll.sh +101 -0
  146. package/example/bun.lock +2266 -0
  147. package/example/bunfig.toml +2 -0
  148. package/example/components/Breathe.tsx +54 -0
  149. package/example/components/Circle.tsx +69 -0
  150. package/example/components/Collapsible.tsx +44 -0
  151. package/example/components/ExternalLink.tsx +24 -0
  152. package/example/components/HapticTab.tsx +18 -0
  153. package/example/components/HelloWave.tsx +37 -0
  154. package/example/components/Movies.tsx +179 -0
  155. package/example/components/ParallaxScrollView.tsx +81 -0
  156. package/example/components/ThemedText.tsx +60 -0
  157. package/example/components/ThemedView.tsx +14 -0
  158. package/example/components/__tests__/ThemedText-test.tsx +10 -0
  159. package/example/components/__tests__/__snapshots__/ThemedText-test.tsx.snap +24 -0
  160. package/example/components/ui/IconSymbol.ios.tsx +32 -0
  161. package/example/components/ui/IconSymbol.tsx +43 -0
  162. package/example/components/ui/TabBarBackground.ios.tsx +22 -0
  163. package/example/components/ui/TabBarBackground.tsx +6 -0
  164. package/example/constants/Colors.ts +26 -0
  165. package/example/constants/constants.ts +5 -0
  166. package/example/constants/useScrollTest.ts +19 -0
  167. package/example/hooks/useColorScheme.ts +1 -0
  168. package/example/hooks/useColorScheme.web.ts +8 -0
  169. package/example/hooks/useThemeColor.ts +22 -0
  170. package/example/ios/.xcode.env +11 -0
  171. package/example/ios/Podfile +64 -0
  172. package/example/ios/Podfile.lock +2767 -0
  173. package/example/ios/Podfile.properties.json +5 -0
  174. package/example/ios/listtest/AppDelegate.swift +70 -0
  175. package/example/ios/listtest/Images.xcassets/AppIcon.appiconset/App-Icon-1024x1024@1x.png +0 -0
  176. package/example/ios/listtest/Images.xcassets/AppIcon.appiconset/Contents.json +14 -0
  177. package/example/ios/listtest/Images.xcassets/Contents.json +6 -0
  178. package/example/ios/listtest/Images.xcassets/SplashScreenBackground.colorset/Contents.json +20 -0
  179. package/example/ios/listtest/Images.xcassets/SplashScreenLogo.imageset/Contents.json +23 -0
  180. package/example/ios/listtest/Images.xcassets/SplashScreenLogo.imageset/image.png +0 -0
  181. package/example/ios/listtest/Images.xcassets/SplashScreenLogo.imageset/image@2x.png +0 -0
  182. package/example/ios/listtest/Images.xcassets/SplashScreenLogo.imageset/image@3x.png +0 -0
  183. package/example/ios/listtest/Info.plist +85 -0
  184. package/example/ios/listtest/PrivacyInfo.xcprivacy +48 -0
  185. package/example/ios/listtest/SplashScreen.storyboard +42 -0
  186. package/example/ios/listtest/Supporting/Expo.plist +12 -0
  187. package/example/ios/listtest/listtest-Bridging-Header.h +3 -0
  188. package/example/ios/listtest/listtest.entitlements +5 -0
  189. package/example/ios/listtest.xcodeproj/project.pbxproj +547 -0
  190. package/example/ios/listtest.xcodeproj/xcshareddata/xcschemes/listtest.xcscheme +88 -0
  191. package/example/ios/listtest.xcworkspace/contents.xcworkspacedata +10 -0
  192. package/example/metro.config.js +16 -0
  193. package/example/package.json +73 -0
  194. package/example/scripts/reset-project.js +84 -0
  195. package/example/tsconfig.json +26 -0
  196. package/package.json +88 -34
  197. package/posttsup.ts +24 -0
  198. package/src/Container.tsx +176 -0
  199. package/src/Containers.tsx +85 -0
  200. package/src/ContextContainer.ts +145 -0
  201. package/src/DebugView.tsx +83 -0
  202. package/src/LazyLegendList.tsx +41 -0
  203. package/src/LeanView.tsx +18 -0
  204. package/src/LegendList.tsx +558 -0
  205. package/src/ListComponent.tsx +191 -0
  206. package/src/ScrollAdjust.tsx +24 -0
  207. package/src/ScrollAdjustHandler.ts +26 -0
  208. package/src/Separator.tsx +14 -0
  209. package/src/animated.tsx +6 -0
  210. package/src/calculateItemsInView.ts +363 -0
  211. package/src/calculateOffsetForIndex.ts +23 -0
  212. package/src/calculateOffsetWithOffsetPosition.ts +26 -0
  213. package/src/checkAllSizesKnown.ts +17 -0
  214. package/src/checkAtBottom.ts +36 -0
  215. package/src/checkAtTop.ts +27 -0
  216. package/src/checkThreshold.ts +30 -0
  217. package/src/constants.ts +11 -0
  218. package/src/createColumnWrapperStyle.ts +16 -0
  219. package/src/doInitialAllocateContainers.ts +40 -0
  220. package/src/doMaintainScrollAtEnd.ts +34 -0
  221. package/src/findAvailableContainers.ts +98 -0
  222. package/src/finishScrollTo.ts +8 -0
  223. package/src/getId.ts +21 -0
  224. package/src/getItemSize.ts +52 -0
  225. package/src/getRenderedItem.ts +34 -0
  226. package/src/getScrollVelocity.ts +47 -0
  227. package/src/handleLayout.ts +70 -0
  228. package/src/helpers.ts +39 -0
  229. package/src/index.ts +11 -0
  230. package/src/keyboard-controller.tsx +63 -0
  231. package/src/onScroll.ts +66 -0
  232. package/src/prepareMVCP.ts +50 -0
  233. package/src/reanimated.tsx +63 -0
  234. package/src/requestAdjust.ts +41 -0
  235. package/src/scrollTo.ts +40 -0
  236. package/src/scrollToIndex.ts +34 -0
  237. package/src/setDidLayout.ts +25 -0
  238. package/src/setPaddingTop.ts +28 -0
  239. package/src/state.tsx +304 -0
  240. package/src/types.ts +610 -0
  241. package/src/updateAlignItemsPaddingTop.ts +18 -0
  242. package/src/updateAllPositions.ts +130 -0
  243. package/src/updateItemSize.ts +203 -0
  244. package/src/updateTotalSize.ts +44 -0
  245. package/src/useAnimatedValue.ts +6 -0
  246. package/src/useCombinedRef.ts +22 -0
  247. package/src/useInit.ts +17 -0
  248. package/src/useSyncLayout.tsx +68 -0
  249. package/src/useValue$.ts +53 -0
  250. package/src/viewability.ts +279 -0
  251. package/tsconfig.json +59 -0
  252. package/tsup.config.ts +21 -0
  253. /package/{animated.js → dist/animated.js} +0 -0
  254. /package/{animated.mjs → dist/animated.mjs} +0 -0
  255. /package/{keyboard-controller.js → dist/keyboard-controller.js} +0 -0
  256. /package/{keyboard-controller.mjs → dist/keyboard-controller.mjs} +0 -0
  257. /package/{reanimated.d.mts → dist/reanimated.d.mts} +0 -0
  258. /package/{reanimated.d.ts → dist/reanimated.d.ts} +0 -0
  259. /package/{reanimated.js → dist/reanimated.js} +0 -0
  260. /package/{reanimated.mjs → dist/reanimated.mjs} +0 -0
@@ -0,0 +1,191 @@
1
+ import { useMemo } from "react";
2
+ import * as React from "react";
3
+ import {
4
+ Animated,
5
+ type LayoutChangeEvent,
6
+ type LayoutRectangle,
7
+ type NativeScrollEvent,
8
+ type NativeSyntheticEvent,
9
+ ScrollView,
10
+ type ScrollViewProps,
11
+ View,
12
+ type ViewStyle,
13
+ } from "react-native";
14
+ import { Containers } from "./Containers";
15
+ import { ScrollAdjust } from "./ScrollAdjust";
16
+ import type { ScrollAdjustHandler } from "./ScrollAdjustHandler";
17
+ import { ENABLE_DEVMODE } from "./constants";
18
+ import { set$, useStateContext } from "./state";
19
+ import { type GetRenderedItem, type LegendListProps, typedMemo } from "./types";
20
+ import { useSyncLayout } from "./useSyncLayout";
21
+ import { useValue$ } from "./useValue$";
22
+
23
+ interface ListComponentProps<ItemT>
24
+ extends Omit<
25
+ LegendListProps<ItemT> & { scrollEventThrottle: number | undefined },
26
+ | "data"
27
+ | "estimatedItemSize"
28
+ | "drawDistance"
29
+ | "maintainScrollAtEnd"
30
+ | "maintainScrollAtEndThreshold"
31
+ | "maintainVisibleContentPosition"
32
+ | "style"
33
+ > {
34
+ horizontal: boolean;
35
+ initialContentOffset: number | undefined;
36
+ refScrollView: React.Ref<ScrollView>;
37
+ getRenderedItem: GetRenderedItem;
38
+ updateItemSize: (itemKey: string, size: { width: number; height: number }) => void;
39
+ onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
40
+ onLayout: (event: LayoutChangeEvent) => void;
41
+ onLayoutHeader: (rect: LayoutRectangle, fromLayoutEffect: boolean) => void;
42
+ maintainVisibleContentPosition: boolean;
43
+ renderScrollComponent?: (props: ScrollViewProps) => React.ReactElement<ScrollViewProps>;
44
+ style: ViewStyle;
45
+ canRender: boolean;
46
+ scrollAdjustHandler: ScrollAdjustHandler;
47
+ }
48
+
49
+ const getComponent = (Component: React.ComponentType<any> | React.ReactElement) => {
50
+ if (React.isValidElement<any>(Component)) {
51
+ return Component;
52
+ }
53
+ if (Component) {
54
+ return <Component />;
55
+ }
56
+ return null;
57
+ };
58
+
59
+ const Padding = () => {
60
+ const animPaddingTop = useValue$("alignItemsPaddingTop", { delay: 0 });
61
+
62
+ return <Animated.View style={{ paddingTop: animPaddingTop }} />;
63
+ };
64
+
65
+ const PaddingDevMode = () => {
66
+ const animPaddingTop = useValue$("alignItemsPaddingTop", { delay: 0 });
67
+
68
+ return (
69
+ <>
70
+ <Animated.View style={{ paddingTop: animPaddingTop }} />
71
+ <Animated.View
72
+ style={{
73
+ position: "absolute",
74
+ top: 0,
75
+ height: animPaddingTop,
76
+ left: 0,
77
+ right: 0,
78
+ backgroundColor: "green",
79
+ }}
80
+ />
81
+ </>
82
+ );
83
+ };
84
+
85
+ export const ListComponent = typedMemo(function ListComponent<ItemT>({
86
+ canRender,
87
+ style,
88
+ contentContainerStyle,
89
+ horizontal,
90
+ initialContentOffset,
91
+ recycleItems,
92
+ ItemSeparatorComponent,
93
+ alignItemsAtEnd,
94
+ waitForInitialLayout,
95
+ onScroll,
96
+ onLayout,
97
+ ListHeaderComponent,
98
+ ListHeaderComponentStyle,
99
+ ListFooterComponent,
100
+ ListFooterComponentStyle,
101
+ ListEmptyComponent,
102
+ getRenderedItem,
103
+ updateItemSize,
104
+ refScrollView,
105
+ maintainVisibleContentPosition,
106
+ renderScrollComponent,
107
+ scrollAdjustHandler,
108
+ onLayoutHeader,
109
+ ...rest
110
+ }: ListComponentProps<ItemT>) {
111
+ const ctx = useStateContext();
112
+ const { onLayout: onLayoutHeaderSync, ref: refHeader } = useSyncLayout({
113
+ onChange: onLayoutHeader,
114
+ });
115
+
116
+ // Use renderScrollComponent if provided, otherwise a regular ScrollView
117
+ const ScrollComponent = renderScrollComponent
118
+ ? useMemo(
119
+ () => React.forwardRef((props, ref) => renderScrollComponent({ ...props, ref } as any)),
120
+ [renderScrollComponent],
121
+ )
122
+ : ScrollView;
123
+
124
+ React.useEffect(() => {
125
+ if (canRender) {
126
+ setTimeout(() => {
127
+ scrollAdjustHandler.setMounted();
128
+ }, 0);
129
+ }
130
+ }, [canRender]);
131
+
132
+ return (
133
+ <ScrollComponent
134
+ {...rest}
135
+ style={style}
136
+ maintainVisibleContentPosition={
137
+ maintainVisibleContentPosition && !ListEmptyComponent ? { minIndexForVisible: 0 } : undefined
138
+ }
139
+ contentContainerStyle={[
140
+ contentContainerStyle,
141
+ horizontal
142
+ ? {
143
+ height: "100%",
144
+ }
145
+ : {},
146
+ ]}
147
+ onScroll={onScroll}
148
+ onLayout={onLayout}
149
+ horizontal={horizontal}
150
+ contentOffset={
151
+ initialContentOffset
152
+ ? horizontal
153
+ ? { x: initialContentOffset, y: 0 }
154
+ : { x: 0, y: initialContentOffset }
155
+ : undefined
156
+ }
157
+ ref={refScrollView as any}
158
+ >
159
+ {maintainVisibleContentPosition && <ScrollAdjust />}
160
+ {ENABLE_DEVMODE ? <PaddingDevMode /> : <Padding />}
161
+ {ListHeaderComponent && (
162
+ <View style={ListHeaderComponentStyle} onLayout={onLayoutHeaderSync} ref={refHeader}>
163
+ {getComponent(ListHeaderComponent)}
164
+ </View>
165
+ )}
166
+ {ListEmptyComponent && getComponent(ListEmptyComponent)}
167
+
168
+ {canRender && (
169
+ <Containers
170
+ horizontal={horizontal!}
171
+ recycleItems={recycleItems!}
172
+ waitForInitialLayout={waitForInitialLayout}
173
+ getRenderedItem={getRenderedItem}
174
+ ItemSeparatorComponent={ItemSeparatorComponent}
175
+ updateItemSize={updateItemSize}
176
+ />
177
+ )}
178
+ {ListFooterComponent && (
179
+ <View
180
+ style={ListFooterComponentStyle}
181
+ onLayout={(event) => {
182
+ const size = event.nativeEvent.layout[horizontal ? "width" : "height"];
183
+ set$(ctx, "footerSize", size);
184
+ }}
185
+ >
186
+ {getComponent(ListFooterComponent)}
187
+ </View>
188
+ )}
189
+ </ScrollComponent>
190
+ );
191
+ });
@@ -0,0 +1,24 @@
1
+ // biome-ignore lint/correctness/noUnusedImports: Leaving this out makes it crash in some environments
2
+ import * as React from "react";
3
+ import { View } from "react-native";
4
+ import { useArr$ } from "./state";
5
+
6
+ export function ScrollAdjust() {
7
+ // Use a large bias to ensure this value never goes negative
8
+ const bias = 10_000_000;
9
+ const [scrollAdjust, scrollAdjustUserOffset] = useArr$(["scrollAdjust", "scrollAdjustUserOffset"]);
10
+ const scrollOffset = (scrollAdjust || 0) + (scrollAdjustUserOffset || 0) + bias;
11
+ const horizontal = false;
12
+
13
+ return (
14
+ <View
15
+ style={{
16
+ position: "absolute",
17
+ height: 0,
18
+ width: 0,
19
+ top: horizontal ? 0 : scrollOffset,
20
+ left: horizontal ? scrollOffset : 0,
21
+ }}
22
+ />
23
+ );
24
+ }
@@ -0,0 +1,26 @@
1
+ import { type StateContext, peek$, set$ } from "./state";
2
+
3
+ export class ScrollAdjustHandler {
4
+ private appliedAdjust = 0;
5
+ private context: StateContext;
6
+ private mounted = false;
7
+
8
+ constructor(ctx: StateContext) {
9
+ this.context = ctx;
10
+ }
11
+ requestAdjust(add: number) {
12
+ const oldAdjustTop = peek$(this.context, "scrollAdjust") || 0;
13
+
14
+ this.appliedAdjust = add + oldAdjustTop;
15
+
16
+ const set = () => set$(this.context, "scrollAdjust", this.appliedAdjust);
17
+ if (this.mounted) {
18
+ set();
19
+ } else {
20
+ requestAnimationFrame(set);
21
+ }
22
+ }
23
+ setMounted() {
24
+ this.mounted = true;
25
+ }
26
+ }
@@ -0,0 +1,14 @@
1
+ import { useArr$ } from "./state";
2
+
3
+ export interface SeparatorProps<ItemT> {
4
+ ItemSeparatorComponent: React.ComponentType<{ leadingItem: ItemT }>;
5
+ itemKey: string;
6
+ leadingItem: ItemT;
7
+ }
8
+
9
+ export function Separator<ItemT>({ ItemSeparatorComponent, itemKey, leadingItem }: SeparatorProps<ItemT>) {
10
+ const [lastItemKeys] = useArr$(["lastItemKeys"]);
11
+ const isALastItem = lastItemKeys.includes(itemKey);
12
+
13
+ return isALastItem ? null : <ItemSeparatorComponent leadingItem={leadingItem} />;
14
+ }
@@ -0,0 +1,6 @@
1
+ import { LegendList } from "@legendapp/list";
2
+ import { Animated } from "react-native";
3
+
4
+ const AnimatedLegendList = Animated.createAnimatedComponent(LegendList);
5
+
6
+ export { AnimatedLegendList };
@@ -0,0 +1,363 @@
1
+ import { calculateOffsetForIndex } from "./calculateOffsetForIndex";
2
+ import { calculateOffsetWithOffsetPosition } from "./calculateOffsetWithOffsetPosition";
3
+ import { checkAllSizesKnown } from "./checkAllSizesKnown";
4
+ import { ENABLE_DEBUG_VIEW, POSITION_OUT_OF_VIEW } from "./constants";
5
+ import { findAvailableContainers } from "./findAvailableContainers";
6
+ import { getId } from "./getId";
7
+ import { getItemSize } from "./getItemSize";
8
+ import { getScrollVelocity } from "./getScrollVelocity";
9
+ import { prepareMVCP } from "./prepareMVCP";
10
+ import { setDidLayout } from "./setDidLayout";
11
+ import { type StateContext, peek$, set$ } from "./state";
12
+ import type { InternalState } from "./types";
13
+ import { updateAllPositions } from "./updateAllPositions";
14
+ import { updateViewableItems } from "./viewability";
15
+
16
+ export function calculateItemsInView(
17
+ ctx: StateContext,
18
+ state: InternalState,
19
+ params: { doMVCP?: boolean; dataChanged?: boolean } = {},
20
+ ) {
21
+ const {
22
+ scrollLength,
23
+ startBufferedId: startBufferedIdOrig,
24
+ positions,
25
+ columns,
26
+ containerItemKeys,
27
+ idCache,
28
+ sizes,
29
+ indexByKey,
30
+ scrollForNextCalculateItemsInView,
31
+ enableScrollForNextCalculateItemsInView,
32
+ minIndexSizeChanged,
33
+ } = state;
34
+ const data = state.props.data;
35
+ if (!data || scrollLength === 0) {
36
+ return;
37
+ }
38
+
39
+ const totalSize = peek$(ctx, "totalSize");
40
+ const topPad = peek$(ctx, "stylePaddingTop") + peek$(ctx, "headerSize");
41
+ const numColumns = peek$(ctx, "numColumns");
42
+ const previousScrollAdjust = 0;
43
+ const { dataChanged, doMVCP } = params;
44
+ const speed = getScrollVelocity(state);
45
+
46
+ if (doMVCP || dataChanged) {
47
+ // TODO: This should only run if a size changed or items changed
48
+ // Handle maintainVisibleContentPosition adjustment early
49
+ const checkMVCP = doMVCP ? prepareMVCP(ctx, state) : undefined;
50
+
51
+ // Update all positions upfront so we can assume they're correct
52
+ updateAllPositions(ctx, state, dataChanged);
53
+
54
+ checkMVCP?.();
55
+ }
56
+
57
+ const scrollExtra = 0;
58
+ // Disabled this optimization for now because it was causing blanks to appear sometimes
59
+ // We may need to control speed calculation better, or not have a 5 item history to avoid this issue
60
+ // const scrollExtra = Math.max(-16, Math.min(16, speed)) * 24;
61
+
62
+ const { queuedInitialLayout } = state;
63
+ let { scroll: scrollState } = state;
64
+
65
+ // If this is before the initial layout, and we have an initialScrollIndex,
66
+ // then ignore the actual scroll which might be shifting due to scrollAdjustHandler
67
+ // and use the calculated offset of the initialScrollIndex instead.
68
+ const initialScroll = state.props.initialScroll;
69
+ if (!queuedInitialLayout && initialScroll) {
70
+ const updatedOffset = calculateOffsetWithOffsetPosition(
71
+ state,
72
+ calculateOffsetForIndex(ctx, state, initialScroll.index),
73
+ initialScroll,
74
+ );
75
+ scrollState = updatedOffset;
76
+ }
77
+
78
+ const scrollAdjustPad = -previousScrollAdjust - topPad;
79
+ let scroll = scrollState + scrollExtra + scrollAdjustPad;
80
+
81
+ // Sometimes we may have scrolled past the visible area which can make items at the top of the
82
+ // screen not render. So make sure we clamp scroll to the end.
83
+ if (scroll + scrollLength > totalSize) {
84
+ scroll = totalSize - scrollLength;
85
+ }
86
+
87
+ if (ENABLE_DEBUG_VIEW) {
88
+ set$(ctx, "debugRawScroll", scrollState);
89
+ set$(ctx, "debugComputedScroll", scroll);
90
+ }
91
+
92
+ const scrollBuffer = state.props.scrollBuffer;
93
+ let scrollBufferTop = scrollBuffer;
94
+ let scrollBufferBottom = scrollBuffer;
95
+
96
+ if (speed > 0) {
97
+ scrollBufferTop = scrollBuffer * 0.5;
98
+ scrollBufferBottom = scrollBuffer * 1.5;
99
+ } else {
100
+ scrollBufferTop = scrollBuffer * 1.5;
101
+ scrollBufferBottom = scrollBuffer * 0.5;
102
+ }
103
+
104
+ const scrollTopBuffered = scroll - scrollBufferTop;
105
+ const scrollBottom = scroll + scrollLength;
106
+ const scrollBottomBuffered = scrollBottom + scrollBufferBottom;
107
+
108
+ // Check precomputed scroll range to see if we can skip this check
109
+ if (scrollForNextCalculateItemsInView) {
110
+ const { top, bottom } = scrollForNextCalculateItemsInView;
111
+ if (scrollTopBuffered > top && scrollBottomBuffered < bottom) {
112
+ return;
113
+ }
114
+ }
115
+
116
+ let startNoBuffer: number | null = null;
117
+ let startBuffered: number | null = null;
118
+ let startBufferedId: string | null = null;
119
+ let endNoBuffer: number | null = null;
120
+ let endBuffered: number | null = null;
121
+
122
+ let loopStart: number = startBufferedIdOrig ? indexByKey.get(startBufferedIdOrig) || 0 : 0;
123
+
124
+ if (minIndexSizeChanged !== undefined) {
125
+ loopStart = Math.min(minIndexSizeChanged, loopStart);
126
+ state.minIndexSizeChanged = undefined;
127
+ }
128
+
129
+ // Go backwards from the last start position to find the first item that is in view
130
+ // This is an optimization to avoid looping through all items, which could slow down
131
+ // when scrolling at the end of a long list.
132
+ for (let i = loopStart; i >= 0; i--) {
133
+ const id = idCache.get(i) ?? getId(state, i)!;
134
+ const top = positions.get(id)!;
135
+ const size = sizes.get(id) ?? getItemSize(state, id, i, data[i]);
136
+ const bottom = top + size;
137
+
138
+ if (bottom > scroll - scrollBuffer) {
139
+ loopStart = i;
140
+ } else {
141
+ break;
142
+ }
143
+ }
144
+
145
+ const loopStartMod = loopStart % numColumns;
146
+ if (loopStartMod > 0) {
147
+ loopStart -= loopStartMod;
148
+ }
149
+
150
+ let foundEnd = false;
151
+ let nextTop: number | undefined;
152
+ let nextBottom: number | undefined;
153
+
154
+ // TODO PERF: Could cache this while looping through numContainers at the end of this function
155
+ // This takes 0.03 ms in an example in the ios simulator
156
+ const prevNumContainers = ctx.values.get("numContainers") as number;
157
+ let maxIndexRendered = 0;
158
+ for (let i = 0; i < prevNumContainers; i++) {
159
+ const key = peek$(ctx, `containerItemKey${i}`);
160
+ if (key !== undefined) {
161
+ const index = indexByKey.get(key)!;
162
+ maxIndexRendered = Math.max(maxIndexRendered, index);
163
+ }
164
+ }
165
+
166
+ let firstFullyOnScreenIndex: number | undefined;
167
+
168
+ // scan data forwards
169
+ // Continue until we've found the end and we've updated positions of all items that were previously in view
170
+ const dataLength = data!.length;
171
+ for (let i = Math.max(0, loopStart); i < dataLength && (!foundEnd || i <= maxIndexRendered); i++) {
172
+ const id = idCache.get(i) ?? getId(state, i)!;
173
+ const size = sizes.get(id) ?? getItemSize(state, id, i, data[i]);
174
+ const top = positions.get(id)!;
175
+
176
+ if (!foundEnd) {
177
+ if (startNoBuffer === null && top + size > scroll) {
178
+ startNoBuffer = i;
179
+ }
180
+ // Subtract 10px for a little buffer so it can be slightly off screen
181
+ if (firstFullyOnScreenIndex === undefined && top >= scroll - 10) {
182
+ firstFullyOnScreenIndex = i;
183
+ }
184
+
185
+ if (startBuffered === null && top + size > scrollTopBuffered) {
186
+ startBuffered = i;
187
+ startBufferedId = id;
188
+ nextTop = top;
189
+ }
190
+ if (startNoBuffer !== null) {
191
+ if (top <= scrollBottom) {
192
+ endNoBuffer = i;
193
+ }
194
+ if (top <= scrollBottomBuffered) {
195
+ endBuffered = i;
196
+ nextBottom = top + size;
197
+ } else {
198
+ foundEnd = true;
199
+ }
200
+ }
201
+ }
202
+ }
203
+
204
+ const idsInView: string[] = [];
205
+ for (let i = firstFullyOnScreenIndex!; i <= endNoBuffer!; i++) {
206
+ const id = idCache.get(i) ?? getId(state, i)!;
207
+ idsInView.push(id);
208
+ }
209
+
210
+ Object.assign(state, {
211
+ startBuffered,
212
+ startBufferedId,
213
+ startNoBuffer,
214
+ endBuffered,
215
+ endNoBuffer,
216
+ idsInView,
217
+ firstFullyOnScreenIndex,
218
+ });
219
+
220
+ // Precompute the scroll that will be needed for the range to change
221
+ // so it can be skipped if not needed
222
+ if (enableScrollForNextCalculateItemsInView && nextTop !== undefined && nextBottom !== undefined) {
223
+ state.scrollForNextCalculateItemsInView =
224
+ nextTop !== undefined && nextBottom !== undefined
225
+ ? {
226
+ top: nextTop,
227
+ bottom: nextBottom,
228
+ }
229
+ : undefined;
230
+ }
231
+
232
+ const numContainers = peek$(ctx, "numContainers");
233
+ // Reset containers that aren't used anymore because the data has changed
234
+ const pendingRemoval: number[] = [];
235
+ if (dataChanged) {
236
+ for (let i = 0; i < numContainers; i++) {
237
+ const itemKey = peek$(ctx, `containerItemKey${i}`);
238
+ if (!state.props.keyExtractor || (itemKey && indexByKey.get(itemKey) === undefined)) {
239
+ pendingRemoval.push(i);
240
+ }
241
+ }
242
+ }
243
+
244
+ // Place newly added items into containers
245
+ if (startBuffered !== null && endBuffered !== null) {
246
+ let numContainers = prevNumContainers;
247
+ const needNewContainers: number[] = [];
248
+
249
+ for (let i = startBuffered!; i <= endBuffered; i++) {
250
+ const id = idCache.get(i) ?? getId(state, i)!;
251
+ if (!containerItemKeys.has(id)) {
252
+ needNewContainers.push(i);
253
+ }
254
+ }
255
+
256
+ if (needNewContainers.length > 0) {
257
+ const availableContainers = findAvailableContainers(
258
+ ctx,
259
+ state,
260
+ needNewContainers.length,
261
+ startBuffered,
262
+ endBuffered,
263
+ pendingRemoval,
264
+ );
265
+ for (let idx = 0; idx < needNewContainers.length; idx++) {
266
+ const i = needNewContainers[idx];
267
+ const containerIndex = availableContainers[idx];
268
+ const id = idCache.get(i) ?? getId(state, i)!;
269
+
270
+ // Remove old key from cache
271
+ const oldKey = peek$(ctx, `containerItemKey${containerIndex}`);
272
+ if (oldKey && oldKey !== id) {
273
+ containerItemKeys!.delete(oldKey);
274
+ }
275
+
276
+ set$(ctx, `containerItemKey${containerIndex}`, id);
277
+ set$(ctx, `containerItemData${containerIndex}`, data[i]);
278
+
279
+ // Update cache when adding new item
280
+ containerItemKeys!.add(id);
281
+
282
+ if (containerIndex >= numContainers) {
283
+ numContainers = containerIndex + 1;
284
+ }
285
+ }
286
+
287
+ if (numContainers !== prevNumContainers) {
288
+ set$(ctx, "numContainers", numContainers);
289
+ if (numContainers > peek$(ctx, "numContainersPooled")) {
290
+ set$(ctx, "numContainersPooled", Math.ceil(numContainers * 1.5));
291
+ }
292
+ }
293
+ }
294
+ }
295
+
296
+ // Update top positions of all containers
297
+ for (let i = 0; i < numContainers; i++) {
298
+ const itemKey = peek$(ctx, `containerItemKey${i}`);
299
+
300
+ // If it was
301
+ if (pendingRemoval.includes(i)) {
302
+ // Update cache when removing item
303
+ if (itemKey) {
304
+ containerItemKeys!.delete(itemKey);
305
+ }
306
+
307
+ set$(ctx, `containerItemKey${i}`, undefined);
308
+ set$(ctx, `containerItemData${i}`, undefined);
309
+ set$(ctx, `containerPosition${i}`, POSITION_OUT_OF_VIEW);
310
+ set$(ctx, `containerColumn${i}`, -1);
311
+ } else {
312
+ const itemIndex = indexByKey.get(itemKey)!;
313
+ const item = data[itemIndex];
314
+ if (item !== undefined) {
315
+ const id = idCache.get(itemIndex) ?? getId(state, itemIndex);
316
+ const position = positions.get(id);
317
+
318
+ if (position === undefined) {
319
+ // This item may have been in view before data changed and positions were reset
320
+ // so we need to set it to out of view
321
+ set$(ctx, `containerPosition${i}`, POSITION_OUT_OF_VIEW);
322
+ } else {
323
+ const pos = positions.get(id)!;
324
+ const column = columns.get(id) || 1;
325
+
326
+ const prevPos = peek$(ctx, `containerPosition${i}`);
327
+ const prevColumn = peek$(ctx, `containerColumn${i}`);
328
+ const prevData = peek$(ctx, `containerItemData${i}`);
329
+
330
+ if (!prevPos || (pos > POSITION_OUT_OF_VIEW && pos !== prevPos)) {
331
+ set$(ctx, `containerPosition${i}`, pos);
332
+ }
333
+ if (column >= 0 && column !== prevColumn) {
334
+ set$(ctx, `containerColumn${i}`, column);
335
+ }
336
+
337
+ if (prevData !== item) {
338
+ set$(ctx, `containerItemData${i}`, data[itemIndex]);
339
+ }
340
+ }
341
+ }
342
+ }
343
+ }
344
+
345
+ if (!queuedInitialLayout && endBuffered !== null) {
346
+ // If waiting for initial layout and all items in view have a known size then
347
+ // initial layout is complete
348
+ if (checkAllSizesKnown(state)) {
349
+ setDidLayout(ctx, state);
350
+ }
351
+ }
352
+
353
+ if (state.props.viewabilityConfigCallbackPairs) {
354
+ updateViewableItems(
355
+ state,
356
+ ctx,
357
+ state.props.viewabilityConfigCallbackPairs,
358
+ scrollLength,
359
+ startNoBuffer!,
360
+ endNoBuffer!,
361
+ );
362
+ }
363
+ }
@@ -0,0 +1,23 @@
1
+ import { getId } from "./getId";
2
+ import { type StateContext, peek$ } from "./state";
3
+ import type { InternalState } from "./types";
4
+
5
+ export function calculateOffsetForIndex(ctx: StateContext, state: InternalState, index: number | undefined) {
6
+ let position = 0;
7
+
8
+ if (index !== undefined) {
9
+ position = state?.positions.get(getId(state, index)) || 0;
10
+ }
11
+
12
+ const paddingTop = peek$(ctx, "stylePaddingTop");
13
+ if (paddingTop) {
14
+ position += paddingTop;
15
+ }
16
+
17
+ const headerSize = peek$(ctx, "headerSize");
18
+ if (headerSize) {
19
+ position += headerSize;
20
+ }
21
+
22
+ return position;
23
+ }
@@ -0,0 +1,26 @@
1
+ import { getId } from "./getId";
2
+ import { getItemSize } from "./getItemSize";
3
+ import type { InternalState, ScrollIndexWithOffsetPosition } from "./types";
4
+
5
+ export function calculateOffsetWithOffsetPosition(
6
+ state: InternalState,
7
+ offsetParam: number,
8
+ params: Partial<ScrollIndexWithOffsetPosition>,
9
+ ) {
10
+ const { index, viewOffset, viewPosition } = params;
11
+ let offset = offsetParam;
12
+
13
+ if (viewOffset) {
14
+ offset -= viewOffset;
15
+ }
16
+
17
+ if (viewPosition !== undefined && index !== undefined) {
18
+ // TODO: This can be inaccurate if the item size is very different from the estimatedItemSize
19
+ // In the future we can improve this by listening for the item size change and then updating the scroll position
20
+ offset -=
21
+ viewPosition *
22
+ (state.scrollLength - getItemSize(state, getId(state, index), index, state.props.data[index]!));
23
+ }
24
+
25
+ return offset;
26
+ }
@@ -0,0 +1,17 @@
1
+ import { getId } from "./getId";
2
+ import type { InternalState } from "./types";
3
+
4
+ export function checkAllSizesKnown(state: InternalState) {
5
+ const { startBuffered, endBuffered, sizesKnown } = state;
6
+ if (endBuffered !== null) {
7
+ // If waiting for initial layout and all items in view have a known size then
8
+ // initial layout is complete
9
+ let areAllKnown = true;
10
+ for (let i = startBuffered!; areAllKnown && i <= endBuffered!; i++) {
11
+ const key = getId(state, i)!;
12
+ areAllKnown &&= sizesKnown.has(key);
13
+ }
14
+ return areAllKnown;
15
+ }
16
+ return false;
17
+ }