@shopify/flash-list 2.0.0-rc.1 → 2.0.0-rc.3
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 +1 -1
- package/dist/FlashListProps.d.ts +10 -2
- package/dist/FlashListProps.d.ts.map +1 -1
- package/dist/FlashListProps.js.map +1 -1
- package/dist/__tests__/RenderStackManager.test.js +1 -2
- package/dist/__tests__/RenderStackManager.test.js.map +1 -1
- package/dist/recyclerview/RecyclerView.d.ts.map +1 -1
- package/dist/recyclerview/RecyclerView.js +11 -3
- package/dist/recyclerview/RecyclerView.js.map +1 -1
- package/dist/recyclerview/RecyclerViewManager.d.ts +1 -0
- package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -1
- package/dist/recyclerview/RecyclerViewManager.js +5 -0
- package/dist/recyclerview/RecyclerViewManager.js.map +1 -1
- package/dist/recyclerview/RenderStackManager.d.ts +1 -0
- package/dist/recyclerview/RenderStackManager.d.ts.map +1 -1
- package/dist/recyclerview/RenderStackManager.js +26 -7
- package/dist/recyclerview/RenderStackManager.js.map +1 -1
- package/dist/recyclerview/components/StickyHeaders.js +1 -1
- package/dist/recyclerview/components/StickyHeaders.js.map +1 -1
- package/dist/recyclerview/helpers/RenderTimeTracker.d.ts +1 -0
- package/dist/recyclerview/helpers/RenderTimeTracker.d.ts.map +1 -1
- package/dist/recyclerview/helpers/RenderTimeTracker.js +3 -0
- package/dist/recyclerview/helpers/RenderTimeTracker.js.map +1 -1
- package/dist/recyclerview/hooks/useLayoutState.d.ts +3 -1
- package/dist/recyclerview/hooks/useLayoutState.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useLayoutState.js +5 -3
- package/dist/recyclerview/hooks/useLayoutState.js.map +1 -1
- package/dist/recyclerview/hooks/useRecyclerViewController.d.ts +2 -1
- package/dist/recyclerview/hooks/useRecyclerViewController.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useRecyclerViewController.js +232 -187
- 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 +2 -1
- package/dist/recyclerview/hooks/useRecyclerViewManager.js.map +1 -1
- package/dist/recyclerview/hooks/useRecyclingState.d.ts +4 -2
- package/dist/recyclerview/hooks/useRecyclingState.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useRecyclingState.js +2 -2
- package/dist/recyclerview/hooks/useRecyclingState.js.map +1 -1
- package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts +14 -6
- package/dist/recyclerview/layout-managers/GridLayoutManager.d.ts.map +1 -1
- package/dist/recyclerview/layout-managers/GridLayoutManager.js +40 -23
- package/dist/recyclerview/layout-managers/GridLayoutManager.js.map +1 -1
- package/dist/recyclerview/layout-managers/LayoutManager.d.ts +26 -6
- package/dist/recyclerview/layout-managers/LayoutManager.d.ts.map +1 -1
- package/dist/recyclerview/layout-managers/LayoutManager.js +89 -15
- package/dist/recyclerview/layout-managers/LayoutManager.js.map +1 -1
- package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts +9 -1
- package/dist/recyclerview/layout-managers/MasonryLayoutManager.d.ts.map +1 -1
- package/dist/recyclerview/layout-managers/MasonryLayoutManager.js +28 -12
- package/dist/recyclerview/layout-managers/MasonryLayoutManager.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/viewability/ViewabilityManager.d.ts.map +1 -1
- package/dist/viewability/ViewabilityManager.js +10 -3
- package/dist/viewability/ViewabilityManager.js.map +1 -1
- package/package.json +1 -1
- package/src/FlashListProps.ts +16 -2
- package/src/__tests__/RenderStackManager.test.ts +1 -2
- package/src/recyclerview/RecyclerView.tsx +27 -14
- package/src/recyclerview/RecyclerViewManager.ts +6 -0
- package/src/recyclerview/RenderStackManager.ts +32 -6
- package/src/recyclerview/components/StickyHeaders.tsx +1 -1
- package/src/recyclerview/helpers/RenderTimeTracker.ts +4 -0
- package/src/recyclerview/hooks/useLayoutState.ts +15 -6
- package/src/recyclerview/hooks/useRecyclerViewController.tsx +232 -165
- package/src/recyclerview/hooks/useRecyclerViewManager.ts +3 -1
- package/src/recyclerview/hooks/useRecyclingState.ts +11 -7
- package/src/recyclerview/layout-managers/GridLayoutManager.ts +44 -23
- package/src/recyclerview/layout-managers/LayoutManager.ts +98 -20
- package/src/recyclerview/layout-managers/MasonryLayoutManager.ts +30 -8
- package/src/viewability/ViewabilityManager.ts +10 -6
|
@@ -57,43 +57,71 @@ export function useRecyclerViewController<T>(
|
|
|
57
57
|
// Track the first visible item for maintaining scroll position
|
|
58
58
|
const firstVisibleItemKey = useRef<string | undefined>(undefined);
|
|
59
59
|
const firstVisibleItemLayout = useRef<RVLayout | undefined>(undefined);
|
|
60
|
-
|
|
60
|
+
|
|
61
|
+
// Queue to store callbacks that should be executed after scroll offset updates
|
|
62
|
+
const pendingScrollCallbacks = useRef<(() => void)[]>([]);
|
|
61
63
|
|
|
62
64
|
// Handle initial scroll position when the list first loads
|
|
63
65
|
// useOnLoad(recyclerViewManager, () => {
|
|
64
66
|
|
|
65
67
|
// });
|
|
66
68
|
/**
|
|
67
|
-
* Updates the scroll offset and
|
|
68
|
-
*
|
|
69
|
+
* Updates the scroll offset and calls the provided callback
|
|
70
|
+
* after the update has been applied and the component has re-rendered.
|
|
71
|
+
*
|
|
72
|
+
* @param offset - The new scroll offset to apply
|
|
73
|
+
* @param callback - Optional callback to execute after the update is applied
|
|
69
74
|
*/
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
const updateScrollOffsetWithCallback = useCallback(
|
|
76
|
+
(offset: number, callback: () => void): void => {
|
|
77
|
+
// Attempt to update the scroll offset in the RecyclerViewManager
|
|
78
|
+
// This returns undefined if no update is needed
|
|
79
|
+
if (recyclerViewManager.updateScrollOffset(offset) !== undefined) {
|
|
80
|
+
// It will be executed after the next render
|
|
81
|
+
pendingScrollCallbacks.current.push(callback);
|
|
82
|
+
// Trigger a re-render to apply the scroll offset update
|
|
83
|
+
setRenderId((prev) => prev + 1);
|
|
84
|
+
} else {
|
|
85
|
+
// No update needed, execute callback immediately
|
|
86
|
+
callback();
|
|
87
|
+
}
|
|
82
88
|
},
|
|
83
89
|
[recyclerViewManager]
|
|
84
90
|
);
|
|
85
91
|
|
|
92
|
+
const computeFirstVisibleIndexForOffsetCorrection = useCallback(() => {
|
|
93
|
+
const { data, keyExtractor } = recyclerViewManager.props;
|
|
94
|
+
if (data && keyExtractor) {
|
|
95
|
+
// Update the tracked first visible item
|
|
96
|
+
const firstVisibleIndex = Math.max(
|
|
97
|
+
0,
|
|
98
|
+
recyclerViewManager.computeVisibleIndices().startIndex
|
|
99
|
+
);
|
|
100
|
+
if (firstVisibleIndex !== undefined && firstVisibleIndex >= 0) {
|
|
101
|
+
firstVisibleItemKey.current = keyExtractor(
|
|
102
|
+
data![firstVisibleIndex],
|
|
103
|
+
firstVisibleIndex
|
|
104
|
+
);
|
|
105
|
+
firstVisibleItemLayout.current = {
|
|
106
|
+
...recyclerViewManager.getLayout(firstVisibleIndex),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}, [recyclerViewManager]);
|
|
111
|
+
|
|
86
112
|
/**
|
|
87
113
|
* Maintains the visible content position when the list updates.
|
|
88
114
|
* This is particularly useful for chat applications where we want to keep
|
|
89
115
|
* the user's current view position when new messages are added.
|
|
90
116
|
*/
|
|
91
|
-
const
|
|
117
|
+
const applyOffsetCorrection = useCallback(() => {
|
|
92
118
|
const { horizontal, data, keyExtractor } = recyclerViewManager.props;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
119
|
+
|
|
120
|
+
// Execute all pending callbacks from previous scroll offset updates
|
|
121
|
+
// This ensures any scroll operations that were waiting for render are completed
|
|
122
|
+
const callbacks = pendingScrollCallbacks.current;
|
|
123
|
+
pendingScrollCallbacks.current = [];
|
|
124
|
+
callbacks.forEach((callback) => callback());
|
|
97
125
|
|
|
98
126
|
const currentDataLength = data?.length ?? 0;
|
|
99
127
|
|
|
@@ -152,8 +180,9 @@ export function useRecyclerViewController<T>(
|
|
|
152
180
|
scrollViewRef.current?.scrollTo(scrollToParams);
|
|
153
181
|
}
|
|
154
182
|
if (hasDataChanged) {
|
|
155
|
-
|
|
156
|
-
recyclerViewManager.getAbsoluteLastScrollOffset() + diff
|
|
183
|
+
updateScrollOffsetWithCallback(
|
|
184
|
+
recyclerViewManager.getAbsoluteLastScrollOffset() + diff,
|
|
185
|
+
() => {}
|
|
157
186
|
);
|
|
158
187
|
recyclerViewManager.ignoreScrollEvents = true;
|
|
159
188
|
setTimeout(() => {
|
|
@@ -164,20 +193,7 @@ export function useRecyclerViewController<T>(
|
|
|
164
193
|
}
|
|
165
194
|
}
|
|
166
195
|
|
|
167
|
-
|
|
168
|
-
const firstVisibleIndex = Math.max(
|
|
169
|
-
0,
|
|
170
|
-
recyclerViewManager.computeVisibleIndices().startIndex
|
|
171
|
-
);
|
|
172
|
-
if (firstVisibleIndex !== undefined && firstVisibleIndex >= 0) {
|
|
173
|
-
firstVisibleItemKey.current = keyExtractor(
|
|
174
|
-
data![firstVisibleIndex],
|
|
175
|
-
firstVisibleIndex
|
|
176
|
-
);
|
|
177
|
-
firstVisibleItemLayout.current = {
|
|
178
|
-
...recyclerViewManager.getLayout(firstVisibleIndex),
|
|
179
|
-
};
|
|
180
|
-
}
|
|
196
|
+
computeFirstVisibleIndexForOffsetCorrection();
|
|
181
197
|
}
|
|
182
198
|
lastDataLengthRef.current = data?.length ?? 0;
|
|
183
199
|
}, [
|
|
@@ -185,7 +201,8 @@ export function useRecyclerViewController<T>(
|
|
|
185
201
|
scrollAnchorRef,
|
|
186
202
|
scrollViewRef,
|
|
187
203
|
setTimeout,
|
|
188
|
-
|
|
204
|
+
updateScrollOffsetWithCallback,
|
|
205
|
+
computeFirstVisibleIndexForOffsetCorrection,
|
|
189
206
|
]);
|
|
190
207
|
|
|
191
208
|
const handlerMethods: FlashListRef<T> = useMemo(() => {
|
|
@@ -278,144 +295,189 @@ export function useRecyclerViewController<T>(
|
|
|
278
295
|
/**
|
|
279
296
|
* Scrolls to a specific index in the list.
|
|
280
297
|
* Supports viewPosition and viewOffset for precise positioning.
|
|
298
|
+
* Returns a Promise that resolves when the scroll is complete.
|
|
281
299
|
*/
|
|
282
|
-
scrollToIndex:
|
|
300
|
+
scrollToIndex: ({
|
|
283
301
|
index,
|
|
284
302
|
animated,
|
|
285
303
|
viewPosition,
|
|
286
304
|
viewOffset,
|
|
287
|
-
}: ScrollToIndexParams) => {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
305
|
+
}: ScrollToIndexParams): Promise<void> => {
|
|
306
|
+
return new Promise((resolve) => {
|
|
307
|
+
const { horizontal } = recyclerViewManager.props;
|
|
308
|
+
if (
|
|
309
|
+
scrollViewRef.current &&
|
|
310
|
+
index >= 0 &&
|
|
311
|
+
index < recyclerViewManager.getDataLength()
|
|
312
|
+
) {
|
|
313
|
+
// Pause the scroll offset adjustments
|
|
314
|
+
pauseOffsetCorrection.current = true;
|
|
315
|
+
recyclerViewManager.setOffsetProjectionEnabled(false);
|
|
316
|
+
|
|
317
|
+
const getFinalOffset = () => {
|
|
318
|
+
const layout = recyclerViewManager.getLayout(index);
|
|
319
|
+
const offset = horizontal ? layout.x : layout.y;
|
|
320
|
+
let finalOffset = offset;
|
|
321
|
+
// take viewPosition etc into account
|
|
322
|
+
if (viewPosition !== undefined || viewOffset !== undefined) {
|
|
323
|
+
const containerSize = horizontal
|
|
324
|
+
? recyclerViewManager.getWindowSize().width
|
|
325
|
+
: recyclerViewManager.getWindowSize().height;
|
|
326
|
+
|
|
327
|
+
const itemSize = horizontal ? layout.width : layout.height;
|
|
328
|
+
|
|
329
|
+
if (viewPosition !== undefined) {
|
|
330
|
+
// viewPosition: 0 = top, 0.5 = center, 1 = bottom
|
|
331
|
+
finalOffset =
|
|
332
|
+
offset - (containerSize - itemSize) * viewPosition;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (viewOffset !== undefined) {
|
|
336
|
+
finalOffset += viewOffset;
|
|
337
|
+
}
|
|
314
338
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
339
|
+
return finalOffset + recyclerViewManager.firstItemOffset;
|
|
340
|
+
};
|
|
341
|
+
const lastAbsoluteScrollOffset =
|
|
342
|
+
recyclerViewManager.getAbsoluteLastScrollOffset();
|
|
343
|
+
const bufferForScroll = horizontal
|
|
344
|
+
? recyclerViewManager.getWindowSize().width
|
|
345
|
+
: recyclerViewManager.getWindowSize().height;
|
|
346
|
+
|
|
347
|
+
const bufferForCompute = bufferForScroll * 2;
|
|
348
|
+
|
|
349
|
+
const getStartScrollOffset = () => {
|
|
350
|
+
let lastScrollOffset = lastAbsoluteScrollOffset;
|
|
351
|
+
const finalOffset = getFinalOffset();
|
|
352
|
+
|
|
353
|
+
if (finalOffset > lastScrollOffset) {
|
|
354
|
+
lastScrollOffset = Math.max(
|
|
355
|
+
finalOffset - bufferForCompute,
|
|
356
|
+
lastScrollOffset
|
|
357
|
+
);
|
|
358
|
+
recyclerViewManager.setScrollDirection("forward");
|
|
359
|
+
} else {
|
|
360
|
+
lastScrollOffset = Math.min(
|
|
361
|
+
finalOffset + bufferForCompute,
|
|
362
|
+
lastScrollOffset
|
|
363
|
+
);
|
|
364
|
+
recyclerViewManager.setScrollDirection("backward");
|
|
365
|
+
}
|
|
366
|
+
return lastScrollOffset;
|
|
367
|
+
};
|
|
368
|
+
let initialTargetOffset = getFinalOffset();
|
|
369
|
+
let initialStartScrollOffset = getStartScrollOffset();
|
|
370
|
+
let finalOffset = initialTargetOffset;
|
|
371
|
+
let startScrollOffset = initialStartScrollOffset;
|
|
372
|
+
|
|
373
|
+
const steps = 5;
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Recursively performs the scroll animation steps.
|
|
377
|
+
* This function replaces the async/await loop with callback-based execution.
|
|
378
|
+
*
|
|
379
|
+
* @param currentStep - The current step in the animation (0 to steps-1)
|
|
380
|
+
*/
|
|
381
|
+
const performScrollStep = (currentStep: number) => {
|
|
382
|
+
// Check if component is unmounted or we've completed all steps
|
|
383
|
+
if (isUnmounted.current) {
|
|
384
|
+
return;
|
|
385
|
+
} else if (currentStep >= steps) {
|
|
386
|
+
// All steps completed, perform final scroll
|
|
387
|
+
finishScrollToIndex();
|
|
388
|
+
return;
|
|
318
389
|
}
|
|
319
|
-
}
|
|
320
|
-
return finalOffset + recyclerViewManager.firstItemOffset;
|
|
321
|
-
};
|
|
322
|
-
const lastAbsoluteScrollOffset =
|
|
323
|
-
recyclerViewManager.getAbsoluteLastScrollOffset();
|
|
324
|
-
const bufferForScroll = horizontal
|
|
325
|
-
? recyclerViewManager.getWindowSize().width
|
|
326
|
-
: recyclerViewManager.getWindowSize().height;
|
|
327
|
-
|
|
328
|
-
const bufferForCompute = bufferForScroll * 2;
|
|
329
|
-
|
|
330
|
-
const getStartScrollOffset = () => {
|
|
331
|
-
let lastScrollOffset = lastAbsoluteScrollOffset;
|
|
332
|
-
const finalOffset = getFinalOffset();
|
|
333
|
-
|
|
334
|
-
if (finalOffset > lastScrollOffset) {
|
|
335
|
-
lastScrollOffset = Math.max(
|
|
336
|
-
finalOffset - bufferForCompute,
|
|
337
|
-
lastScrollOffset
|
|
338
|
-
);
|
|
339
|
-
recyclerViewManager.setScrollDirection("forward");
|
|
340
|
-
} else {
|
|
341
|
-
lastScrollOffset = Math.min(
|
|
342
|
-
finalOffset + bufferForCompute,
|
|
343
|
-
lastScrollOffset
|
|
344
|
-
);
|
|
345
|
-
recyclerViewManager.setScrollDirection("backward");
|
|
346
|
-
}
|
|
347
|
-
return lastScrollOffset;
|
|
348
|
-
};
|
|
349
|
-
let initialTargetOffset = getFinalOffset();
|
|
350
|
-
let initialStartScrollOffset = getStartScrollOffset();
|
|
351
|
-
let finalOffset = initialTargetOffset;
|
|
352
|
-
let startScrollOffset = initialStartScrollOffset;
|
|
353
|
-
|
|
354
|
-
const steps = 5;
|
|
355
|
-
// go from finalOffset to startScrollOffset in 5 steps for animated
|
|
356
|
-
// otherwise go from startScrollOffset to finalOffset in 5 steps
|
|
357
|
-
for (let i = 0; i < steps; i++) {
|
|
358
|
-
if (isUnmounted.current) {
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
const nextOffset = animated
|
|
362
|
-
? finalOffset +
|
|
363
|
-
(startScrollOffset - finalOffset) * (i / (steps - 1))
|
|
364
|
-
: startScrollOffset +
|
|
365
|
-
(finalOffset - startScrollOffset) * (i / (steps - 1));
|
|
366
|
-
await updateScrollOffsetAsync(nextOffset);
|
|
367
|
-
|
|
368
|
-
// In case some change happens in the middle of this operation
|
|
369
|
-
// and the index is out of bounds, scroll to the end
|
|
370
|
-
if (index >= recyclerViewManager.getDataLength()) {
|
|
371
|
-
handlerMethods.scrollToEnd({ animated });
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
390
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
391
|
+
// Calculate the offset for this step
|
|
392
|
+
// For animated scrolls: interpolate from finalOffset to startScrollOffset
|
|
393
|
+
// For non-animated: interpolate from startScrollOffset to finalOffset
|
|
394
|
+
const nextOffset = animated
|
|
395
|
+
? finalOffset +
|
|
396
|
+
(startScrollOffset - finalOffset) *
|
|
397
|
+
(currentStep / (steps - 1))
|
|
398
|
+
: startScrollOffset +
|
|
399
|
+
(finalOffset - startScrollOffset) *
|
|
400
|
+
(currentStep / (steps - 1));
|
|
401
|
+
|
|
402
|
+
// Update scroll offset with a callback to continue to the next step
|
|
403
|
+
updateScrollOffsetWithCallback(nextOffset, () => {
|
|
404
|
+
// Check if the index is still valid after the update
|
|
405
|
+
if (index >= recyclerViewManager.getDataLength()) {
|
|
406
|
+
// Index out of bounds, scroll to end instead
|
|
407
|
+
handlerMethods.scrollToEnd({ animated });
|
|
408
|
+
resolve(); // Resolve the promise as we're done
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Check if the target position has changed significantly
|
|
413
|
+
const newFinalOffset = getFinalOffset();
|
|
414
|
+
if (
|
|
415
|
+
(newFinalOffset < initialTargetOffset &&
|
|
416
|
+
newFinalOffset < initialStartScrollOffset) ||
|
|
417
|
+
(newFinalOffset > initialTargetOffset &&
|
|
418
|
+
newFinalOffset > initialStartScrollOffset)
|
|
419
|
+
) {
|
|
420
|
+
// Target has moved, recalculate and restart from beginning
|
|
421
|
+
finalOffset = newFinalOffset;
|
|
422
|
+
startScrollOffset = getStartScrollOffset();
|
|
423
|
+
initialTargetOffset = newFinalOffset;
|
|
424
|
+
initialStartScrollOffset = startScrollOffset;
|
|
425
|
+
performScrollStep(0); // Restart from step 0
|
|
426
|
+
} else {
|
|
427
|
+
// Continue to next step
|
|
428
|
+
performScrollStep(currentStep + 1);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Completes the scroll to index operation by performing the final scroll
|
|
435
|
+
* and re-enabling offset correction after a delay.
|
|
436
|
+
*/
|
|
437
|
+
const finishScrollToIndex = () => {
|
|
438
|
+
finalOffset = getFinalOffset();
|
|
439
|
+
const maxOffset = recyclerViewManager.getMaxScrollOffset();
|
|
440
|
+
|
|
441
|
+
if (finalOffset > maxOffset) {
|
|
442
|
+
finalOffset = maxOffset;
|
|
443
|
+
}
|
|
389
444
|
|
|
390
|
-
|
|
391
|
-
|
|
445
|
+
if (animated) {
|
|
446
|
+
// For animated scrolls, first jump to the start position
|
|
447
|
+
// We don't need to add firstItemOffset here as it's already added
|
|
448
|
+
handlerMethods.scrollToOffset({
|
|
449
|
+
offset: startScrollOffset,
|
|
450
|
+
animated: false,
|
|
451
|
+
skipFirstItemOffset: true,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
392
454
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
455
|
+
// Perform the final scroll to the target position
|
|
456
|
+
handlerMethods.scrollToOffset({
|
|
457
|
+
offset: finalOffset,
|
|
458
|
+
animated,
|
|
459
|
+
skipFirstItemOffset: true,
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Re-enable offset correction after a delay
|
|
463
|
+
// Longer delay for animated scrolls to allow animation to complete
|
|
464
|
+
setTimeout(
|
|
465
|
+
() => {
|
|
466
|
+
pauseOffsetCorrection.current = false;
|
|
467
|
+
recyclerViewManager.setOffsetProjectionEnabled(true);
|
|
468
|
+
resolve(); // Resolve the promise after re-enabling corrections
|
|
469
|
+
},
|
|
470
|
+
animated ? 300 : 200
|
|
471
|
+
);
|
|
472
|
+
};
|
|
396
473
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
skipFirstItemOffset: true,
|
|
403
|
-
});
|
|
474
|
+
// Start the scroll animation process
|
|
475
|
+
performScrollStep(0);
|
|
476
|
+
} else {
|
|
477
|
+
// Invalid parameters, resolve immediately
|
|
478
|
+
resolve();
|
|
404
479
|
}
|
|
405
|
-
|
|
406
|
-
offset: finalOffset,
|
|
407
|
-
animated,
|
|
408
|
-
skipFirstItemOffset: true,
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
setTimeout(
|
|
412
|
-
() => {
|
|
413
|
-
pauseOffsetCorrection.current = false;
|
|
414
|
-
recyclerViewManager.setOffsetProjectionEnabled(true);
|
|
415
|
-
},
|
|
416
|
-
animated ? 300 : 200
|
|
417
|
-
);
|
|
418
|
-
}
|
|
480
|
+
});
|
|
419
481
|
},
|
|
420
482
|
|
|
421
483
|
/**
|
|
@@ -483,7 +545,7 @@ export function useRecyclerViewController<T>(
|
|
|
483
545
|
scrollViewRef,
|
|
484
546
|
setTimeout,
|
|
485
547
|
isUnmounted,
|
|
486
|
-
|
|
548
|
+
updateScrollOffsetWithCallback,
|
|
487
549
|
]);
|
|
488
550
|
|
|
489
551
|
const applyInitialScrollIndex = useCallback(() => {
|
|
@@ -534,5 +596,10 @@ export function useRecyclerViewController<T>(
|
|
|
534
596
|
[handlerMethods, scrollViewRef]
|
|
535
597
|
);
|
|
536
598
|
|
|
537
|
-
return {
|
|
599
|
+
return {
|
|
600
|
+
applyOffsetCorrection,
|
|
601
|
+
computeFirstVisibleIndexForOffsetCorrection,
|
|
602
|
+
applyInitialScrollIndex,
|
|
603
|
+
handlerMethods,
|
|
604
|
+
};
|
|
538
605
|
}
|
|
@@ -28,11 +28,13 @@ export const useRecyclerViewManager = <T>(props: RecyclerViewProps<T>) => {
|
|
|
28
28
|
}, [data]);
|
|
29
29
|
|
|
30
30
|
useEffect(() => {
|
|
31
|
+
recyclerViewManager.restoreIfNeeded();
|
|
32
|
+
|
|
31
33
|
return () => {
|
|
32
34
|
recyclerViewManager.dispose();
|
|
33
35
|
velocityTracker.cleanUp();
|
|
34
36
|
};
|
|
35
|
-
//
|
|
37
|
+
// Used to perform cleanup on unmount
|
|
36
38
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
37
39
|
}, []);
|
|
38
40
|
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useCallback, useMemo, useRef } from "react";
|
|
2
2
|
|
|
3
|
-
import { useLayoutState } from "./useLayoutState";
|
|
3
|
+
import { LayoutStateSetter, useLayoutState } from "./useLayoutState";
|
|
4
|
+
|
|
5
|
+
export type RecyclingStateSetter<T> = LayoutStateSetter<T>;
|
|
6
|
+
|
|
7
|
+
export type RecyclingStateInitialValue<T> = T | (() => T);
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* A custom hook that provides state management with automatic reset functionality.
|
|
@@ -16,10 +20,10 @@ import { useLayoutState } from "./useLayoutState";
|
|
|
16
20
|
* - A setState function that works like useState's setState
|
|
17
21
|
*/
|
|
18
22
|
export function useRecyclingState<T>(
|
|
19
|
-
initialState: T
|
|
23
|
+
initialState: RecyclingStateInitialValue<T>,
|
|
20
24
|
deps: React.DependencyList,
|
|
21
25
|
onReset?: () => void
|
|
22
|
-
): [T,
|
|
26
|
+
): [T, RecyclingStateSetter<T>] {
|
|
23
27
|
// Store the current state value in a ref to persist between renders
|
|
24
28
|
const valueStore = useRef<T>();
|
|
25
29
|
// Use layoutState to trigger re-renders when state changes
|
|
@@ -42,8 +46,8 @@ export function useRecyclingState<T>(
|
|
|
42
46
|
* Proxy setState function that updates the stored value and triggers a re-render.
|
|
43
47
|
* Only triggers a re-render if the new value is different from the current value.
|
|
44
48
|
*/
|
|
45
|
-
const setStateProxy = useCallback(
|
|
46
|
-
(newValue
|
|
49
|
+
const setStateProxy: RecyclingStateSetter<T> = useCallback(
|
|
50
|
+
(newValue, skipParentLayout) => {
|
|
47
51
|
// Calculate next state value from function or direct value
|
|
48
52
|
const nextState =
|
|
49
53
|
typeof newValue === "function"
|
|
@@ -53,7 +57,7 @@ export function useRecyclingState<T>(
|
|
|
53
57
|
// Only update and trigger re-render if value has changed
|
|
54
58
|
if (nextState !== valueStore.current) {
|
|
55
59
|
valueStore.current = nextState;
|
|
56
|
-
setCounter((prev) => prev + 1);
|
|
60
|
+
setCounter((prev) => prev + 1, skipParentLayout);
|
|
57
61
|
}
|
|
58
62
|
},
|
|
59
63
|
[setCounter]
|