@shopify/flash-list 2.0.0-rc.2 → 2.0.0-rc.4
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/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/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 +237 -190
- 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/helpers/RenderTimeTracker.ts +4 -0
- package/src/recyclerview/hooks/useLayoutState.ts +15 -6
- package/src/recyclerview/hooks/useRecyclerViewController.tsx +240 -168
- 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
|
@@ -51,51 +51,84 @@ export function useRecyclerViewController<T>(
|
|
|
51
51
|
const [_, setRenderId] = useState(0);
|
|
52
52
|
const pauseOffsetCorrection = useRef(false);
|
|
53
53
|
const initialScrollCompletedRef = useRef(false);
|
|
54
|
-
const lastDataLengthRef = useRef(recyclerViewManager.
|
|
54
|
+
const lastDataLengthRef = useRef(recyclerViewManager.getDataLength());
|
|
55
55
|
const { setTimeout } = useUnmountAwareTimeout();
|
|
56
56
|
|
|
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 (
|
|
95
|
+
recyclerViewManager.getIsFirstLayoutComplete() &&
|
|
96
|
+
keyExtractor &&
|
|
97
|
+
recyclerViewManager.getDataLength() > 0 &&
|
|
98
|
+
recyclerViewManager.shouldMaintainVisibleContentPosition()
|
|
99
|
+
) {
|
|
100
|
+
// Update the tracked first visible item
|
|
101
|
+
const firstVisibleIndex = Math.max(
|
|
102
|
+
0,
|
|
103
|
+
recyclerViewManager.computeVisibleIndices().startIndex
|
|
104
|
+
);
|
|
105
|
+
if (firstVisibleIndex !== undefined && firstVisibleIndex >= 0) {
|
|
106
|
+
firstVisibleItemKey.current = keyExtractor(
|
|
107
|
+
data![firstVisibleIndex],
|
|
108
|
+
firstVisibleIndex
|
|
109
|
+
);
|
|
110
|
+
firstVisibleItemLayout.current = {
|
|
111
|
+
...recyclerViewManager.getLayout(firstVisibleIndex),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}, [recyclerViewManager]);
|
|
116
|
+
|
|
86
117
|
/**
|
|
87
118
|
* Maintains the visible content position when the list updates.
|
|
88
119
|
* This is particularly useful for chat applications where we want to keep
|
|
89
120
|
* the user's current view position when new messages are added.
|
|
90
121
|
*/
|
|
91
|
-
const
|
|
122
|
+
const applyOffsetCorrection = useCallback(() => {
|
|
92
123
|
const { horizontal, data, keyExtractor } = recyclerViewManager.props;
|
|
93
|
-
// Resolve all pending scroll updates from previous calls
|
|
94
|
-
const resolves = pendingScrollResolves.current;
|
|
95
|
-
pendingScrollResolves.current = [];
|
|
96
|
-
resolves.forEach((resolve) => resolve());
|
|
97
124
|
|
|
98
|
-
|
|
125
|
+
// Execute all pending callbacks from previous scroll offset updates
|
|
126
|
+
// This ensures any scroll operations that were waiting for render are completed
|
|
127
|
+
const callbacks = pendingScrollCallbacks.current;
|
|
128
|
+
pendingScrollCallbacks.current = [];
|
|
129
|
+
callbacks.forEach((callback) => callback());
|
|
130
|
+
|
|
131
|
+
const currentDataLength = recyclerViewManager.getDataLength();
|
|
99
132
|
|
|
100
133
|
if (
|
|
101
134
|
recyclerViewManager.getIsFirstLayoutComplete() &&
|
|
@@ -152,8 +185,9 @@ export function useRecyclerViewController<T>(
|
|
|
152
185
|
scrollViewRef.current?.scrollTo(scrollToParams);
|
|
153
186
|
}
|
|
154
187
|
if (hasDataChanged) {
|
|
155
|
-
|
|
156
|
-
recyclerViewManager.getAbsoluteLastScrollOffset() + diff
|
|
188
|
+
updateScrollOffsetWithCallback(
|
|
189
|
+
recyclerViewManager.getAbsoluteLastScrollOffset() + diff,
|
|
190
|
+
() => {}
|
|
157
191
|
);
|
|
158
192
|
recyclerViewManager.ignoreScrollEvents = true;
|
|
159
193
|
setTimeout(() => {
|
|
@@ -164,28 +198,16 @@ export function useRecyclerViewController<T>(
|
|
|
164
198
|
}
|
|
165
199
|
}
|
|
166
200
|
|
|
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
|
-
}
|
|
201
|
+
computeFirstVisibleIndexForOffsetCorrection();
|
|
181
202
|
}
|
|
182
|
-
lastDataLengthRef.current =
|
|
203
|
+
lastDataLengthRef.current = recyclerViewManager.getDataLength();
|
|
183
204
|
}, [
|
|
184
205
|
recyclerViewManager,
|
|
185
206
|
scrollAnchorRef,
|
|
186
207
|
scrollViewRef,
|
|
187
208
|
setTimeout,
|
|
188
|
-
|
|
209
|
+
updateScrollOffsetWithCallback,
|
|
210
|
+
computeFirstVisibleIndexForOffsetCorrection,
|
|
189
211
|
]);
|
|
190
212
|
|
|
191
213
|
const handlerMethods: FlashListRef<T> = useMemo(() => {
|
|
@@ -278,144 +300,189 @@ export function useRecyclerViewController<T>(
|
|
|
278
300
|
/**
|
|
279
301
|
* Scrolls to a specific index in the list.
|
|
280
302
|
* Supports viewPosition and viewOffset for precise positioning.
|
|
303
|
+
* Returns a Promise that resolves when the scroll is complete.
|
|
281
304
|
*/
|
|
282
|
-
scrollToIndex:
|
|
305
|
+
scrollToIndex: ({
|
|
283
306
|
index,
|
|
284
307
|
animated,
|
|
285
308
|
viewPosition,
|
|
286
309
|
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
|
-
|
|
310
|
+
}: ScrollToIndexParams): Promise<void> => {
|
|
311
|
+
return new Promise((resolve) => {
|
|
312
|
+
const { horizontal } = recyclerViewManager.props;
|
|
313
|
+
if (
|
|
314
|
+
scrollViewRef.current &&
|
|
315
|
+
index >= 0 &&
|
|
316
|
+
index < recyclerViewManager.getDataLength()
|
|
317
|
+
) {
|
|
318
|
+
// Pause the scroll offset adjustments
|
|
319
|
+
pauseOffsetCorrection.current = true;
|
|
320
|
+
recyclerViewManager.setOffsetProjectionEnabled(false);
|
|
321
|
+
|
|
322
|
+
const getFinalOffset = () => {
|
|
323
|
+
const layout = recyclerViewManager.getLayout(index);
|
|
324
|
+
const offset = horizontal ? layout.x : layout.y;
|
|
325
|
+
let finalOffset = offset;
|
|
326
|
+
// take viewPosition etc into account
|
|
327
|
+
if (viewPosition !== undefined || viewOffset !== undefined) {
|
|
328
|
+
const containerSize = horizontal
|
|
329
|
+
? recyclerViewManager.getWindowSize().width
|
|
330
|
+
: recyclerViewManager.getWindowSize().height;
|
|
331
|
+
|
|
332
|
+
const itemSize = horizontal ? layout.width : layout.height;
|
|
333
|
+
|
|
334
|
+
if (viewPosition !== undefined) {
|
|
335
|
+
// viewPosition: 0 = top, 0.5 = center, 1 = bottom
|
|
336
|
+
finalOffset =
|
|
337
|
+
offset - (containerSize - itemSize) * viewPosition;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (viewOffset !== undefined) {
|
|
341
|
+
finalOffset += viewOffset;
|
|
342
|
+
}
|
|
314
343
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
344
|
+
return finalOffset + recyclerViewManager.firstItemOffset;
|
|
345
|
+
};
|
|
346
|
+
const lastAbsoluteScrollOffset =
|
|
347
|
+
recyclerViewManager.getAbsoluteLastScrollOffset();
|
|
348
|
+
const bufferForScroll = horizontal
|
|
349
|
+
? recyclerViewManager.getWindowSize().width
|
|
350
|
+
: recyclerViewManager.getWindowSize().height;
|
|
351
|
+
|
|
352
|
+
const bufferForCompute = bufferForScroll * 2;
|
|
353
|
+
|
|
354
|
+
const getStartScrollOffset = () => {
|
|
355
|
+
let lastScrollOffset = lastAbsoluteScrollOffset;
|
|
356
|
+
const finalOffset = getFinalOffset();
|
|
357
|
+
|
|
358
|
+
if (finalOffset > lastScrollOffset) {
|
|
359
|
+
lastScrollOffset = Math.max(
|
|
360
|
+
finalOffset - bufferForCompute,
|
|
361
|
+
lastScrollOffset
|
|
362
|
+
);
|
|
363
|
+
recyclerViewManager.setScrollDirection("forward");
|
|
364
|
+
} else {
|
|
365
|
+
lastScrollOffset = Math.min(
|
|
366
|
+
finalOffset + bufferForCompute,
|
|
367
|
+
lastScrollOffset
|
|
368
|
+
);
|
|
369
|
+
recyclerViewManager.setScrollDirection("backward");
|
|
370
|
+
}
|
|
371
|
+
return lastScrollOffset;
|
|
372
|
+
};
|
|
373
|
+
let initialTargetOffset = getFinalOffset();
|
|
374
|
+
let initialStartScrollOffset = getStartScrollOffset();
|
|
375
|
+
let finalOffset = initialTargetOffset;
|
|
376
|
+
let startScrollOffset = initialStartScrollOffset;
|
|
377
|
+
|
|
378
|
+
const steps = 5;
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Recursively performs the scroll animation steps.
|
|
382
|
+
* This function replaces the async/await loop with callback-based execution.
|
|
383
|
+
*
|
|
384
|
+
* @param currentStep - The current step in the animation (0 to steps-1)
|
|
385
|
+
*/
|
|
386
|
+
const performScrollStep = (currentStep: number) => {
|
|
387
|
+
// Check if component is unmounted or we've completed all steps
|
|
388
|
+
if (isUnmounted.current) {
|
|
389
|
+
return;
|
|
390
|
+
} else if (currentStep >= steps) {
|
|
391
|
+
// All steps completed, perform final scroll
|
|
392
|
+
finishScrollToIndex();
|
|
393
|
+
return;
|
|
318
394
|
}
|
|
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
395
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
396
|
+
// Calculate the offset for this step
|
|
397
|
+
// For animated scrolls: interpolate from finalOffset to startScrollOffset
|
|
398
|
+
// For non-animated: interpolate from startScrollOffset to finalOffset
|
|
399
|
+
const nextOffset = animated
|
|
400
|
+
? finalOffset +
|
|
401
|
+
(startScrollOffset - finalOffset) *
|
|
402
|
+
(currentStep / (steps - 1))
|
|
403
|
+
: startScrollOffset +
|
|
404
|
+
(finalOffset - startScrollOffset) *
|
|
405
|
+
(currentStep / (steps - 1));
|
|
406
|
+
|
|
407
|
+
// Update scroll offset with a callback to continue to the next step
|
|
408
|
+
updateScrollOffsetWithCallback(nextOffset, () => {
|
|
409
|
+
// Check if the index is still valid after the update
|
|
410
|
+
if (index >= recyclerViewManager.getDataLength()) {
|
|
411
|
+
// Index out of bounds, scroll to end instead
|
|
412
|
+
handlerMethods.scrollToEnd({ animated });
|
|
413
|
+
resolve(); // Resolve the promise as we're done
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Check if the target position has changed significantly
|
|
418
|
+
const newFinalOffset = getFinalOffset();
|
|
419
|
+
if (
|
|
420
|
+
(newFinalOffset < initialTargetOffset &&
|
|
421
|
+
newFinalOffset < initialStartScrollOffset) ||
|
|
422
|
+
(newFinalOffset > initialTargetOffset &&
|
|
423
|
+
newFinalOffset > initialStartScrollOffset)
|
|
424
|
+
) {
|
|
425
|
+
// Target has moved, recalculate and restart from beginning
|
|
426
|
+
finalOffset = newFinalOffset;
|
|
427
|
+
startScrollOffset = getStartScrollOffset();
|
|
428
|
+
initialTargetOffset = newFinalOffset;
|
|
429
|
+
initialStartScrollOffset = startScrollOffset;
|
|
430
|
+
performScrollStep(0); // Restart from step 0
|
|
431
|
+
} else {
|
|
432
|
+
// Continue to next step
|
|
433
|
+
performScrollStep(currentStep + 1);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Completes the scroll to index operation by performing the final scroll
|
|
440
|
+
* and re-enabling offset correction after a delay.
|
|
441
|
+
*/
|
|
442
|
+
const finishScrollToIndex = () => {
|
|
443
|
+
finalOffset = getFinalOffset();
|
|
444
|
+
const maxOffset = recyclerViewManager.getMaxScrollOffset();
|
|
445
|
+
|
|
446
|
+
if (finalOffset > maxOffset) {
|
|
447
|
+
finalOffset = maxOffset;
|
|
448
|
+
}
|
|
389
449
|
|
|
390
|
-
|
|
391
|
-
|
|
450
|
+
if (animated) {
|
|
451
|
+
// For animated scrolls, first jump to the start position
|
|
452
|
+
// We don't need to add firstItemOffset here as it's already added
|
|
453
|
+
handlerMethods.scrollToOffset({
|
|
454
|
+
offset: startScrollOffset,
|
|
455
|
+
animated: false,
|
|
456
|
+
skipFirstItemOffset: true,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
392
459
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
460
|
+
// Perform the final scroll to the target position
|
|
461
|
+
handlerMethods.scrollToOffset({
|
|
462
|
+
offset: finalOffset,
|
|
463
|
+
animated,
|
|
464
|
+
skipFirstItemOffset: true,
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Re-enable offset correction after a delay
|
|
468
|
+
// Longer delay for animated scrolls to allow animation to complete
|
|
469
|
+
setTimeout(
|
|
470
|
+
() => {
|
|
471
|
+
pauseOffsetCorrection.current = false;
|
|
472
|
+
recyclerViewManager.setOffsetProjectionEnabled(true);
|
|
473
|
+
resolve(); // Resolve the promise after re-enabling corrections
|
|
474
|
+
},
|
|
475
|
+
animated ? 300 : 200
|
|
476
|
+
);
|
|
477
|
+
};
|
|
396
478
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
skipFirstItemOffset: true,
|
|
403
|
-
});
|
|
479
|
+
// Start the scroll animation process
|
|
480
|
+
performScrollStep(0);
|
|
481
|
+
} else {
|
|
482
|
+
// Invalid parameters, resolve immediately
|
|
483
|
+
resolve();
|
|
404
484
|
}
|
|
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
|
-
}
|
|
485
|
+
});
|
|
419
486
|
},
|
|
420
487
|
|
|
421
488
|
/**
|
|
@@ -483,7 +550,7 @@ export function useRecyclerViewController<T>(
|
|
|
483
550
|
scrollViewRef,
|
|
484
551
|
setTimeout,
|
|
485
552
|
isUnmounted,
|
|
486
|
-
|
|
553
|
+
updateScrollOffsetWithCallback,
|
|
487
554
|
]);
|
|
488
555
|
|
|
489
556
|
const applyInitialScrollIndex = useCallback(() => {
|
|
@@ -534,5 +601,10 @@ export function useRecyclerViewController<T>(
|
|
|
534
601
|
[handlerMethods, scrollViewRef]
|
|
535
602
|
);
|
|
536
603
|
|
|
537
|
-
return {
|
|
604
|
+
return {
|
|
605
|
+
applyOffsetCorrection,
|
|
606
|
+
computeFirstVisibleIndexForOffsetCorrection,
|
|
607
|
+
applyInitialScrollIndex,
|
|
608
|
+
handlerMethods,
|
|
609
|
+
};
|
|
538
610
|
}
|
|
@@ -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]
|