@momo-kits/carousel 0.165.2-beta.1 → 0.165.2-beta.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/core/LICENSE +21 -0
- package/core/README.md +16 -0
- package/core/components/Carousel.tsx +28 -0
- package/core/components/CarouselLayout.tsx +231 -0
- package/core/components/ItemLayout.tsx +215 -0
- package/core/components/ItemRenderer.tsx +126 -0
- package/core/components/LazyView.tsx +14 -0
- package/core/components/ScrollViewGesture.tsx +443 -0
- package/core/constants/index.ts +11 -0
- package/core/hooks/useAutoPlay.ts +63 -0
- package/core/hooks/useCarouselController.tsx +295 -0
- package/core/hooks/useCheckMounted.ts +14 -0
- package/core/hooks/useCommonVariables.ts +261 -0
- package/core/hooks/useInitProps.ts +177 -0
- package/core/hooks/useLayoutConfig.ts +35 -0
- package/core/hooks/useOffsetX.ts +81 -0
- package/core/hooks/useOnProgressChange.ts +33 -0
- package/core/hooks/usePanGestureProxy.ts +144 -0
- package/core/hooks/usePropsErrorBoundary.ts +41 -0
- package/core/hooks/useUpdateGestureConfig.ts +17 -0
- package/core/hooks/useVisibleRanges.tsx +141 -0
- package/core/layouts/index.tsx +10 -0
- package/core/layouts/normal.ts +32 -0
- package/core/layouts/parallax.ts +50 -0
- package/core/layouts/stack.ts +266 -0
- package/core/public-types.ts +209 -0
- package/core/store/index.tsx +63 -0
- package/core/types.ts +32 -0
- package/core/utils/carousel-direction.ts +39 -0
- package/core/utils/carousel-math.ts +156 -0
- package/core/utils/compute-gesture-translation.ts +29 -0
- package/core/utils/compute-offset-if-size-changed.ts +14 -0
- package/core/utils/computed-with-auto-fill-data.ts +92 -0
- package/core/utils/deal-with-animation.ts +19 -0
- package/core/utils/log.ts +12 -0
- package/core/utils/sanitize-animation-style.ts +20 -0
- package/index.tsx +390 -455
- package/package.json +6 -5
- package/types.ts +123 -48
- package/animation.ts +0 -62
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import React, { useRef } from "react";
|
|
2
|
+
import type { SharedValue } from "react-native-reanimated";
|
|
3
|
+
import { useAnimatedReaction, useSharedValue } from "react-native-reanimated";
|
|
4
|
+
import { scheduleOnRN } from "react-native-worklets";
|
|
5
|
+
|
|
6
|
+
import { useGlobalState } from "../store";
|
|
7
|
+
import type {
|
|
8
|
+
CarouselAnimation,
|
|
9
|
+
CarouselRef,
|
|
10
|
+
InternalCarouselScrollToOptions,
|
|
11
|
+
InternalCarouselStepOptions,
|
|
12
|
+
} from "../types";
|
|
13
|
+
import {
|
|
14
|
+
getLogicalProgress,
|
|
15
|
+
getNearestLogicalPage,
|
|
16
|
+
getSettledRawIndex,
|
|
17
|
+
getShortestLoopTargetPage,
|
|
18
|
+
positiveModulo,
|
|
19
|
+
} from "../utils/carousel-math";
|
|
20
|
+
import { dealWithAnimation } from "../utils/deal-with-animation";
|
|
21
|
+
|
|
22
|
+
interface CarouselControllerOptions {
|
|
23
|
+
ref: React.ForwardedRef<CarouselRef>;
|
|
24
|
+
loop: boolean;
|
|
25
|
+
size: number;
|
|
26
|
+
dataLength: number;
|
|
27
|
+
rawDataLength: number;
|
|
28
|
+
handlerOffset: SharedValue<number>;
|
|
29
|
+
animation: CarouselAnimation;
|
|
30
|
+
defaultIndex?: number | undefined;
|
|
31
|
+
onScrollStart?: (() => void) | undefined;
|
|
32
|
+
onMovementEnd?: ((index: number) => void) | undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CarouselController {
|
|
36
|
+
getSharedIndex: () => number;
|
|
37
|
+
prev: (options?: InternalCarouselStepOptions) => void;
|
|
38
|
+
next: (options?: InternalCarouselStepOptions) => void;
|
|
39
|
+
getCurrentIndex: () => number;
|
|
40
|
+
scrollTo: (options: InternalCarouselScrollToOptions) => void;
|
|
41
|
+
index: SharedValue<number>;
|
|
42
|
+
startMovement: () => void;
|
|
43
|
+
cancelMovement: () => void;
|
|
44
|
+
settle: () => number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function useCarouselController(options: CarouselControllerOptions): CarouselController {
|
|
48
|
+
const {
|
|
49
|
+
animation,
|
|
50
|
+
dataLength,
|
|
51
|
+
defaultIndex = 0,
|
|
52
|
+
handlerOffset,
|
|
53
|
+
loop,
|
|
54
|
+
rawDataLength,
|
|
55
|
+
ref,
|
|
56
|
+
size,
|
|
57
|
+
} = options;
|
|
58
|
+
const {
|
|
59
|
+
common: {
|
|
60
|
+
cancelMovement: cancelCommonMovement,
|
|
61
|
+
dataRevision,
|
|
62
|
+
isMoving,
|
|
63
|
+
resolvedSize,
|
|
64
|
+
settleMovement,
|
|
65
|
+
sizePhase,
|
|
66
|
+
startMovement: startCommonMovement,
|
|
67
|
+
},
|
|
68
|
+
} = useGlobalState();
|
|
69
|
+
|
|
70
|
+
const index = useSharedValue(defaultIndex);
|
|
71
|
+
const liveRawIndex = useRef(rawDataLength > 0 ? defaultIndex : 0);
|
|
72
|
+
const settledIndex = useRef(rawDataLength > 0 ? defaultIndex : 0);
|
|
73
|
+
const warnedCommands = useRef(new Set<string>());
|
|
74
|
+
const canMove = rawDataLength > 1;
|
|
75
|
+
|
|
76
|
+
const warnOnce = React.useCallback((key: string, message: string) => {
|
|
77
|
+
if (!__DEV__ || warnedCommands.current.has(key)) return;
|
|
78
|
+
warnedCommands.current.add(key);
|
|
79
|
+
console.warn(`[react-native-reanimated-carousel] ${message}`);
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
const currentVisualPage = React.useCallback(() => {
|
|
83
|
+
if (size <= 0 || rawDataLength <= 0) return 0;
|
|
84
|
+
|
|
85
|
+
const page = getNearestLogicalPage(handlerOffset.value, size);
|
|
86
|
+
return loop ? page : Math.max(0, Math.min(rawDataLength - 1, page));
|
|
87
|
+
}, [handlerOffset, loop, rawDataLength, size]);
|
|
88
|
+
|
|
89
|
+
function setLiveRawIndex(nextIndex: number) {
|
|
90
|
+
liveRawIndex.current = nextIndex;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
useAnimatedReaction(
|
|
94
|
+
() => {
|
|
95
|
+
if (size <= 0 || dataLength <= 0 || rawDataLength <= 0) {
|
|
96
|
+
return { physicalIndex: 0, rawIndex: 0 };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const page = getNearestLogicalPage(handlerOffset.value, size);
|
|
100
|
+
return {
|
|
101
|
+
physicalIndex: positiveModulo(page, dataLength),
|
|
102
|
+
rawIndex: positiveModulo(page, rawDataLength),
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
({ physicalIndex, rawIndex }) => {
|
|
106
|
+
index.value = physicalIndex;
|
|
107
|
+
scheduleOnRN(setLiveRawIndex, rawIndex);
|
|
108
|
+
},
|
|
109
|
+
[dataLength, handlerOffset, index, rawDataLength, size]
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const settle = React.useCallback(
|
|
113
|
+
(targetIndex?: number) => {
|
|
114
|
+
settleMovement();
|
|
115
|
+
|
|
116
|
+
const nextSettledIndex =
|
|
117
|
+
typeof targetIndex === "number" && rawDataLength > 0
|
|
118
|
+
? positiveModulo(Math.round(targetIndex), rawDataLength)
|
|
119
|
+
: getSettledRawIndex(
|
|
120
|
+
loop
|
|
121
|
+
? getLogicalProgress(handlerOffset.value, size)
|
|
122
|
+
: Math.max(
|
|
123
|
+
0,
|
|
124
|
+
Math.min(rawDataLength - 1, getLogicalProgress(handlerOffset.value, size))
|
|
125
|
+
),
|
|
126
|
+
rawDataLength
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
settledIndex.current = nextSettledIndex;
|
|
130
|
+
return nextSettledIndex;
|
|
131
|
+
},
|
|
132
|
+
[handlerOffset, loop, rawDataLength, settleMovement, size]
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const getCurrentIndex = React.useCallback(() => settledIndex.current, []);
|
|
136
|
+
const getSharedIndex = React.useCallback(() => liveRawIndex.current, []);
|
|
137
|
+
const startMovement = React.useCallback(() => startCommonMovement(), [startCommonMovement]);
|
|
138
|
+
const cancelMovement = React.useCallback(() => cancelCommonMovement(), [cancelCommonMovement]);
|
|
139
|
+
|
|
140
|
+
React.useEffect(() => {
|
|
141
|
+
if (!isMoving.value) settle();
|
|
142
|
+
}, [dataRevision, isMoving, rawDataLength, settle]);
|
|
143
|
+
|
|
144
|
+
const isReady = React.useCallback(() => {
|
|
145
|
+
const currentSize = resolvedSize.value ?? size;
|
|
146
|
+
return sizePhase.value === "ready" && currentSize > 0;
|
|
147
|
+
}, [resolvedSize, size, sizePhase]);
|
|
148
|
+
|
|
149
|
+
const emitStart = React.useCallback(() => {
|
|
150
|
+
startMovement();
|
|
151
|
+
options.onScrollStart?.();
|
|
152
|
+
}, [options, startMovement]);
|
|
153
|
+
|
|
154
|
+
const emitSettle = React.useCallback(
|
|
155
|
+
(targetIndex?: number) => {
|
|
156
|
+
const nextSettledIndex = settle(targetIndex);
|
|
157
|
+
options.onMovementEnd?.(nextSettledIndex);
|
|
158
|
+
},
|
|
159
|
+
[options, settle]
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const animateTo = React.useCallback(
|
|
163
|
+
(targetOffset: number, targetIndex: number, onFinished?: () => void) => {
|
|
164
|
+
"worklet";
|
|
165
|
+
return dealWithAnimation(animation)(targetOffset, (finished) => {
|
|
166
|
+
"worklet";
|
|
167
|
+
if (finished) {
|
|
168
|
+
scheduleOnRN(emitSettle, targetIndex);
|
|
169
|
+
if (onFinished) scheduleOnRN(onFinished);
|
|
170
|
+
} else {
|
|
171
|
+
scheduleOnRN(cancelMovement);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
[animation, cancelMovement, emitSettle]
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const moveToPage = React.useCallback(
|
|
179
|
+
(targetPage: number, animated: boolean, onFinished?: () => void) => {
|
|
180
|
+
"worklet";
|
|
181
|
+
const targetRawIndex = positiveModulo(targetPage, rawDataLength);
|
|
182
|
+
emitStart();
|
|
183
|
+
index.value = positiveModulo(targetPage, Math.max(dataLength, 1));
|
|
184
|
+
|
|
185
|
+
const targetOffset = -targetPage * size;
|
|
186
|
+
if (animated) {
|
|
187
|
+
handlerOffset.value = animateTo(targetOffset, targetRawIndex, onFinished);
|
|
188
|
+
} else {
|
|
189
|
+
handlerOffset.value = targetOffset;
|
|
190
|
+
emitSettle(targetRawIndex);
|
|
191
|
+
onFinished?.();
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
[animateTo, dataLength, emitSettle, emitStart, handlerOffset, index, rawDataLength, size]
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const validateCount = React.useCallback(
|
|
198
|
+
(command: "next" | "prev", count: number) => {
|
|
199
|
+
if (count === 0) return false;
|
|
200
|
+
if (Number.isInteger(count) && count > 0) return true;
|
|
201
|
+
|
|
202
|
+
warnOnce(
|
|
203
|
+
`${command}:count`,
|
|
204
|
+
`${command} count must be a positive integer; received ${String(count)}.`
|
|
205
|
+
);
|
|
206
|
+
return false;
|
|
207
|
+
},
|
|
208
|
+
[warnOnce]
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const next = React.useCallback(
|
|
212
|
+
(stepOptions: InternalCarouselStepOptions = {}) => {
|
|
213
|
+
"worklet";
|
|
214
|
+
const { animated = true, count = 1, onFinished } = stepOptions;
|
|
215
|
+
if (!canMove || !isReady() || !validateCount("next", count)) return;
|
|
216
|
+
|
|
217
|
+
const currentPage = currentVisualPage();
|
|
218
|
+
const targetPage = loop
|
|
219
|
+
? currentPage + count
|
|
220
|
+
: Math.min(rawDataLength - 1, currentPage + count);
|
|
221
|
+
if (targetPage === currentPage) return;
|
|
222
|
+
|
|
223
|
+
moveToPage(targetPage, animated, onFinished);
|
|
224
|
+
},
|
|
225
|
+
[canMove, currentVisualPage, isReady, loop, moveToPage, rawDataLength, validateCount]
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const prev = React.useCallback(
|
|
229
|
+
(stepOptions: InternalCarouselStepOptions = {}) => {
|
|
230
|
+
"worklet";
|
|
231
|
+
const { animated = true, count = 1, onFinished } = stepOptions;
|
|
232
|
+
if (!canMove || !isReady() || !validateCount("prev", count)) return;
|
|
233
|
+
|
|
234
|
+
const currentPage = currentVisualPage();
|
|
235
|
+
const targetPage = loop ? currentPage - count : Math.max(0, currentPage - count);
|
|
236
|
+
if (targetPage === currentPage) return;
|
|
237
|
+
|
|
238
|
+
moveToPage(targetPage, animated, onFinished);
|
|
239
|
+
},
|
|
240
|
+
[canMove, currentVisualPage, isReady, loop, moveToPage, validateCount]
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const scrollTo = React.useCallback(
|
|
244
|
+
(scrollOptions: InternalCarouselScrollToOptions) => {
|
|
245
|
+
"worklet";
|
|
246
|
+
const { animated = true, index: targetIndex, onFinished } = scrollOptions;
|
|
247
|
+
if (!canMove || !isReady()) return;
|
|
248
|
+
|
|
249
|
+
if (!Number.isInteger(targetIndex) || targetIndex < 0 || targetIndex >= rawDataLength) {
|
|
250
|
+
warnOnce(
|
|
251
|
+
"scrollTo:index",
|
|
252
|
+
`scrollTo index must be an integer between 0 and ${rawDataLength - 1}; received ${String(targetIndex)}.`
|
|
253
|
+
);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const currentPage = currentVisualPage();
|
|
258
|
+
const currentRawIndex = positiveModulo(currentPage, rawDataLength);
|
|
259
|
+
if (currentRawIndex === targetIndex) return;
|
|
260
|
+
|
|
261
|
+
const targetPage = loop
|
|
262
|
+
? getShortestLoopTargetPage({
|
|
263
|
+
currentPage,
|
|
264
|
+
targetIndex,
|
|
265
|
+
count: rawDataLength,
|
|
266
|
+
})
|
|
267
|
+
: targetIndex;
|
|
268
|
+
moveToPage(targetPage, animated, onFinished);
|
|
269
|
+
},
|
|
270
|
+
[canMove, currentVisualPage, isReady, loop, moveToPage, rawDataLength, warnOnce]
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
React.useImperativeHandle(
|
|
274
|
+
ref,
|
|
275
|
+
() => ({
|
|
276
|
+
next,
|
|
277
|
+
prev,
|
|
278
|
+
getCurrentIndex,
|
|
279
|
+
scrollTo,
|
|
280
|
+
}),
|
|
281
|
+
[getCurrentIndex, next, prev, scrollTo]
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
next,
|
|
286
|
+
prev,
|
|
287
|
+
scrollTo,
|
|
288
|
+
getCurrentIndex,
|
|
289
|
+
getSharedIndex,
|
|
290
|
+
index,
|
|
291
|
+
startMovement,
|
|
292
|
+
cancelMovement,
|
|
293
|
+
settle,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { StyleSheet } from "react-native";
|
|
3
|
+
import type { SharedValue } from "react-native-reanimated";
|
|
4
|
+
import { useAnimatedReaction, useSharedValue } from "react-native-reanimated";
|
|
5
|
+
import { scheduleOnRN } from "react-native-worklets";
|
|
6
|
+
|
|
7
|
+
import type { InitializedCarouselProps } from "./useInitProps";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
getNearestLogicalPage,
|
|
11
|
+
positiveModulo,
|
|
12
|
+
reconcileOffsetAfterDataChange,
|
|
13
|
+
} from "../utils/carousel-math";
|
|
14
|
+
import { computeOffsetIfSizeChanged } from "../utils/compute-offset-if-size-changed";
|
|
15
|
+
|
|
16
|
+
export type CarouselSizePhase = "pending" | "ready" | "updating";
|
|
17
|
+
|
|
18
|
+
export interface CarouselCommonVariables {
|
|
19
|
+
size: number;
|
|
20
|
+
validLength: number;
|
|
21
|
+
handlerOffset: SharedValue<number>;
|
|
22
|
+
resolvedSize: SharedValue<number | null>;
|
|
23
|
+
sizePhase: SharedValue<CarouselSizePhase>;
|
|
24
|
+
sizeExplicit: boolean;
|
|
25
|
+
isMoving: SharedValue<boolean>;
|
|
26
|
+
dataRevision: number;
|
|
27
|
+
startMovement: () => void;
|
|
28
|
+
cancelMovement: () => void;
|
|
29
|
+
settleMovement: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function useCommonVariables(
|
|
33
|
+
props: InitializedCarouselProps<unknown>
|
|
34
|
+
): CarouselCommonVariables {
|
|
35
|
+
const {
|
|
36
|
+
orientation,
|
|
37
|
+
style,
|
|
38
|
+
dataLength,
|
|
39
|
+
defaultIndex,
|
|
40
|
+
keyExtractor,
|
|
41
|
+
loop,
|
|
42
|
+
itemSize,
|
|
43
|
+
rawData,
|
|
44
|
+
rawDataLength,
|
|
45
|
+
} = props;
|
|
46
|
+
const isVertical = orientation === "vertical";
|
|
47
|
+
|
|
48
|
+
const manualSize = React.useMemo(() => {
|
|
49
|
+
if (typeof itemSize === "number" && itemSize > 0) {
|
|
50
|
+
return itemSize;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { width, height } = StyleSheet.flatten(style) || {};
|
|
54
|
+
const candidate = isVertical ? height : width;
|
|
55
|
+
if (typeof candidate === "number" && candidate > 0) {
|
|
56
|
+
return candidate;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return null;
|
|
60
|
+
}, [isVertical, itemSize, style]);
|
|
61
|
+
|
|
62
|
+
const resolvedSize = useSharedValue<number | null>(manualSize);
|
|
63
|
+
const sizePhase = useSharedValue<CarouselSizePhase>(manualSize ? "ready" : "pending");
|
|
64
|
+
|
|
65
|
+
const defaultHandlerOffsetValue =
|
|
66
|
+
manualSize && dataLength > 0 ? -Math.abs(defaultIndex * manualSize) : 0;
|
|
67
|
+
const _handlerOffset = useSharedValue<number>(defaultHandlerOffsetValue);
|
|
68
|
+
const handlerOffset = props.scrollOffsetValue ?? _handlerOffset;
|
|
69
|
+
const prevDataLength = useSharedValue(rawDataLength);
|
|
70
|
+
const previousRawData = React.useRef(rawData);
|
|
71
|
+
const previousKeyExtractor = React.useRef(keyExtractor);
|
|
72
|
+
const isMoving = useSharedValue(false);
|
|
73
|
+
const pendingDataChange = useSharedValue<{
|
|
74
|
+
previousLength: number;
|
|
75
|
+
currentLength: number;
|
|
76
|
+
retainedIndex: number | null;
|
|
77
|
+
} | null>(null);
|
|
78
|
+
// Start at zero even when a synchronous item size is available. The first
|
|
79
|
+
// size reaction owns initialization and must write the default-index offset
|
|
80
|
+
// into consumer-provided SharedValues as well as the internal value.
|
|
81
|
+
const prevSize = useSharedValue(0);
|
|
82
|
+
const hasInitializedOffset = useSharedValue(false);
|
|
83
|
+
const sizeExplicit = typeof itemSize === "number" && itemSize > 0;
|
|
84
|
+
|
|
85
|
+
const [size, setSize] = React.useState<number>(manualSize ?? 0);
|
|
86
|
+
const [dataRevision, setDataRevision] = React.useState(0);
|
|
87
|
+
|
|
88
|
+
const syncSize = React.useCallback((next: number) => {
|
|
89
|
+
setSize((current) => (current === next ? current : next));
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
useAnimatedReaction(
|
|
93
|
+
() => resolvedSize.value,
|
|
94
|
+
(current, previous) => {
|
|
95
|
+
if (current == null || current <= 0) return;
|
|
96
|
+
if (current !== previous) {
|
|
97
|
+
scheduleOnRN(syncSize, current);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
[syncSize]
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
React.useEffect(() => {
|
|
104
|
+
if (manualSize && manualSize > 0) {
|
|
105
|
+
resolvedSize.value = manualSize;
|
|
106
|
+
sizePhase.value = "ready";
|
|
107
|
+
}
|
|
108
|
+
}, [manualSize, resolvedSize, sizePhase]);
|
|
109
|
+
|
|
110
|
+
React.useEffect(() => {
|
|
111
|
+
const currentSize = resolvedSize.value ?? 0;
|
|
112
|
+
if (currentSize <= 0 || dataLength <= 0 || hasInitializedOffset.value) return;
|
|
113
|
+
|
|
114
|
+
handlerOffset.value = -(defaultIndex * currentSize);
|
|
115
|
+
prevSize.value = currentSize;
|
|
116
|
+
hasInitializedOffset.value = true;
|
|
117
|
+
}, [dataLength, defaultIndex, handlerOffset, hasInitializedOffset, prevSize, resolvedSize]);
|
|
118
|
+
|
|
119
|
+
const reconcileDataChange = React.useCallback(
|
|
120
|
+
(previousLength: number, currentLength: number, retainedIndex?: number) => {
|
|
121
|
+
const currentSize = resolvedSize.value ?? 0;
|
|
122
|
+
if (currentSize <= 0) return;
|
|
123
|
+
|
|
124
|
+
handlerOffset.value = reconcileOffsetAfterDataChange({
|
|
125
|
+
offset: handlerOffset.value,
|
|
126
|
+
itemSize: currentSize,
|
|
127
|
+
previousCount: previousLength,
|
|
128
|
+
nextCount: currentLength,
|
|
129
|
+
defaultIndex,
|
|
130
|
+
loop,
|
|
131
|
+
retainedIndex,
|
|
132
|
+
});
|
|
133
|
+
setDataRevision((revision) => revision + 1);
|
|
134
|
+
},
|
|
135
|
+
[defaultIndex, handlerOffset, loop, resolvedSize]
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const startMovement = React.useCallback(() => {
|
|
139
|
+
isMoving.value = true;
|
|
140
|
+
}, [isMoving]);
|
|
141
|
+
|
|
142
|
+
const finishMovement = React.useCallback(() => {
|
|
143
|
+
isMoving.value = false;
|
|
144
|
+
const pending = pendingDataChange.value;
|
|
145
|
+
if (!pending) return;
|
|
146
|
+
|
|
147
|
+
pendingDataChange.value = null;
|
|
148
|
+
reconcileDataChange(
|
|
149
|
+
pending.previousLength,
|
|
150
|
+
pending.currentLength,
|
|
151
|
+
pending.retainedIndex ?? undefined
|
|
152
|
+
);
|
|
153
|
+
}, [isMoving, pendingDataChange, reconcileDataChange]);
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* When data changes, we need to compute new index for handlerOffset
|
|
157
|
+
*/
|
|
158
|
+
React.useEffect(() => {
|
|
159
|
+
const previousLength = prevDataLength.value;
|
|
160
|
+
const previousItems = previousRawData.current;
|
|
161
|
+
const dataChanged =
|
|
162
|
+
previousLength !== rawDataLength ||
|
|
163
|
+
previousItems.length !== rawData.length ||
|
|
164
|
+
previousItems.some((item, index) => !Object.is(item, rawData[index]));
|
|
165
|
+
|
|
166
|
+
previousRawData.current = rawData;
|
|
167
|
+
const extractorForPreviousData = previousKeyExtractor.current ?? keyExtractor;
|
|
168
|
+
previousKeyExtractor.current = keyExtractor;
|
|
169
|
+
|
|
170
|
+
if (!dataChanged) return;
|
|
171
|
+
|
|
172
|
+
let retainedIndex: number | undefined;
|
|
173
|
+
if (keyExtractor && extractorForPreviousData && previousLength > 0 && rawDataLength > 0) {
|
|
174
|
+
const currentSize = resolvedSize.value ?? 0;
|
|
175
|
+
const pendingIndex = pendingDataChange.value?.retainedIndex;
|
|
176
|
+
const previousIndex =
|
|
177
|
+
typeof pendingIndex === "number"
|
|
178
|
+
? pendingIndex
|
|
179
|
+
: positiveModulo(getNearestLogicalPage(handlerOffset.value, currentSize), previousLength);
|
|
180
|
+
if (previousIndex < previousItems.length) {
|
|
181
|
+
const selectedKey = extractorForPreviousData(previousItems[previousIndex], previousIndex);
|
|
182
|
+
const matchingIndex = rawData.findIndex(
|
|
183
|
+
(item, index) => keyExtractor(item, index) === selectedKey
|
|
184
|
+
);
|
|
185
|
+
if (matchingIndex >= 0) retainedIndex = matchingIndex;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
prevDataLength.value = rawDataLength;
|
|
190
|
+
if (isMoving.value) {
|
|
191
|
+
pendingDataChange.value = {
|
|
192
|
+
previousLength: pendingDataChange.value?.previousLength ?? previousLength,
|
|
193
|
+
currentLength: rawDataLength,
|
|
194
|
+
retainedIndex: retainedIndex ?? null,
|
|
195
|
+
};
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
reconcileDataChange(previousLength, rawDataLength, retainedIndex);
|
|
200
|
+
}, [
|
|
201
|
+
handlerOffset,
|
|
202
|
+
isMoving,
|
|
203
|
+
keyExtractor,
|
|
204
|
+
pendingDataChange,
|
|
205
|
+
prevDataLength,
|
|
206
|
+
rawData,
|
|
207
|
+
rawDataLength,
|
|
208
|
+
reconcileDataChange,
|
|
209
|
+
resolvedSize,
|
|
210
|
+
]);
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* When size changes, we need to compute new index for handlerOffset
|
|
214
|
+
*/
|
|
215
|
+
useAnimatedReaction(
|
|
216
|
+
() => resolvedSize.value,
|
|
217
|
+
(currentSize) => {
|
|
218
|
+
if (currentSize == null || currentSize <= 0) return;
|
|
219
|
+
const previousSize = prevSize.value;
|
|
220
|
+
|
|
221
|
+
if (previousSize === currentSize) return;
|
|
222
|
+
|
|
223
|
+
sizePhase.value = previousSize > 0 ? "updating" : sizePhase.value;
|
|
224
|
+
|
|
225
|
+
if (dataLength <= 0) {
|
|
226
|
+
prevSize.value = currentSize;
|
|
227
|
+
sizePhase.value = "ready";
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (previousSize <= 0 || !hasInitializedOffset.value) {
|
|
232
|
+
handlerOffset.value = -(defaultIndex * currentSize);
|
|
233
|
+
hasInitializedOffset.value = true;
|
|
234
|
+
} else {
|
|
235
|
+
handlerOffset.value = computeOffsetIfSizeChanged({
|
|
236
|
+
handlerOffset: handlerOffset.value,
|
|
237
|
+
prevSize: previousSize,
|
|
238
|
+
size: currentSize,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
prevSize.value = currentSize;
|
|
243
|
+
sizePhase.value = "ready";
|
|
244
|
+
},
|
|
245
|
+
[dataLength, defaultIndex, hasInitializedOffset, resolvedSize, sizePhase]
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
size,
|
|
250
|
+
validLength: dataLength - 1,
|
|
251
|
+
handlerOffset,
|
|
252
|
+
resolvedSize,
|
|
253
|
+
sizePhase,
|
|
254
|
+
sizeExplicit,
|
|
255
|
+
isMoving,
|
|
256
|
+
dataRevision,
|
|
257
|
+
startMovement,
|
|
258
|
+
cancelMovement: finishMovement,
|
|
259
|
+
settleMovement: finishMovement,
|
|
260
|
+
};
|
|
261
|
+
}
|