@shopify/flash-list 2.0.0-alpha.10 → 2.0.0-alpha.11
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.
- package/README.md +6 -2
- package/dist/AnimatedFlashList.d.ts.map +1 -1
- package/dist/AnimatedFlashList.js +3 -3
- package/dist/AnimatedFlashList.js.map +1 -1
- package/dist/FlashList.d.ts +9 -0
- package/dist/FlashList.d.ts.map +1 -1
- package/dist/FlashList.js +20 -0
- package/dist/FlashList.js.map +1 -1
- package/dist/FlashListProps.d.ts +13 -6
- package/dist/FlashListProps.d.ts.map +1 -1
- package/dist/FlashListProps.js.map +1 -1
- package/dist/FlashListRef.d.ts +295 -0
- package/dist/FlashListRef.d.ts.map +1 -0
- package/dist/FlashListRef.js +3 -0
- package/dist/FlashListRef.js.map +1 -0
- package/dist/__tests__/RecyclerView.test.js +62 -27
- package/dist/__tests__/RecyclerView.test.js.map +1 -1
- package/dist/__tests__/RenderStackManager.test.d.ts +2 -0
- package/dist/__tests__/RenderStackManager.test.d.ts.map +1 -0
- package/dist/__tests__/RenderStackManager.test.js +405 -0
- package/dist/__tests__/RenderStackManager.test.js.map +1 -0
- package/dist/__tests__/useUnmountAwareCallbacks.test.js +1 -1
- package/dist/__tests__/useUnmountAwareCallbacks.test.js.map +1 -1
- package/dist/benchmark/useFlatListBenchmark.js +8 -7
- package/dist/benchmark/useFlatListBenchmark.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/recyclerview/RecyclerView.d.ts +2 -1
- package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
- package/dist/recyclerview/RecyclerView.js +33 -14
- package/dist/recyclerview/RecyclerView.js.map +1 -1
- package/dist/recyclerview/RecyclerViewContextProvider.d.ts +6 -5
- package/dist/recyclerview/RecyclerViewContextProvider.d.ts.map +1 -1
- package/dist/recyclerview/RecyclerViewContextProvider.js.map +1 -1
- package/dist/recyclerview/RecyclerViewManager.d.ts +11 -7
- package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -1
- package/dist/recyclerview/RecyclerViewManager.js +57 -102
- package/dist/recyclerview/RecyclerViewManager.js.map +1 -1
- package/dist/recyclerview/RenderStackManager.d.ts +85 -0
- package/dist/recyclerview/RenderStackManager.d.ts.map +1 -0
- package/dist/recyclerview/RenderStackManager.js +261 -0
- package/dist/recyclerview/RenderStackManager.js.map +1 -0
- package/dist/recyclerview/ViewHolder.d.ts.map +1 -1
- package/dist/recyclerview/ViewHolder.js +5 -3
- package/dist/recyclerview/ViewHolder.js.map +1 -1
- package/dist/recyclerview/ViewHolderCollection.d.ts +3 -1
- package/dist/recyclerview/ViewHolderCollection.d.ts.map +1 -1
- package/dist/recyclerview/ViewHolderCollection.js +19 -3
- package/dist/recyclerview/ViewHolderCollection.js.map +1 -1
- package/dist/recyclerview/components/ScrollAnchor.d.ts.map +1 -1
- package/dist/recyclerview/components/ScrollAnchor.js +1 -1
- package/dist/recyclerview/components/ScrollAnchor.js.map +1 -1
- package/dist/recyclerview/components/StickyHeaders.d.ts.map +1 -1
- package/dist/recyclerview/components/StickyHeaders.js +44 -17
- package/dist/recyclerview/components/StickyHeaders.js.map +1 -1
- package/dist/recyclerview/hooks/useBoundDetection.d.ts +1 -2
- package/dist/recyclerview/hooks/useBoundDetection.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useBoundDetection.js +19 -16
- package/dist/recyclerview/hooks/useBoundDetection.js.map +1 -1
- package/dist/recyclerview/hooks/useOnLoad.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useOnLoad.js +4 -6
- package/dist/recyclerview/hooks/useOnLoad.js.map +1 -1
- package/dist/recyclerview/hooks/useRecyclerViewController.d.ts +3 -48
- package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useRecyclerViewController.js +93 -71
- package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -1
- package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useRecyclerViewManager.js +6 -0
- package/dist/recyclerview/hooks/useRecyclerViewManager.js.map +1 -1
- package/dist/recyclerview/hooks/useSecondaryProps.js +1 -1
- package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts +10 -3
- package/dist/recyclerview/hooks/useUnmountAwareCallbacks.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js +33 -4
- package/dist/recyclerview/hooks/useUnmountAwareCallbacks.js.map +1 -1
- package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +6 -0
- package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -1
- package/dist/recyclerview/layout-managers/GridLayoutManager.js +27 -5
- package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -1
- package/dist/recyclerview/layout-managers/LayoutManager.d.ts +2 -2
- package/dist/recyclerview/layout-managers/LayoutManager.js +2 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/jestSetup.js +30 -11
- package/package.json +1 -1
- package/src/AnimatedFlashList.ts +3 -2
- package/src/FlashList.tsx +24 -0
- package/src/FlashListProps.ts +16 -7
- package/src/FlashListRef.ts +309 -0
- package/src/__tests__/RecyclerView.test.tsx +83 -29
- package/src/__tests__/RenderStackManager.test.ts +488 -0
- package/src/__tests__/useUnmountAwareCallbacks.test.tsx +12 -12
- package/src/benchmark/useFlatListBenchmark.ts +2 -2
- package/src/index.ts +1 -0
- package/src/recyclerview/RecyclerView.tsx +38 -23
- package/src/recyclerview/RecyclerViewContextProvider.ts +12 -6
- package/src/recyclerview/RecyclerViewManager.ts +73 -88
- package/src/recyclerview/RenderStackManager.ts +265 -0
- package/src/recyclerview/ViewHolder.tsx +5 -3
- package/src/recyclerview/ViewHolderCollection.tsx +29 -8
- package/src/recyclerview/components/ScrollAnchor.tsx +9 -5
- package/src/recyclerview/components/StickyHeaders.tsx +57 -19
- package/src/recyclerview/hooks/useBoundDetection.ts +25 -18
- package/src/recyclerview/hooks/useOnLoad.ts +4 -6
- package/src/recyclerview/hooks/useRecyclerViewController.tsx +104 -125
- package/src/recyclerview/hooks/useRecyclerViewManager.ts +6 -0
- package/src/recyclerview/hooks/useSecondaryProps.tsx +1 -1
- package/src/recyclerview/hooks/useUnmountAwareCallbacks.ts +39 -3
- package/src/recyclerview/layout-managers/GridLayoutManager.ts +30 -7
- package/src/recyclerview/layout-managers/LayoutManager.ts +2 -2
- package/dist/__tests__/RecycleKeyManager.test.d.ts +0 -2
- package/dist/__tests__/RecycleKeyManager.test.d.ts.map +0 -1
- package/dist/__tests__/RecycleKeyManager.test.js +0 -210
- package/dist/__tests__/RecycleKeyManager.test.js.map +0 -1
- package/dist/recyclerview/RecycleKeyManager.d.ts +0 -82
- package/dist/recyclerview/RecycleKeyManager.d.ts.map +0 -1
- package/dist/recyclerview/RecycleKeyManager.js +0 -135
- package/dist/recyclerview/RecycleKeyManager.js.map +0 -1
- package/src/__tests__/RecycleKeyManager.test.ts +0 -254
- package/src/recyclerview/RecycleKeyManager.ts +0 -185
|
@@ -8,7 +8,13 @@ import {
|
|
|
8
8
|
} from "react";
|
|
9
9
|
import { I18nManager } from "react-native";
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
ScrollToOffsetParams,
|
|
13
|
+
ScrollToIndexParams,
|
|
14
|
+
ScrollToItemParams,
|
|
15
|
+
ScrollToEdgeParams,
|
|
16
|
+
FlashListRef,
|
|
17
|
+
} from "../../FlashListRef";
|
|
12
18
|
import { CompatScroller } from "../components/CompatScroller";
|
|
13
19
|
import { RecyclerViewManager } from "../RecyclerViewManager";
|
|
14
20
|
import { adjustOffsetForRTL } from "../utils/adjustOffsetForRTL";
|
|
@@ -17,58 +23,7 @@ import { ScrollAnchorRef } from "../components/ScrollAnchor";
|
|
|
17
23
|
import { PlatformConfig } from "../../native/config/PlatformHelper";
|
|
18
24
|
|
|
19
25
|
import { useUnmountFlag } from "./useUnmountFlag";
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Parameters for scrolling to a specific position in the list.
|
|
24
|
-
* Extends ScrollToEdgeParams to include view positioning options.
|
|
25
|
-
*/
|
|
26
|
-
export interface ScrollToParams extends ScrollToEdgeParams {
|
|
27
|
-
/** Position of the target item relative to the viewport (0 = top, 0.5 = center, 1 = bottom) */
|
|
28
|
-
viewPosition?: number;
|
|
29
|
-
/** Additional offset to apply after viewPosition calculation */
|
|
30
|
-
viewOffset?: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Parameters for scrolling to a specific offset in the list.
|
|
35
|
-
* Used when you want to scroll to an exact pixel position.
|
|
36
|
-
*/
|
|
37
|
-
export interface ScrollToOffsetParams extends ScrollToParams {
|
|
38
|
-
/** The pixel offset to scroll to */
|
|
39
|
-
offset: number;
|
|
40
|
-
/**
|
|
41
|
-
* If true, the first item offset will not be added to the offset calculation.
|
|
42
|
-
* First offset represents header size or top padding.
|
|
43
|
-
*/
|
|
44
|
-
skipFirstItemOffset?: boolean;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Parameters for scrolling to a specific index in the list.
|
|
49
|
-
* Used when you want to scroll to a specific item by its position in the data array.
|
|
50
|
-
*/
|
|
51
|
-
export interface ScrollToIndexParams extends ScrollToParams {
|
|
52
|
-
/** The index of the item to scroll to */
|
|
53
|
-
index: number;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Parameters for scrolling to a specific item in the list.
|
|
58
|
-
* Used when you want to scroll to a specific item by its data value.
|
|
59
|
-
*/
|
|
60
|
-
export interface ScrollToItemParams<T> extends ScrollToParams {
|
|
61
|
-
/** The item to scroll to */
|
|
62
|
-
item: T;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Base parameters for scrolling to the edges of the list.
|
|
67
|
-
*/
|
|
68
|
-
export interface ScrollToEdgeParams {
|
|
69
|
-
/** Whether the scroll should be animated */
|
|
70
|
-
animated?: boolean;
|
|
71
|
-
}
|
|
26
|
+
import { useUnmountAwareTimeout } from "./useUnmountAwareCallbacks";
|
|
72
27
|
|
|
73
28
|
/**
|
|
74
29
|
* Comprehensive hook that manages RecyclerView scrolling behavior and provides
|
|
@@ -88,61 +43,22 @@ export interface ScrollToEdgeParams {
|
|
|
88
43
|
*/
|
|
89
44
|
export function useRecyclerViewController<T>(
|
|
90
45
|
recyclerViewManager: RecyclerViewManager<T>,
|
|
91
|
-
ref: React.Ref<
|
|
46
|
+
ref: React.Ref<FlashListRef<T>>,
|
|
92
47
|
scrollViewRef: RefObject<CompatScroller>,
|
|
93
|
-
scrollAnchorRef: React.RefObject<ScrollAnchorRef
|
|
94
|
-
props: RecyclerViewProps<T>
|
|
48
|
+
scrollAnchorRef: React.RefObject<ScrollAnchorRef>
|
|
95
49
|
) {
|
|
96
|
-
const { horizontal, data } = props;
|
|
97
50
|
const isUnmounted = useUnmountFlag();
|
|
98
51
|
const [_, setRenderId] = useState(0);
|
|
99
52
|
const pauseOffsetCorrection = useRef(false);
|
|
100
53
|
const initialScrollCompletedRef = useRef(false);
|
|
101
|
-
const lastDataLengthRef = useRef(data?.length ?? 0);
|
|
102
|
-
const { setTimeout } =
|
|
54
|
+
const lastDataLengthRef = useRef(recyclerViewManager.props.data?.length ?? 0);
|
|
55
|
+
const { setTimeout } = useUnmountAwareTimeout();
|
|
103
56
|
|
|
104
57
|
// Track the first visible item for maintaining scroll position
|
|
105
58
|
const firstVisibleItemKey = useRef<string | undefined>(undefined);
|
|
106
59
|
const firstVisibleItemLayout = useRef<RVLayout | undefined>(undefined);
|
|
107
60
|
const pendingScrollResolves = useRef<(() => void)[]>([]);
|
|
108
61
|
|
|
109
|
-
const applyInitialScrollIndex = useCallback(() => {
|
|
110
|
-
const initialScrollIndex =
|
|
111
|
-
recyclerViewManager.getInitialScrollIndex() ?? -1;
|
|
112
|
-
const dataLength = props.data?.length ?? 0;
|
|
113
|
-
if (
|
|
114
|
-
initialScrollIndex >= 0 &&
|
|
115
|
-
initialScrollIndex < dataLength &&
|
|
116
|
-
!initialScrollCompletedRef.current &&
|
|
117
|
-
recyclerViewManager.getIsFirstLayoutComplete()
|
|
118
|
-
) {
|
|
119
|
-
// Use setTimeout to ensure that we keep trying to scroll on first few renders
|
|
120
|
-
setTimeout(() => {
|
|
121
|
-
initialScrollCompletedRef.current = true;
|
|
122
|
-
pauseOffsetCorrection.current = false;
|
|
123
|
-
}, 100);
|
|
124
|
-
|
|
125
|
-
pauseOffsetCorrection.current = true;
|
|
126
|
-
|
|
127
|
-
const offset = horizontal
|
|
128
|
-
? recyclerViewManager.getLayout(initialScrollIndex).x
|
|
129
|
-
: recyclerViewManager.getLayout(initialScrollIndex).y;
|
|
130
|
-
handlerMethods.scrollToOffset({
|
|
131
|
-
offset,
|
|
132
|
-
animated: false,
|
|
133
|
-
skipFirstItemOffset: false,
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
setTimeout(() => {
|
|
137
|
-
handlerMethods.scrollToOffset({
|
|
138
|
-
offset,
|
|
139
|
-
animated: false,
|
|
140
|
-
skipFirstItemOffset: false,
|
|
141
|
-
});
|
|
142
|
-
}, 0);
|
|
143
|
-
}
|
|
144
|
-
}, [recyclerViewManager, props.data]);
|
|
145
|
-
|
|
146
62
|
// Handle initial scroll position when the list first loads
|
|
147
63
|
// useOnLoad(recyclerViewManager, () => {
|
|
148
64
|
|
|
@@ -173,19 +89,21 @@ export function useRecyclerViewController<T>(
|
|
|
173
89
|
* the user's current view position when new messages are added.
|
|
174
90
|
*/
|
|
175
91
|
const applyContentOffset = useCallback(async () => {
|
|
92
|
+
const { horizontal, data, keyExtractor, maintainVisibleContentPosition } =
|
|
93
|
+
recyclerViewManager.props;
|
|
176
94
|
// Resolve all pending scroll updates from previous calls
|
|
177
95
|
const resolves = pendingScrollResolves.current;
|
|
178
96
|
pendingScrollResolves.current = [];
|
|
179
97
|
resolves.forEach((resolve) => resolve());
|
|
180
98
|
|
|
181
|
-
const currentDataLength =
|
|
99
|
+
const currentDataLength = data?.length ?? 0;
|
|
182
100
|
|
|
183
101
|
if (
|
|
184
|
-
!
|
|
102
|
+
!horizontal &&
|
|
185
103
|
recyclerViewManager.getIsFirstLayoutComplete() &&
|
|
186
|
-
|
|
104
|
+
keyExtractor &&
|
|
187
105
|
currentDataLength > 0 &&
|
|
188
|
-
|
|
106
|
+
maintainVisibleContentPosition?.disabled !== true
|
|
189
107
|
) {
|
|
190
108
|
const hasDataChanged = currentDataLength !== lastDataLengthRef.current;
|
|
191
109
|
// If we have a tracked first visible item, maintain its position
|
|
@@ -195,14 +113,13 @@ export function useRecyclerViewController<T>(
|
|
|
195
113
|
.getEngagedIndices()
|
|
196
114
|
.findValue(
|
|
197
115
|
(index) =>
|
|
198
|
-
|
|
116
|
+
keyExtractor?.(data![index], index) ===
|
|
199
117
|
firstVisibleItemKey.current
|
|
200
118
|
) ??
|
|
201
119
|
(hasDataChanged
|
|
202
|
-
?
|
|
120
|
+
? data?.findIndex(
|
|
203
121
|
(item, index) =>
|
|
204
|
-
|
|
205
|
-
firstVisibleItemKey.current
|
|
122
|
+
keyExtractor?.(item, index) === firstVisibleItemKey.current
|
|
206
123
|
)
|
|
207
124
|
: undefined);
|
|
208
125
|
|
|
@@ -240,11 +157,11 @@ export function useRecyclerViewController<T>(
|
|
|
240
157
|
// Update the tracked first visible item
|
|
241
158
|
const firstVisibleIndex = Math.max(
|
|
242
159
|
0,
|
|
243
|
-
recyclerViewManager.
|
|
160
|
+
recyclerViewManager.computeVisibleIndices().startIndex
|
|
244
161
|
);
|
|
245
162
|
if (firstVisibleIndex !== undefined && firstVisibleIndex >= 0) {
|
|
246
|
-
firstVisibleItemKey.current =
|
|
247
|
-
|
|
163
|
+
firstVisibleItemKey.current = keyExtractor(
|
|
164
|
+
data![firstVisibleIndex],
|
|
248
165
|
firstVisibleIndex
|
|
249
166
|
);
|
|
250
167
|
firstVisibleItemLayout.current = {
|
|
@@ -252,12 +169,20 @@ export function useRecyclerViewController<T>(
|
|
|
252
169
|
};
|
|
253
170
|
}
|
|
254
171
|
}
|
|
255
|
-
lastDataLengthRef.current =
|
|
256
|
-
}, [
|
|
257
|
-
|
|
258
|
-
|
|
172
|
+
lastDataLengthRef.current = data?.length ?? 0;
|
|
173
|
+
}, [
|
|
174
|
+
recyclerViewManager,
|
|
175
|
+
scrollAnchorRef,
|
|
176
|
+
scrollViewRef,
|
|
177
|
+
setTimeout,
|
|
178
|
+
updateScrollOffsetAsync,
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
const handlerMethods: FlashListRef<T> = useMemo(() => {
|
|
259
182
|
return {
|
|
260
|
-
props
|
|
183
|
+
get props() {
|
|
184
|
+
return recyclerViewManager.props;
|
|
185
|
+
},
|
|
261
186
|
/**
|
|
262
187
|
* Scrolls the list to a specific offset position.
|
|
263
188
|
* Handles RTL layouts and first item offset adjustments.
|
|
@@ -267,6 +192,7 @@ export function useRecyclerViewController<T>(
|
|
|
267
192
|
animated,
|
|
268
193
|
skipFirstItemOffset = true,
|
|
269
194
|
}: ScrollToOffsetParams) => {
|
|
195
|
+
const { horizontal } = recyclerViewManager.props;
|
|
270
196
|
if (scrollViewRef.current) {
|
|
271
197
|
// Adjust offset for RTL layouts in horizontal mode
|
|
272
198
|
if (I18nManager.isRTL && horizontal) {
|
|
@@ -314,6 +240,7 @@ export function useRecyclerViewController<T>(
|
|
|
314
240
|
* Scrolls to the end of the list.
|
|
315
241
|
*/
|
|
316
242
|
scrollToEnd: async ({ animated }: ScrollToEdgeParams = {}) => {
|
|
243
|
+
const { data } = recyclerViewManager.props;
|
|
317
244
|
if (data && data.length > 0) {
|
|
318
245
|
await handlerMethods.scrollToIndex({
|
|
319
246
|
index: data.length - 1,
|
|
@@ -345,11 +272,11 @@ export function useRecyclerViewController<T>(
|
|
|
345
272
|
viewPosition,
|
|
346
273
|
viewOffset,
|
|
347
274
|
}: ScrollToIndexParams) => {
|
|
275
|
+
const { horizontal } = recyclerViewManager.props;
|
|
348
276
|
if (
|
|
349
277
|
scrollViewRef.current &&
|
|
350
|
-
data &&
|
|
351
278
|
index >= 0 &&
|
|
352
|
-
index <
|
|
279
|
+
index < recyclerViewManager.getDataLength()
|
|
353
280
|
) {
|
|
354
281
|
// Pause the scroll offset adjustments
|
|
355
282
|
pauseOffsetCorrection.current = true;
|
|
@@ -424,6 +351,14 @@ export function useRecyclerViewController<T>(
|
|
|
424
351
|
: startScrollOffset +
|
|
425
352
|
(finalOffset - startScrollOffset) * (i / (steps - 1));
|
|
426
353
|
await updateScrollOffsetAsync(nextOffset);
|
|
354
|
+
|
|
355
|
+
// In case some change happens in the middle of this operation
|
|
356
|
+
// and the index is out of bounds, scroll to the end
|
|
357
|
+
if (index >= recyclerViewManager.getDataLength()) {
|
|
358
|
+
handlerMethods.scrollToEnd({ animated });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
427
362
|
const newFinalOffset = getFinalOffset();
|
|
428
363
|
if (
|
|
429
364
|
(newFinalOffset < initialTargetOffset &&
|
|
@@ -480,11 +415,10 @@ export function useRecyclerViewController<T>(
|
|
|
480
415
|
viewPosition,
|
|
481
416
|
viewOffset,
|
|
482
417
|
}: ScrollToItemParams<T>) => {
|
|
418
|
+
const { data } = recyclerViewManager.props;
|
|
483
419
|
if (scrollViewRef.current && data) {
|
|
484
420
|
// Find the index of the item in the data array
|
|
485
|
-
const index =
|
|
486
|
-
(dataItem) => dataItem === item
|
|
487
|
-
);
|
|
421
|
+
const index = data.findIndex((dataItem) => dataItem === item);
|
|
488
422
|
if (index >= 0) {
|
|
489
423
|
handlerMethods.scrollToIndex({
|
|
490
424
|
index,
|
|
@@ -504,7 +438,7 @@ export function useRecyclerViewController<T>(
|
|
|
504
438
|
return recyclerViewManager.getWindowSize();
|
|
505
439
|
},
|
|
506
440
|
getLayout: (index: number) => {
|
|
507
|
-
return recyclerViewManager.
|
|
441
|
+
return recyclerViewManager.tryGetLayout(index);
|
|
508
442
|
},
|
|
509
443
|
getAbsoluteLastScrollOffset: () => {
|
|
510
444
|
return recyclerViewManager.getAbsoluteLastScrollOffset();
|
|
@@ -515,11 +449,11 @@ export function useRecyclerViewController<T>(
|
|
|
515
449
|
recordInteraction: () => {
|
|
516
450
|
recyclerViewManager.recordInteraction();
|
|
517
451
|
},
|
|
518
|
-
|
|
519
|
-
return recyclerViewManager.
|
|
452
|
+
computeVisibleIndices: () => {
|
|
453
|
+
return recyclerViewManager.computeVisibleIndices();
|
|
520
454
|
},
|
|
521
455
|
getFirstVisibleIndex: () => {
|
|
522
|
-
return recyclerViewManager.
|
|
456
|
+
return recyclerViewManager.computeVisibleIndices().startIndex;
|
|
523
457
|
},
|
|
524
458
|
recomputeViewableItems: () => {
|
|
525
459
|
recyclerViewManager.recomputeViewableItems();
|
|
@@ -528,10 +462,55 @@ export function useRecyclerViewController<T>(
|
|
|
528
462
|
* Disables item recycling in preparation for layout animations.
|
|
529
463
|
*/
|
|
530
464
|
prepareForLayoutAnimationRender: () => {
|
|
531
|
-
recyclerViewManager.disableRecycling
|
|
465
|
+
recyclerViewManager.disableRecycling(true);
|
|
532
466
|
},
|
|
533
467
|
};
|
|
534
|
-
}, [
|
|
468
|
+
}, [
|
|
469
|
+
recyclerViewManager,
|
|
470
|
+
scrollViewRef,
|
|
471
|
+
setTimeout,
|
|
472
|
+
isUnmounted,
|
|
473
|
+
updateScrollOffsetAsync,
|
|
474
|
+
]);
|
|
475
|
+
|
|
476
|
+
const applyInitialScrollIndex = useCallback(() => {
|
|
477
|
+
const { horizontal, data } = recyclerViewManager.props;
|
|
478
|
+
|
|
479
|
+
const initialScrollIndex =
|
|
480
|
+
recyclerViewManager.getInitialScrollIndex() ?? -1;
|
|
481
|
+
const dataLength = data?.length ?? 0;
|
|
482
|
+
if (
|
|
483
|
+
initialScrollIndex >= 0 &&
|
|
484
|
+
initialScrollIndex < dataLength &&
|
|
485
|
+
!initialScrollCompletedRef.current &&
|
|
486
|
+
recyclerViewManager.getIsFirstLayoutComplete()
|
|
487
|
+
) {
|
|
488
|
+
// Use setTimeout to ensure that we keep trying to scroll on first few renders
|
|
489
|
+
setTimeout(() => {
|
|
490
|
+
initialScrollCompletedRef.current = true;
|
|
491
|
+
pauseOffsetCorrection.current = false;
|
|
492
|
+
}, 100);
|
|
493
|
+
|
|
494
|
+
pauseOffsetCorrection.current = true;
|
|
495
|
+
|
|
496
|
+
const offset = horizontal
|
|
497
|
+
? recyclerViewManager.getLayout(initialScrollIndex).x
|
|
498
|
+
: recyclerViewManager.getLayout(initialScrollIndex).y;
|
|
499
|
+
handlerMethods.scrollToOffset({
|
|
500
|
+
offset,
|
|
501
|
+
animated: false,
|
|
502
|
+
skipFirstItemOffset: false,
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
setTimeout(() => {
|
|
506
|
+
handlerMethods.scrollToOffset({
|
|
507
|
+
offset,
|
|
508
|
+
animated: false,
|
|
509
|
+
skipFirstItemOffset: false,
|
|
510
|
+
});
|
|
511
|
+
}, 0);
|
|
512
|
+
}
|
|
513
|
+
}, [handlerMethods, recyclerViewManager, setTimeout]);
|
|
535
514
|
|
|
536
515
|
// Expose imperative methods through the ref
|
|
537
516
|
useImperativeHandle(
|
|
@@ -539,8 +518,8 @@ export function useRecyclerViewController<T>(
|
|
|
539
518
|
() => {
|
|
540
519
|
return { ...scrollViewRef.current, ...handlerMethods };
|
|
541
520
|
},
|
|
542
|
-
[handlerMethods]
|
|
521
|
+
[handlerMethods, scrollViewRef]
|
|
543
522
|
);
|
|
544
523
|
|
|
545
|
-
return { applyContentOffset, applyInitialScrollIndex };
|
|
524
|
+
return { applyContentOffset, applyInitialScrollIndex, handlerMethods };
|
|
546
525
|
}
|
|
@@ -14,6 +14,8 @@ export const useRecyclerViewManager = <T>(props: RecyclerViewProps<T>) => {
|
|
|
14
14
|
|
|
15
15
|
useMemo(() => {
|
|
16
16
|
recyclerViewManager.updateProps(props);
|
|
17
|
+
// used to update props so rule can be disabled
|
|
18
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
17
19
|
}, [props]);
|
|
18
20
|
|
|
19
21
|
/**
|
|
@@ -21,6 +23,8 @@ export const useRecyclerViewManager = <T>(props: RecyclerViewProps<T>) => {
|
|
|
21
23
|
*/
|
|
22
24
|
useMemo(() => {
|
|
23
25
|
recyclerViewManager.processDataUpdate();
|
|
26
|
+
// used to process data update so rule can be disabled
|
|
27
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
24
28
|
}, [data]);
|
|
25
29
|
|
|
26
30
|
useEffect(() => {
|
|
@@ -28,6 +32,8 @@ export const useRecyclerViewManager = <T>(props: RecyclerViewProps<T>) => {
|
|
|
28
32
|
recyclerViewManager.dispose();
|
|
29
33
|
velocityTracker.cleanUp();
|
|
30
34
|
};
|
|
35
|
+
// needs to run only on unmount
|
|
36
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
31
37
|
}, []);
|
|
32
38
|
|
|
33
39
|
return { recyclerViewManager, velocityTracker };
|
|
@@ -105,7 +105,7 @@ export function useSecondaryProps<T>(props: RecyclerViewProps<T>) {
|
|
|
105
105
|
const ForwardedScrollComponent = React.forwardRef((_props, ref) =>
|
|
106
106
|
(renderScrollComponent as any)({ ..._props, ref } as any)
|
|
107
107
|
);
|
|
108
|
-
ForwardedScrollComponent.displayName = "
|
|
108
|
+
ForwardedScrollComponent.displayName = "CustomScrollView";
|
|
109
109
|
scrollComponent = ForwardedScrollComponent as any;
|
|
110
110
|
} else if (renderScrollComponent) {
|
|
111
111
|
scrollComponent = renderScrollComponent;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from "react";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Hook that provides
|
|
5
|
-
* Any timeouts created with
|
|
4
|
+
* Hook that provides a setTimeout which is aware of component unmount state.
|
|
5
|
+
* Any timeouts created with this hook will be automatically cleared when the component unmounts.
|
|
6
6
|
*/
|
|
7
|
-
export function
|
|
7
|
+
export function useUnmountAwareTimeout() {
|
|
8
8
|
// Store active timeout IDs in a Set for more efficient add/remove operations
|
|
9
9
|
const [timeoutIds] = useState<Set<NodeJS.Timeout>>(() => new Set());
|
|
10
10
|
|
|
@@ -35,3 +35,39 @@ export function useUnmountAwareCallbacks() {
|
|
|
35
35
|
setTimeout,
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Hook that provides a requestAnimationFrame which is aware of component unmount state.
|
|
41
|
+
* Any animation frames requested with this hook will be automatically canceled when the component unmounts.
|
|
42
|
+
*/
|
|
43
|
+
export function useUnmountAwareAnimationFrame() {
|
|
44
|
+
// Store active animation frame request IDs in a Set for more efficient add/remove operations
|
|
45
|
+
const [requestIds] = useState<Set<number>>(() => new Set());
|
|
46
|
+
|
|
47
|
+
// Cancel all animation frame requests on unmount
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
return () => {
|
|
50
|
+
requestIds.forEach((id) => cancelAnimationFrame(id));
|
|
51
|
+
requestIds.clear();
|
|
52
|
+
};
|
|
53
|
+
}, [requestIds]);
|
|
54
|
+
|
|
55
|
+
// Create a safe requestAnimationFrame that will be canceled on unmount
|
|
56
|
+
const requestAnimationFrame = useCallback(
|
|
57
|
+
(callback: FrameRequestCallback): void => {
|
|
58
|
+
const id = global.requestAnimationFrame((timestamp) => {
|
|
59
|
+
// Remove this request ID from the tracking set
|
|
60
|
+
requestIds.delete(id);
|
|
61
|
+
callback(timestamp);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Track this request ID
|
|
65
|
+
requestIds.add(id);
|
|
66
|
+
},
|
|
67
|
+
[requestIds]
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
requestAnimationFrame,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -78,12 +78,10 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
|
|
|
78
78
|
*/
|
|
79
79
|
getLayoutSize(): RVDimension {
|
|
80
80
|
if (this.layouts.length === 0) return { width: 0, height: 0 };
|
|
81
|
-
const
|
|
82
|
-
this.layouts.length - 1
|
|
83
|
-
);
|
|
81
|
+
const totalHeight = this.computeTotalHeight(this.layouts.length - 1);
|
|
84
82
|
return {
|
|
85
83
|
width: this.boundedSize,
|
|
86
|
-
height:
|
|
84
|
+
height: totalHeight,
|
|
87
85
|
};
|
|
88
86
|
}
|
|
89
87
|
|
|
@@ -93,7 +91,9 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
|
|
|
93
91
|
* @param endIndex Ending index of items to recompute
|
|
94
92
|
*/
|
|
95
93
|
recomputeLayouts(startIndex: number, endIndex: number): void {
|
|
96
|
-
const newStartIndex = this.locateFirstNeighbourIndex(
|
|
94
|
+
const newStartIndex = this.locateFirstNeighbourIndex(
|
|
95
|
+
Math.max(0, startIndex - 1)
|
|
96
|
+
);
|
|
97
97
|
const startVal = this.getLayout(newStartIndex);
|
|
98
98
|
|
|
99
99
|
let startX = startVal.x;
|
|
@@ -102,7 +102,7 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
|
|
|
102
102
|
for (let i = newStartIndex; i <= endIndex; i++) {
|
|
103
103
|
const layout = this.getLayout(i);
|
|
104
104
|
if (!this.checkBounds(startX, layout.width)) {
|
|
105
|
-
const tallestItem = this.processAndReturnTallestItemInRow(i);
|
|
105
|
+
const tallestItem = this.processAndReturnTallestItemInRow(i - 1);
|
|
106
106
|
startY = tallestItem.y + tallestItem.height;
|
|
107
107
|
startX = 0;
|
|
108
108
|
}
|
|
@@ -111,6 +111,9 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
|
|
|
111
111
|
layout.y = startY;
|
|
112
112
|
startX += layout.width;
|
|
113
113
|
}
|
|
114
|
+
if (endIndex === this.layouts.length - 1) {
|
|
115
|
+
this.processAndReturnTallestItemInRow(endIndex);
|
|
116
|
+
}
|
|
114
117
|
}
|
|
115
118
|
|
|
116
119
|
/**
|
|
@@ -182,6 +185,26 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
|
|
|
182
185
|
return tallestItem;
|
|
183
186
|
}
|
|
184
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Computes the total height of the layout.
|
|
190
|
+
* @param index Index of the last item in the layout
|
|
191
|
+
* @returns Total height of the layout
|
|
192
|
+
*/
|
|
193
|
+
private computeTotalHeight(index: number): number {
|
|
194
|
+
const startIndex = this.locateFirstNeighbourIndex(index);
|
|
195
|
+
const y = this.layouts[startIndex].y;
|
|
196
|
+
let maxHeight = 0;
|
|
197
|
+
let i = startIndex;
|
|
198
|
+
while (Math.ceil(this.layouts[i].y) === Math.ceil(y)) {
|
|
199
|
+
maxHeight = Math.max(maxHeight, this.layouts[i].height);
|
|
200
|
+
i++;
|
|
201
|
+
if (i >= this.layouts.length) {
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return y + maxHeight;
|
|
206
|
+
}
|
|
207
|
+
|
|
185
208
|
/**
|
|
186
209
|
* Checks if an item can fit within the bounded width.
|
|
187
210
|
* @param itemX Starting X position of the item
|
|
@@ -201,7 +224,7 @@ export class RVGridLayoutManagerImpl extends RVLayoutManager {
|
|
|
201
224
|
if (startIndex === 0) {
|
|
202
225
|
return 0;
|
|
203
226
|
}
|
|
204
|
-
let i = startIndex
|
|
227
|
+
let i = startIndex;
|
|
205
228
|
for (; i >= 0; i--) {
|
|
206
229
|
if (this.layouts[i].x === 0) {
|
|
207
230
|
break;
|
|
@@ -114,8 +114,8 @@ export abstract class RVLayoutManager {
|
|
|
114
114
|
/**
|
|
115
115
|
* Gets indices of items currently visible in the viewport.
|
|
116
116
|
* Uses binary search for efficient lookup.
|
|
117
|
-
* @param unboundDimensionStart Start position of viewport
|
|
118
|
-
* @param unboundDimensionEnd End position of viewport
|
|
117
|
+
* @param unboundDimensionStart Start position of viewport (start X or start Y)
|
|
118
|
+
* @param unboundDimensionEnd End position of viewport (end X or end Y)
|
|
119
119
|
* @returns ConsecutiveNumbers containing visible indices
|
|
120
120
|
*/
|
|
121
121
|
getVisibleLayouts(
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"RecycleKeyManager.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/RecycleKeyManager.test.ts"],"names":[],"mappings":""}
|