@shopify/flash-list 2.0.0-alpha.9 → 2.0.0-rc.1
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 +37 -97
- 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 +15 -8
- package/dist/FlashListProps.d.ts.map +1 -1
- package/dist/FlashListProps.js.map +1 -1
- package/dist/FlashListRef.d.ts +305 -0
- package/dist/FlashListRef.d.ts.map +1 -0
- package/dist/FlashListRef.js +3 -0
- package/dist/FlashListRef.js.map +1 -0
- package/dist/MasonryFlashList.js.map +1 -1
- 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 +486 -0
- package/dist/__tests__/RenderStackManager.test.js.map +1 -0
- package/dist/__tests__/helpers/createLayoutManager.d.ts.map +1 -1
- package/dist/__tests__/helpers/createLayoutManager.js +3 -4
- package/dist/__tests__/helpers/createLayoutManager.js.map +1 -1
- 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/native/config/PlatformHelper.android.d.ts +1 -0
- package/dist/native/config/PlatformHelper.android.d.ts.map +1 -1
- package/dist/native/config/PlatformHelper.android.js +1 -0
- package/dist/native/config/PlatformHelper.android.js.map +1 -1
- package/dist/native/config/PlatformHelper.d.ts +1 -0
- package/dist/native/config/PlatformHelper.d.ts.map +1 -1
- package/dist/native/config/PlatformHelper.ios.d.ts +1 -0
- package/dist/native/config/PlatformHelper.ios.d.ts.map +1 -1
- package/dist/native/config/PlatformHelper.ios.js +1 -0
- package/dist/native/config/PlatformHelper.ios.js.map +1 -1
- package/dist/native/config/PlatformHelper.js +1 -0
- package/dist/native/config/PlatformHelper.js.map +1 -1
- package/dist/native/config/PlatformHelper.web.d.ts +1 -0
- package/dist/native/config/PlatformHelper.web.d.ts.map +1 -1
- package/dist/native/config/PlatformHelper.web.js +1 -0
- package/dist/native/config/PlatformHelper.web.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 +63 -45
- 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 +21 -7
- package/dist/recyclerview/RecyclerViewManager.d.ts.map +1 -1
- package/dist/recyclerview/RecyclerViewManager.js +105 -113
- 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 +324 -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 +23 -8
- package/dist/recyclerview/ViewHolderCollection.js.map +1 -1
- package/dist/recyclerview/components/ScrollAnchor.d.ts +2 -2
- package/dist/recyclerview/components/ScrollAnchor.d.ts.map +1 -1
- package/dist/recyclerview/components/ScrollAnchor.js +9 -5
- package/dist/recyclerview/components/ScrollAnchor.js.map +1 -1
- package/dist/recyclerview/components/StickyHeaders.d.ts +1 -1
- package/dist/recyclerview/components/StickyHeaders.d.ts.map +1 -1
- package/dist/recyclerview/components/StickyHeaders.js +39 -32
- package/dist/recyclerview/components/StickyHeaders.js.map +1 -1
- package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts +45 -1
- package/dist/recyclerview/helpers/EngagedIndicesTracker.d.ts.map +1 -1
- package/dist/recyclerview/helpers/EngagedIndicesTracker.js +77 -20
- package/dist/recyclerview/helpers/EngagedIndicesTracker.js.map +1 -1
- package/dist/recyclerview/helpers/RenderTimeTracker.d.ts +10 -0
- package/dist/recyclerview/helpers/RenderTimeTracker.d.ts.map +1 -0
- package/dist/recyclerview/helpers/RenderTimeTracker.js +39 -0
- package/dist/recyclerview/helpers/RenderTimeTracker.js.map +1 -0
- package/dist/recyclerview/helpers/VelocityTracker.d.ts +29 -0
- package/dist/recyclerview/helpers/VelocityTracker.d.ts.map +1 -0
- package/dist/recyclerview/helpers/VelocityTracker.js +70 -0
- package/dist/recyclerview/helpers/VelocityTracker.js.map +1 -0
- 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/useMappingHelper.d.ts +1 -1
- package/dist/recyclerview/hooks/useMappingHelper.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useMappingHelper.js +1 -1
- package/dist/recyclerview/hooks/useMappingHelper.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 +174 -123
- package/dist/recyclerview/hooks/useRecyclerViewController.js.map +1 -1
- package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts +2 -0
- package/dist/recyclerview/hooks/useRecyclerViewManager.d.ts.map +1 -1
- package/dist/recyclerview/hooks/useRecyclerViewManager.js +10 -1
- 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 +10 -16
- package/dist/recyclerview/layout-managers/LayoutManager.d.ts.map +1 -1
- package/dist/recyclerview/layout-managers/LayoutManager.js +4 -14
- package/dist/recyclerview/layout-managers/LayoutManager.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/viewability/ViewToken.d.ts +2 -2
- package/dist/viewability/ViewToken.d.ts.map +1 -1
- package/dist/viewability/ViewabilityHelper.js +1 -1
- package/dist/viewability/ViewabilityHelper.js.map +1 -1
- package/dist/viewability/ViewabilityManager.d.ts.map +1 -1
- package/dist/viewability/ViewabilityManager.js +1 -2
- package/dist/viewability/ViewabilityManager.js.map +1 -1
- package/jestSetup.js +30 -11
- package/package.json +2 -1
- package/src/AnimatedFlashList.ts +3 -2
- package/src/FlashList.tsx +24 -0
- package/src/FlashListProps.ts +20 -8
- package/src/FlashListRef.ts +320 -0
- package/src/MasonryFlashList.tsx +2 -2
- package/src/__tests__/RecyclerView.test.tsx +83 -29
- package/src/__tests__/RenderStackManager.test.ts +575 -0
- package/src/__tests__/helpers/createLayoutManager.ts +2 -3
- package/src/__tests__/useUnmountAwareCallbacks.test.tsx +12 -12
- package/src/benchmark/useFlatListBenchmark.ts +2 -2
- package/src/index.ts +1 -0
- package/src/native/config/PlatformHelper.android.ts +1 -0
- package/src/native/config/PlatformHelper.ios.ts +1 -0
- package/src/native/config/PlatformHelper.ts +1 -0
- package/src/native/config/PlatformHelper.web.ts +1 -0
- package/src/recyclerview/RecyclerView.tsx +82 -52
- package/src/recyclerview/RecyclerViewContextProvider.ts +12 -6
- package/src/recyclerview/RecyclerViewManager.ts +123 -98
- package/src/recyclerview/RenderStackManager.ts +291 -0
- package/src/recyclerview/ViewHolder.tsx +5 -3
- package/src/recyclerview/ViewHolderCollection.tsx +33 -12
- package/src/recyclerview/components/ScrollAnchor.tsx +21 -9
- package/src/recyclerview/components/StickyHeaders.tsx +62 -44
- package/src/recyclerview/helpers/EngagedIndicesTracker.ts +118 -23
- package/src/recyclerview/helpers/RenderTimeTracker.ts +38 -0
- package/src/recyclerview/helpers/VelocityTracker.ts +77 -0
- package/src/recyclerview/hooks/useBoundDetection.ts +25 -18
- package/src/recyclerview/hooks/useMappingHelper.ts +1 -1
- package/src/recyclerview/hooks/useOnLoad.ts +4 -6
- package/src/recyclerview/hooks/useRecyclerViewController.tsx +199 -176
- package/src/recyclerview/hooks/useRecyclerViewManager.ts +11 -1
- 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 +12 -21
- package/src/viewability/ViewToken.ts +2 -2
- package/src/viewability/ViewabilityHelper.ts +1 -1
- package/src/viewability/ViewabilityManager.ts +6 -3
- 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
|
-
const
|
|
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
|
-
pauseAdjustRef.current = false;
|
|
123
|
-
}, 100);
|
|
124
|
-
|
|
125
|
-
pauseAdjustRef.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,19 @@ 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 } = recyclerViewManager.props;
|
|
176
93
|
// Resolve all pending scroll updates from previous calls
|
|
177
94
|
const resolves = pendingScrollResolves.current;
|
|
178
95
|
pendingScrollResolves.current = [];
|
|
179
96
|
resolves.forEach((resolve) => resolve());
|
|
180
97
|
|
|
181
|
-
const currentDataLength =
|
|
98
|
+
const currentDataLength = data?.length ?? 0;
|
|
182
99
|
|
|
183
100
|
if (
|
|
184
|
-
!props.horizontal &&
|
|
185
101
|
recyclerViewManager.getIsFirstLayoutComplete() &&
|
|
186
|
-
|
|
102
|
+
keyExtractor &&
|
|
187
103
|
currentDataLength > 0 &&
|
|
188
|
-
|
|
104
|
+
recyclerViewManager.shouldMaintainVisibleContentPosition()
|
|
189
105
|
) {
|
|
190
106
|
const hasDataChanged = currentDataLength !== lastDataLengthRef.current;
|
|
191
107
|
// If we have a tracked first visible item, maintain its position
|
|
@@ -195,34 +111,45 @@ export function useRecyclerViewController<T>(
|
|
|
195
111
|
.getEngagedIndices()
|
|
196
112
|
.findValue(
|
|
197
113
|
(index) =>
|
|
198
|
-
|
|
114
|
+
keyExtractor?.(data![index], index) ===
|
|
199
115
|
firstVisibleItemKey.current
|
|
200
116
|
) ??
|
|
201
117
|
(hasDataChanged
|
|
202
|
-
?
|
|
118
|
+
? data?.findIndex(
|
|
203
119
|
(item, index) =>
|
|
204
|
-
|
|
205
|
-
firstVisibleItemKey.current
|
|
120
|
+
keyExtractor?.(item, index) === firstVisibleItemKey.current
|
|
206
121
|
)
|
|
207
122
|
: undefined);
|
|
208
123
|
|
|
209
|
-
if (
|
|
124
|
+
if (
|
|
125
|
+
currentIndexOfFirstVisibleItem !== undefined &&
|
|
126
|
+
currentIndexOfFirstVisibleItem >= 0
|
|
127
|
+
) {
|
|
210
128
|
// Calculate the difference in position and apply the offset
|
|
211
|
-
const diff =
|
|
212
|
-
recyclerViewManager.getLayout(currentIndexOfFirstVisibleItem).
|
|
213
|
-
|
|
129
|
+
const diff = horizontal
|
|
130
|
+
? recyclerViewManager.getLayout(currentIndexOfFirstVisibleItem).x -
|
|
131
|
+
firstVisibleItemLayout.current!.x
|
|
132
|
+
: recyclerViewManager.getLayout(currentIndexOfFirstVisibleItem).y -
|
|
133
|
+
firstVisibleItemLayout.current!.y;
|
|
214
134
|
firstVisibleItemLayout.current = {
|
|
215
135
|
...recyclerViewManager.getLayout(currentIndexOfFirstVisibleItem),
|
|
216
136
|
};
|
|
217
|
-
if (diff !== 0 && !
|
|
137
|
+
if (diff !== 0 && !pauseOffsetCorrection.current) {
|
|
218
138
|
// console.log("diff", diff, firstVisibleItemKey.current);
|
|
219
139
|
if (PlatformConfig.supportsOffsetCorrection) {
|
|
140
|
+
// console.log("scrollBy", diff);
|
|
220
141
|
scrollAnchorRef.current?.scrollBy(diff);
|
|
221
142
|
} else {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
143
|
+
const scrollToParams = horizontal
|
|
144
|
+
? {
|
|
145
|
+
x: recyclerViewManager.getAbsoluteLastScrollOffset() + diff,
|
|
146
|
+
animated: false,
|
|
147
|
+
}
|
|
148
|
+
: {
|
|
149
|
+
y: recyclerViewManager.getAbsoluteLastScrollOffset() + diff,
|
|
150
|
+
animated: false,
|
|
151
|
+
};
|
|
152
|
+
scrollViewRef.current?.scrollTo(scrollToParams);
|
|
226
153
|
}
|
|
227
154
|
if (hasDataChanged) {
|
|
228
155
|
updateScrollOffsetAsync(
|
|
@@ -240,11 +167,11 @@ export function useRecyclerViewController<T>(
|
|
|
240
167
|
// Update the tracked first visible item
|
|
241
168
|
const firstVisibleIndex = Math.max(
|
|
242
169
|
0,
|
|
243
|
-
recyclerViewManager.
|
|
170
|
+
recyclerViewManager.computeVisibleIndices().startIndex
|
|
244
171
|
);
|
|
245
172
|
if (firstVisibleIndex !== undefined && firstVisibleIndex >= 0) {
|
|
246
|
-
firstVisibleItemKey.current =
|
|
247
|
-
|
|
173
|
+
firstVisibleItemKey.current = keyExtractor(
|
|
174
|
+
data![firstVisibleIndex],
|
|
248
175
|
firstVisibleIndex
|
|
249
176
|
);
|
|
250
177
|
firstVisibleItemLayout.current = {
|
|
@@ -252,12 +179,20 @@ export function useRecyclerViewController<T>(
|
|
|
252
179
|
};
|
|
253
180
|
}
|
|
254
181
|
}
|
|
255
|
-
lastDataLengthRef.current =
|
|
256
|
-
}, [
|
|
257
|
-
|
|
258
|
-
|
|
182
|
+
lastDataLengthRef.current = data?.length ?? 0;
|
|
183
|
+
}, [
|
|
184
|
+
recyclerViewManager,
|
|
185
|
+
scrollAnchorRef,
|
|
186
|
+
scrollViewRef,
|
|
187
|
+
setTimeout,
|
|
188
|
+
updateScrollOffsetAsync,
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
const handlerMethods: FlashListRef<T> = useMemo(() => {
|
|
259
192
|
return {
|
|
260
|
-
props
|
|
193
|
+
get props() {
|
|
194
|
+
return recyclerViewManager.props;
|
|
195
|
+
},
|
|
261
196
|
/**
|
|
262
197
|
* Scrolls the list to a specific offset position.
|
|
263
198
|
* Handles RTL layouts and first item offset adjustments.
|
|
@@ -267,6 +202,7 @@ export function useRecyclerViewController<T>(
|
|
|
267
202
|
animated,
|
|
268
203
|
skipFirstItemOffset = true,
|
|
269
204
|
}: ScrollToOffsetParams) => {
|
|
205
|
+
const { horizontal } = recyclerViewManager.props;
|
|
270
206
|
if (scrollViewRef.current) {
|
|
271
207
|
// Adjust offset for RTL layouts in horizontal mode
|
|
272
208
|
if (I18nManager.isRTL && horizontal) {
|
|
@@ -295,6 +231,9 @@ export function useRecyclerViewController<T>(
|
|
|
295
231
|
});
|
|
296
232
|
}
|
|
297
233
|
},
|
|
234
|
+
clearLayoutCacheOnUpdate: () => {
|
|
235
|
+
recyclerViewManager.markLayoutManagerDirty();
|
|
236
|
+
},
|
|
298
237
|
|
|
299
238
|
// Expose native scroll view methods
|
|
300
239
|
flashScrollIndicators: () => {
|
|
@@ -314,13 +253,16 @@ export function useRecyclerViewController<T>(
|
|
|
314
253
|
* Scrolls to the end of the list.
|
|
315
254
|
*/
|
|
316
255
|
scrollToEnd: async ({ animated }: ScrollToEdgeParams = {}) => {
|
|
256
|
+
const { data } = recyclerViewManager.props;
|
|
317
257
|
if (data && data.length > 0) {
|
|
318
258
|
await handlerMethods.scrollToIndex({
|
|
319
259
|
index: data.length - 1,
|
|
320
260
|
animated,
|
|
321
261
|
});
|
|
322
262
|
}
|
|
323
|
-
|
|
263
|
+
setTimeout(() => {
|
|
264
|
+
scrollViewRef.current!.scrollToEnd({ animated });
|
|
265
|
+
}, 0);
|
|
324
266
|
},
|
|
325
267
|
|
|
326
268
|
/**
|
|
@@ -343,8 +285,15 @@ export function useRecyclerViewController<T>(
|
|
|
343
285
|
viewPosition,
|
|
344
286
|
viewOffset,
|
|
345
287
|
}: ScrollToIndexParams) => {
|
|
346
|
-
|
|
347
|
-
|
|
288
|
+
const { horizontal } = recyclerViewManager.props;
|
|
289
|
+
if (
|
|
290
|
+
scrollViewRef.current &&
|
|
291
|
+
index >= 0 &&
|
|
292
|
+
index < recyclerViewManager.getDataLength()
|
|
293
|
+
) {
|
|
294
|
+
// Pause the scroll offset adjustments
|
|
295
|
+
pauseOffsetCorrection.current = true;
|
|
296
|
+
recyclerViewManager.setOffsetProjectionEnabled(false);
|
|
348
297
|
|
|
349
298
|
const getFinalOffset = () => {
|
|
350
299
|
const layout = recyclerViewManager.getLayout(index);
|
|
@@ -368,74 +317,104 @@ export function useRecyclerViewController<T>(
|
|
|
368
317
|
finalOffset += viewOffset;
|
|
369
318
|
}
|
|
370
319
|
}
|
|
371
|
-
return finalOffset;
|
|
320
|
+
return finalOffset + recyclerViewManager.firstItemOffset;
|
|
372
321
|
};
|
|
373
|
-
|
|
374
|
-
|
|
322
|
+
const lastAbsoluteScrollOffset =
|
|
323
|
+
recyclerViewManager.getAbsoluteLastScrollOffset();
|
|
375
324
|
const bufferForScroll = horizontal
|
|
376
325
|
? recyclerViewManager.getWindowSize().width
|
|
377
326
|
: recyclerViewManager.getWindowSize().height;
|
|
378
327
|
|
|
379
328
|
const bufferForCompute = bufferForScroll * 2;
|
|
380
329
|
|
|
381
|
-
|
|
382
|
-
lastScrollOffset =
|
|
383
|
-
|
|
384
|
-
lastScrollOffset
|
|
385
|
-
);
|
|
386
|
-
recyclerViewManager.setScrollDirection("forward");
|
|
387
|
-
} else {
|
|
388
|
-
lastScrollOffset = Math.min(
|
|
389
|
-
finalOffset + bufferForCompute,
|
|
390
|
-
lastScrollOffset
|
|
391
|
-
);
|
|
392
|
-
recyclerViewManager.setScrollDirection("backward");
|
|
393
|
-
}
|
|
330
|
+
const getStartScrollOffset = () => {
|
|
331
|
+
let lastScrollOffset = lastAbsoluteScrollOffset;
|
|
332
|
+
const finalOffset = getFinalOffset();
|
|
394
333
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
await updateScrollOffsetAsync(
|
|
402
|
-
finalOffset + (lastScrollOffset - finalOffset) * (i / 4)
|
|
334
|
+
if (finalOffset > lastScrollOffset) {
|
|
335
|
+
lastScrollOffset = Math.max(
|
|
336
|
+
finalOffset - bufferForCompute,
|
|
337
|
+
lastScrollOffset
|
|
403
338
|
);
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
await updateScrollOffsetAsync(
|
|
412
|
-
lastScrollOffset + (finalOffset - lastScrollOffset) * (i / 4)
|
|
339
|
+
recyclerViewManager.setScrollDirection("forward");
|
|
340
|
+
} else {
|
|
341
|
+
lastScrollOffset = Math.min(
|
|
342
|
+
finalOffset + bufferForCompute,
|
|
343
|
+
lastScrollOffset
|
|
413
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
|
+
|
|
375
|
+
const newFinalOffset = getFinalOffset();
|
|
376
|
+
if (
|
|
377
|
+
(newFinalOffset < initialTargetOffset &&
|
|
378
|
+
newFinalOffset < initialStartScrollOffset) ||
|
|
379
|
+
(newFinalOffset > initialTargetOffset &&
|
|
380
|
+
newFinalOffset > initialStartScrollOffset)
|
|
381
|
+
) {
|
|
382
|
+
finalOffset = newFinalOffset;
|
|
383
|
+
startScrollOffset = getStartScrollOffset();
|
|
384
|
+
initialTargetOffset = newFinalOffset;
|
|
385
|
+
initialStartScrollOffset = startScrollOffset;
|
|
386
|
+
i = -1; // Restart compute loop
|
|
414
387
|
}
|
|
415
388
|
}
|
|
389
|
+
|
|
416
390
|
finalOffset = getFinalOffset();
|
|
417
391
|
const maxOffset = recyclerViewManager.getMaxScrollOffset();
|
|
418
392
|
|
|
419
393
|
if (finalOffset > maxOffset) {
|
|
420
394
|
finalOffset = maxOffset;
|
|
421
395
|
}
|
|
396
|
+
|
|
422
397
|
if (animated) {
|
|
423
|
-
// We don't need to add firstItemOffset here as it
|
|
398
|
+
// We don't need to add firstItemOffset here as it's already added
|
|
424
399
|
handlerMethods.scrollToOffset({
|
|
425
|
-
offset:
|
|
400
|
+
offset: startScrollOffset,
|
|
426
401
|
animated: false,
|
|
427
|
-
skipFirstItemOffset:
|
|
402
|
+
skipFirstItemOffset: true,
|
|
428
403
|
});
|
|
429
404
|
}
|
|
430
405
|
handlerMethods.scrollToOffset({
|
|
431
406
|
offset: finalOffset,
|
|
432
407
|
animated,
|
|
433
|
-
skipFirstItemOffset:
|
|
408
|
+
skipFirstItemOffset: true,
|
|
434
409
|
});
|
|
435
410
|
|
|
436
|
-
setTimeout(
|
|
437
|
-
|
|
438
|
-
|
|
411
|
+
setTimeout(
|
|
412
|
+
() => {
|
|
413
|
+
pauseOffsetCorrection.current = false;
|
|
414
|
+
recyclerViewManager.setOffsetProjectionEnabled(true);
|
|
415
|
+
},
|
|
416
|
+
animated ? 300 : 200
|
|
417
|
+
);
|
|
439
418
|
}
|
|
440
419
|
},
|
|
441
420
|
|
|
@@ -449,11 +428,10 @@ export function useRecyclerViewController<T>(
|
|
|
449
428
|
viewPosition,
|
|
450
429
|
viewOffset,
|
|
451
430
|
}: ScrollToItemParams<T>) => {
|
|
431
|
+
const { data } = recyclerViewManager.props;
|
|
452
432
|
if (scrollViewRef.current && data) {
|
|
453
433
|
// Find the index of the item in the data array
|
|
454
|
-
const index =
|
|
455
|
-
(dataItem) => dataItem === item
|
|
456
|
-
);
|
|
434
|
+
const index = data.findIndex((dataItem) => dataItem === item);
|
|
457
435
|
if (index >= 0) {
|
|
458
436
|
handlerMethods.scrollToIndex({
|
|
459
437
|
index,
|
|
@@ -473,7 +451,7 @@ export function useRecyclerViewController<T>(
|
|
|
473
451
|
return recyclerViewManager.getWindowSize();
|
|
474
452
|
},
|
|
475
453
|
getLayout: (index: number) => {
|
|
476
|
-
return recyclerViewManager.
|
|
454
|
+
return recyclerViewManager.tryGetLayout(index);
|
|
477
455
|
},
|
|
478
456
|
getAbsoluteLastScrollOffset: () => {
|
|
479
457
|
return recyclerViewManager.getAbsoluteLastScrollOffset();
|
|
@@ -484,11 +462,11 @@ export function useRecyclerViewController<T>(
|
|
|
484
462
|
recordInteraction: () => {
|
|
485
463
|
recyclerViewManager.recordInteraction();
|
|
486
464
|
},
|
|
487
|
-
|
|
488
|
-
return recyclerViewManager.
|
|
465
|
+
computeVisibleIndices: () => {
|
|
466
|
+
return recyclerViewManager.computeVisibleIndices();
|
|
489
467
|
},
|
|
490
468
|
getFirstVisibleIndex: () => {
|
|
491
|
-
return recyclerViewManager.
|
|
469
|
+
return recyclerViewManager.computeVisibleIndices().startIndex;
|
|
492
470
|
},
|
|
493
471
|
recomputeViewableItems: () => {
|
|
494
472
|
recyclerViewManager.recomputeViewableItems();
|
|
@@ -497,10 +475,55 @@ export function useRecyclerViewController<T>(
|
|
|
497
475
|
* Disables item recycling in preparation for layout animations.
|
|
498
476
|
*/
|
|
499
477
|
prepareForLayoutAnimationRender: () => {
|
|
500
|
-
recyclerViewManager.disableRecycling
|
|
478
|
+
recyclerViewManager.disableRecycling(true);
|
|
501
479
|
},
|
|
502
480
|
};
|
|
503
|
-
}, [
|
|
481
|
+
}, [
|
|
482
|
+
recyclerViewManager,
|
|
483
|
+
scrollViewRef,
|
|
484
|
+
setTimeout,
|
|
485
|
+
isUnmounted,
|
|
486
|
+
updateScrollOffsetAsync,
|
|
487
|
+
]);
|
|
488
|
+
|
|
489
|
+
const applyInitialScrollIndex = useCallback(() => {
|
|
490
|
+
const { horizontal, data } = recyclerViewManager.props;
|
|
491
|
+
|
|
492
|
+
const initialScrollIndex =
|
|
493
|
+
recyclerViewManager.getInitialScrollIndex() ?? -1;
|
|
494
|
+
const dataLength = data?.length ?? 0;
|
|
495
|
+
if (
|
|
496
|
+
initialScrollIndex >= 0 &&
|
|
497
|
+
initialScrollIndex < dataLength &&
|
|
498
|
+
!initialScrollCompletedRef.current &&
|
|
499
|
+
recyclerViewManager.getIsFirstLayoutComplete()
|
|
500
|
+
) {
|
|
501
|
+
// Use setTimeout to ensure that we keep trying to scroll on first few renders
|
|
502
|
+
setTimeout(() => {
|
|
503
|
+
initialScrollCompletedRef.current = true;
|
|
504
|
+
pauseOffsetCorrection.current = false;
|
|
505
|
+
}, 100);
|
|
506
|
+
|
|
507
|
+
pauseOffsetCorrection.current = true;
|
|
508
|
+
|
|
509
|
+
const offset = horizontal
|
|
510
|
+
? recyclerViewManager.getLayout(initialScrollIndex).x
|
|
511
|
+
: recyclerViewManager.getLayout(initialScrollIndex).y;
|
|
512
|
+
handlerMethods.scrollToOffset({
|
|
513
|
+
offset,
|
|
514
|
+
animated: false,
|
|
515
|
+
skipFirstItemOffset: false,
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
setTimeout(() => {
|
|
519
|
+
handlerMethods.scrollToOffset({
|
|
520
|
+
offset,
|
|
521
|
+
animated: false,
|
|
522
|
+
skipFirstItemOffset: false,
|
|
523
|
+
});
|
|
524
|
+
}, 0);
|
|
525
|
+
}
|
|
526
|
+
}, [handlerMethods, recyclerViewManager, setTimeout]);
|
|
504
527
|
|
|
505
528
|
// Expose imperative methods through the ref
|
|
506
529
|
useImperativeHandle(
|
|
@@ -508,8 +531,8 @@ export function useRecyclerViewController<T>(
|
|
|
508
531
|
() => {
|
|
509
532
|
return { ...scrollViewRef.current, ...handlerMethods };
|
|
510
533
|
},
|
|
511
|
-
[handlerMethods]
|
|
534
|
+
[handlerMethods, scrollViewRef]
|
|
512
535
|
);
|
|
513
536
|
|
|
514
|
-
return { applyContentOffset, applyInitialScrollIndex };
|
|
537
|
+
return { applyContentOffset, applyInitialScrollIndex, handlerMethods };
|
|
515
538
|
}
|
|
@@ -2,15 +2,20 @@ import { useEffect, useMemo, useState } from "react";
|
|
|
2
2
|
|
|
3
3
|
import { RecyclerViewProps } from "../RecyclerViewProps";
|
|
4
4
|
import { RecyclerViewManager } from "../RecyclerViewManager";
|
|
5
|
+
import { VelocityTracker } from "../helpers/VelocityTracker";
|
|
5
6
|
|
|
6
7
|
export const useRecyclerViewManager = <T>(props: RecyclerViewProps<T>) => {
|
|
7
8
|
const [recyclerViewManager] = useState<RecyclerViewManager<T>>(
|
|
8
9
|
() => new RecyclerViewManager(props)
|
|
9
10
|
);
|
|
11
|
+
const [velocityTracker] = useState(() => new VelocityTracker());
|
|
12
|
+
|
|
10
13
|
const { data } = props;
|
|
11
14
|
|
|
12
15
|
useMemo(() => {
|
|
13
16
|
recyclerViewManager.updateProps(props);
|
|
17
|
+
// used to update props so rule can be disabled
|
|
18
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
14
19
|
}, [props]);
|
|
15
20
|
|
|
16
21
|
/**
|
|
@@ -18,13 +23,18 @@ export const useRecyclerViewManager = <T>(props: RecyclerViewProps<T>) => {
|
|
|
18
23
|
*/
|
|
19
24
|
useMemo(() => {
|
|
20
25
|
recyclerViewManager.processDataUpdate();
|
|
26
|
+
// used to process data update so rule can be disabled
|
|
27
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
21
28
|
}, [data]);
|
|
22
29
|
|
|
23
30
|
useEffect(() => {
|
|
24
31
|
return () => {
|
|
25
32
|
recyclerViewManager.dispose();
|
|
33
|
+
velocityTracker.cleanUp();
|
|
26
34
|
};
|
|
35
|
+
// needs to run only on unmount
|
|
36
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
27
37
|
}, []);
|
|
28
38
|
|
|
29
|
-
return { recyclerViewManager };
|
|
39
|
+
return { recyclerViewManager, velocityTracker };
|
|
30
40
|
};
|
|
@@ -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
|
+
}
|