@momo-kits/carousel 0.165.2-beta.3 → 0.167.1-beta.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.
Files changed (40) hide show
  1. package/animation.ts +62 -0
  2. package/index.tsx +455 -390
  3. package/package.json +5 -6
  4. package/types.ts +48 -123
  5. package/core/LICENSE +0 -21
  6. package/core/README.md +0 -16
  7. package/core/components/Carousel.tsx +0 -28
  8. package/core/components/CarouselLayout.tsx +0 -231
  9. package/core/components/ItemLayout.tsx +0 -215
  10. package/core/components/ItemRenderer.tsx +0 -126
  11. package/core/components/LazyView.tsx +0 -14
  12. package/core/components/ScrollViewGesture.tsx +0 -443
  13. package/core/constants/index.ts +0 -11
  14. package/core/hooks/useAutoPlay.ts +0 -63
  15. package/core/hooks/useCarouselController.tsx +0 -295
  16. package/core/hooks/useCheckMounted.ts +0 -14
  17. package/core/hooks/useCommonVariables.ts +0 -261
  18. package/core/hooks/useInitProps.ts +0 -177
  19. package/core/hooks/useLayoutConfig.ts +0 -35
  20. package/core/hooks/useOffsetX.ts +0 -81
  21. package/core/hooks/useOnProgressChange.ts +0 -33
  22. package/core/hooks/usePanGestureProxy.ts +0 -144
  23. package/core/hooks/usePropsErrorBoundary.ts +0 -41
  24. package/core/hooks/useUpdateGestureConfig.ts +0 -17
  25. package/core/hooks/useVisibleRanges.tsx +0 -141
  26. package/core/layouts/index.tsx +0 -10
  27. package/core/layouts/normal.ts +0 -32
  28. package/core/layouts/parallax.ts +0 -50
  29. package/core/layouts/stack.ts +0 -266
  30. package/core/public-types.ts +0 -209
  31. package/core/store/index.tsx +0 -63
  32. package/core/types.ts +0 -32
  33. package/core/utils/carousel-direction.ts +0 -39
  34. package/core/utils/carousel-math.ts +0 -156
  35. package/core/utils/compute-gesture-translation.ts +0 -29
  36. package/core/utils/compute-offset-if-size-changed.ts +0 -14
  37. package/core/utils/computed-with-auto-fill-data.ts +0 -92
  38. package/core/utils/deal-with-animation.ts +0 -19
  39. package/core/utils/log.ts +0 -12
  40. package/core/utils/sanitize-animation-style.ts +0 -20
@@ -1,443 +0,0 @@
1
- import type { PropsWithChildren } from "react";
2
- import React, { useCallback } from "react";
3
- import type { LayoutChangeEvent, ViewProps } from "react-native";
4
- import type {
5
- GestureStateChangeEvent,
6
- PanGestureHandlerEventPayload,
7
- } from "react-native-gesture-handler";
8
- import { GestureDetector } from "react-native-gesture-handler";
9
- import Animated, {
10
- cancelAnimation,
11
- useAnimatedReaction,
12
- useDerivedValue,
13
- useSharedValue,
14
- withDecay,
15
- } from "react-native-reanimated";
16
- import type { AnimatedProps, SharedValue } from "react-native-reanimated";
17
- import { scheduleOnRN } from "react-native-worklets";
18
-
19
- import { usePanGestureProxy } from "../hooks/usePanGestureProxy";
20
- import { useGlobalState } from "../store";
21
- import { toLogicalGestureValue } from "../utils/carousel-direction";
22
- import { computeGestureTranslation } from "../utils/compute-gesture-translation";
23
- import { dealWithAnimation } from "../utils/deal-with-animation";
24
-
25
- interface Props {
26
- size: number;
27
- infinite?: boolean;
28
- testID?: string;
29
- style?: AnimatedProps<ViewProps>["style"];
30
- translation: SharedValue<number>;
31
- onScrollStart?: () => void;
32
- onScrollEnd?: () => void;
33
- onTouchBegin?: () => void;
34
- onTouchEnd?: () => void;
35
- }
36
-
37
- const IScrollViewGesture: React.FC<PropsWithChildren<Props>> = (props) => {
38
- const {
39
- props: {
40
- onConfigurePanGesture,
41
- orientation,
42
- snapMode,
43
- loop,
44
- animation,
45
- scrollEnabled,
46
- dataLength,
47
- overscrollEnabled,
48
- directionSign,
49
- },
50
- common: { size, resolvedSize, sizePhase, sizeExplicit },
51
- layout: { containerSize, updateContainerSize },
52
- } = useGlobalState();
53
-
54
- const {
55
- translation,
56
- testID,
57
- style = {},
58
- onScrollStart,
59
- onScrollEnd,
60
- onTouchBegin,
61
- onTouchEnd,
62
- } = props;
63
- const isVertical = orientation === "vertical";
64
-
65
- const maxPage = dataLength;
66
- const isHorizontal = useDerivedValue(() => !isVertical, [isVertical]);
67
- const max = useSharedValue(0);
68
- const panOffset = useSharedValue<number | undefined>(undefined); // set to undefined when not actively in a pan gesture
69
- const touching = useSharedValue(false);
70
- const validStart = useSharedValue(false);
71
- const hasStartedMovement = useSharedValue(false);
72
- const scrollEndTranslation = useSharedValue(0);
73
- const scrollEndVelocity = useSharedValue(0);
74
- const sizeReady = useDerivedValue(() => {
75
- const currentSize = resolvedSize.value ?? 0;
76
- return sizePhase.value === "ready" && currentSize > 0;
77
- }, [resolvedSize, sizePhase]);
78
-
79
- // Get the limit of the scroll.
80
- const getLimit = React.useCallback(() => {
81
- "worklet";
82
-
83
- if (size <= 0) return 0;
84
-
85
- if (!loop && !overscrollEnabled) {
86
- const containerMainAxisSize = isVertical
87
- ? containerSize.value.height
88
- : containerSize.value.width;
89
-
90
- // If the item's total size is less than the container's size, then there is no need to scroll.
91
- if (containerMainAxisSize <= 0 || dataLength * size < containerMainAxisSize) return 0;
92
-
93
- // Disable the "overscroll" effect
94
- return dataLength * size - containerMainAxisSize;
95
- }
96
-
97
- return dataLength * size;
98
- }, [loop, size, dataLength, overscrollEnabled, isVertical, containerSize]);
99
-
100
- const withSpring = React.useCallback(
101
- (toValue: number, onFinished?: () => void) => {
102
- "worklet";
103
- return dealWithAnimation(animation)(toValue, (isFinished: boolean) => {
104
- "worklet";
105
- if (isFinished) onFinished && scheduleOnRN(onFinished);
106
- });
107
- },
108
- [animation]
109
- );
110
-
111
- const endWithSpring = React.useCallback(
112
- (
113
- scrollEndTranslationValue: number,
114
- scrollEndVelocityValue: number,
115
- onFinished?: () => void
116
- ) => {
117
- "worklet";
118
- // Use resolvedSize.value (SharedValue) instead of size (React state)
119
- // to avoid race condition where sizeReady is true but size is still 0
120
- const currentSize = resolvedSize.value ?? 0;
121
- if (currentSize <= 0) {
122
- return;
123
- }
124
- const origin = translation.value;
125
- const velocity = scrollEndVelocityValue;
126
- // Default to scroll in the direction of the slide (with deceleration)
127
- let finalTranslation: number = withDecay({ velocity, deceleration: 0.999 }, (isFinished) => {
128
- if (isFinished && onFinished) scheduleOnRN(onFinished);
129
- });
130
-
131
- /**
132
- * The page size is the same as the item size.
133
- * If direction is vertical, the page size is the height of the item.
134
- * If direction is horizontal, the page size is the width of the item.
135
- *
136
- * `page size` equals to `currentSize` variable.
137
- */
138
-
139
- // Calculate target based on final pan position and velocity.
140
- const nextPage = -Math.round((origin + velocity * 2) / currentSize);
141
-
142
- if (snapMode === "page") {
143
- // we'll never go further than a single page away from the current page when paging
144
- // is enabled.
145
-
146
- // distance with direction
147
- const offset = -(scrollEndTranslationValue >= 0 ? 1 : -1); // 1 or -1
148
- const computed = offset < 0 ? Math.ceil : Math.floor;
149
- const page = computed(-origin / currentSize);
150
-
151
- const velocityDirection = -Math.sign(velocity);
152
- if (page === nextPage || velocityDirection !== offset) {
153
- // not going anywhere! Velocity was insufficient to overcome the distance to get to a
154
- // further page. Let's reset gently to the current page.
155
- finalTranslation = withSpring(withProcessTranslation(-page * currentSize), onFinished);
156
- } else if (loop) {
157
- const finalPage = page + offset;
158
- finalTranslation = withSpring(
159
- withProcessTranslation(-finalPage * currentSize),
160
- onFinished
161
- );
162
- } else {
163
- const finalPage = Math.min(maxPage - 1, Math.max(0, page + offset));
164
- finalTranslation = withSpring(
165
- withProcessTranslation(-finalPage * currentSize),
166
- onFinished
167
- );
168
- }
169
- }
170
-
171
- if (snapMode === "nearest") {
172
- finalTranslation = withSpring(withProcessTranslation(-nextPage * currentSize), onFinished);
173
- }
174
-
175
- translation.value = finalTranslation;
176
-
177
- function withProcessTranslation(translation: number) {
178
- if (!loop && !overscrollEnabled) {
179
- const limit = getLimit();
180
- return Math.min(0, Math.max(-limit, translation));
181
- }
182
-
183
- return translation;
184
- }
185
- },
186
- [withSpring, resolvedSize, maxPage, loop, snapMode, translation]
187
- );
188
-
189
- const onFinish = React.useCallback(
190
- (isFinished: boolean) => {
191
- "worklet";
192
- if (isFinished) {
193
- touching.value = false;
194
- onScrollEnd && scheduleOnRN(onScrollEnd);
195
- }
196
- },
197
- [onScrollEnd, touching]
198
- );
199
-
200
- const activeDecay = React.useCallback(() => {
201
- "worklet";
202
- touching.value = true;
203
- translation.value = withDecay({ velocity: scrollEndVelocity.value }, (isFinished) =>
204
- onFinish(isFinished as boolean)
205
- );
206
- }, [onFinish, scrollEndVelocity, touching, translation]);
207
-
208
- const resetBoundary = React.useCallback(() => {
209
- "worklet";
210
- // Use resolvedSize.value (SharedValue) instead of size (React state)
211
- // to avoid race condition where sizeReady is true but size is still 0
212
- const currentSize = resolvedSize.value ?? 0;
213
- if (currentSize <= 0) return;
214
- if (touching.value) return;
215
-
216
- if (translation.value > 0) {
217
- if (scrollEndTranslation.value < 0) {
218
- activeDecay();
219
- return;
220
- }
221
- if (!loop) {
222
- translation.value = withSpring(0);
223
- return;
224
- }
225
- }
226
-
227
- if (translation.value < -((maxPage - 1) * currentSize)) {
228
- if (scrollEndTranslation.value > 0) {
229
- activeDecay();
230
- return;
231
- }
232
- if (!loop) translation.value = withSpring(-((maxPage - 1) * currentSize));
233
- }
234
- }, [touching, translation, resolvedSize, scrollEndTranslation, loop, activeDecay, withSpring]);
235
-
236
- useAnimatedReaction(
237
- () => translation.value,
238
- () => {
239
- if (snapMode !== "page") resetBoundary();
240
- },
241
- [resetBoundary, snapMode]
242
- );
243
-
244
- const onGestureStart = useCallback(
245
- (_: PanGestureHandlerEventPayload) => {
246
- "worklet";
247
- // Use resolvedSize.value (SharedValue) instead of size (React state)
248
- // to avoid race condition where sizeReady is true but size is still 0
249
- const currentSize = resolvedSize.value ?? 0;
250
- if (!sizeReady.value || currentSize <= 0) {
251
- return;
252
- }
253
- touching.value = true;
254
- validStart.value = true;
255
- hasStartedMovement.value = false;
256
-
257
- max.value = (maxPage - 1) * currentSize;
258
- if (!loop && !overscrollEnabled) max.value = getLimit();
259
-
260
- panOffset.value = translation.value;
261
- },
262
- [
263
- max,
264
- maxPage,
265
- loop,
266
- touching,
267
- panOffset,
268
- validStart,
269
- translation,
270
- overscrollEnabled,
271
- getLimit,
272
- hasStartedMovement,
273
- sizeReady,
274
- resolvedSize,
275
- ]
276
- );
277
-
278
- const onGestureUpdate = useCallback(
279
- (e: PanGestureHandlerEventPayload) => {
280
- "worklet";
281
- const currentSize = resolvedSize.value ?? 0;
282
- if (!sizeReady.value || currentSize <= 0) {
283
- return;
284
- }
285
- if (panOffset.value === undefined) {
286
- // This may happen if `onGestureStart` is called as a part of the
287
- // JS thread (instead of the UI thread / worklet). If so, when
288
- // `onGestureStart` sets panOffset.value, the set will be asynchronous,
289
- // and so it may not actually occur before `onGestureUpdate` is called.
290
- //
291
- // Keeping this value as `undefined` when it is not active protects us
292
- // from the situation where we may use the previous value for panOffset
293
- // instead; this would cause a visual flicker in the carousel.
294
-
295
- // console.warn("onGestureUpdate: panOffset is undefined");
296
- return;
297
- }
298
-
299
- if (validStart.value) {
300
- validStart.value = false;
301
- cancelAnimation(translation);
302
- }
303
- touching.value = true;
304
- const { translationX, translationY } = e;
305
-
306
- const physicalPanTranslation = isHorizontal.value ? translationX : translationY;
307
- const panTranslation = toLogicalGestureValue(physicalPanTranslation, directionSign);
308
- if (!hasStartedMovement.value && panTranslation !== 0) {
309
- hasStartedMovement.value = true;
310
- if (onScrollStart) scheduleOnRN(onScrollStart);
311
- }
312
-
313
- translation.value = computeGestureTranslation({
314
- loop,
315
- overscrollEnabled: overscrollEnabled ?? true,
316
- currentTranslation: translation.value,
317
- panOffset: panOffset.value,
318
- panTranslation,
319
- max: max.value,
320
- });
321
- },
322
- [
323
- isHorizontal,
324
- max,
325
- panOffset,
326
- loop,
327
- overscrollEnabled,
328
- hasStartedMovement,
329
- onScrollStart,
330
- translation,
331
- validStart,
332
- touching,
333
- sizeReady,
334
- resolvedSize,
335
- directionSign,
336
- ]
337
- );
338
-
339
- const onGestureEnd = useCallback(
340
- (e: GestureStateChangeEvent<PanGestureHandlerEventPayload>, _success: boolean) => {
341
- "worklet";
342
- // Use resolvedSize.value (SharedValue) instead of size (React state)
343
- // to avoid race condition where sizeReady is true but size is still 0
344
- const currentSize = resolvedSize.value ?? 0;
345
- if (!sizeReady.value || currentSize <= 0) {
346
- panOffset.value = undefined;
347
- return;
348
- }
349
-
350
- if (panOffset.value === undefined) {
351
- // console.warn("onGestureEnd: panOffset is undefined");
352
- return;
353
- }
354
-
355
- if (!hasStartedMovement.value) {
356
- touching.value = false;
357
- panOffset.value = undefined;
358
- return;
359
- }
360
-
361
- const { velocityX, velocityY, translationX, translationY } = e;
362
- const physicalVelocity = isHorizontal.value ? velocityX : velocityY;
363
- const scrollEndVelocityValue = toLogicalGestureValue(physicalVelocity, directionSign);
364
- scrollEndVelocity.value = scrollEndVelocityValue; // may update async: see https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue#remarks
365
-
366
- const physicalPanTranslation = isHorizontal.value ? translationX : translationY;
367
- const panTranslation = toLogicalGestureValue(physicalPanTranslation, directionSign);
368
-
369
- scrollEndTranslation.value = panTranslation; // may update async: see https://docs.swmansion.com/react-native-reanimated/docs/core/useSharedValue#remarks
370
-
371
- endWithSpring(panTranslation, scrollEndVelocityValue, onScrollEnd);
372
-
373
- if (!loop) touching.value = false;
374
-
375
- panOffset.value = undefined;
376
- },
377
- [
378
- loop,
379
- touching,
380
- panOffset,
381
- translation,
382
- isHorizontal,
383
- scrollEndVelocity,
384
- scrollEndTranslation,
385
- endWithSpring,
386
- onScrollEnd,
387
- hasStartedMovement,
388
- sizeReady,
389
- resolvedSize,
390
- directionSign,
391
- ]
392
- );
393
-
394
- const gesture = usePanGestureProxy({
395
- onConfigurePanGesture,
396
- onGestureStart,
397
- onGestureUpdate,
398
- onGestureEnd,
399
- options: { enabled: scrollEnabled },
400
- });
401
-
402
- const onLayout = React.useCallback(
403
- (e: LayoutChangeEvent) => {
404
- "worklet";
405
-
406
- const measuredWidth = e.nativeEvent.layout.width;
407
- const measuredHeight = e.nativeEvent.layout.height;
408
- const measuredSize = Math.round((isVertical ? measuredHeight : measuredWidth) || 0);
409
-
410
- if (!sizeExplicit && measuredSize > 0) {
411
- const current = resolvedSize.value ?? 0;
412
- if (Math.abs(current - measuredSize) > 0) {
413
- sizePhase.value = current > 0 ? "updating" : sizePhase.value;
414
- resolvedSize.value = measuredSize;
415
- sizePhase.value = "ready";
416
- }
417
- }
418
-
419
- updateContainerSize({
420
- width: measuredWidth,
421
- height: measuredHeight,
422
- });
423
- },
424
- [updateContainerSize, resolvedSize, sizePhase, isVertical, sizeExplicit]
425
- );
426
-
427
- return (
428
- <GestureDetector gesture={gesture}>
429
- <Animated.View
430
- testID={testID}
431
- style={style}
432
- onTouchStart={onTouchBegin}
433
- onTouchEnd={onTouchEnd}
434
- onTouchCancel={onTouchEnd}
435
- onLayout={onLayout}
436
- >
437
- {props.children}
438
- </Animated.View>
439
- </GestureDetector>
440
- );
441
- };
442
-
443
- export const ScrollViewGesture = IScrollViewGesture;
@@ -1,11 +0,0 @@
1
- import type { EasingFunction } from "react-native-reanimated";
2
- import { Easing as _Easing } from "react-native-reanimated";
3
-
4
- export enum DATA_LENGTH {
5
- SINGLE_ITEM = 1,
6
- DOUBLE_ITEM = 2,
7
- }
8
-
9
- export const Easing = {
10
- easeOutQuart: _Easing.bezier(0.25, 1, 0.5, 1) as unknown as EasingFunction,
11
- };
@@ -1,63 +0,0 @@
1
- import * as React from "react";
2
-
3
- import type { CarouselController } from "./useCarouselController";
4
-
5
- export function useAutoPlay(options: {
6
- autoplay?: boolean;
7
- autoplayInterval?: number;
8
- autoplayDirection?: "forward" | "backward";
9
- carouselController: CarouselController;
10
- }) {
11
- const {
12
- autoplay = false,
13
- autoplayDirection = "forward",
14
- autoplayInterval,
15
- carouselController,
16
- } = options;
17
- const { prev, next } = carouselController;
18
- const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null);
19
- const stopped = React.useRef(!autoplay);
20
-
21
- const play = React.useCallback(() => {
22
- if (stopped.current) return;
23
-
24
- if (timer.current) {
25
- clearTimeout(timer.current);
26
- timer.current = null;
27
- }
28
- timer.current = setTimeout(() => {
29
- if (autoplayDirection === "backward") prev({ onFinished: play });
30
- else next({ onFinished: play });
31
- }, autoplayInterval);
32
- }, [autoplayDirection, autoplayInterval, next, prev]);
33
-
34
- const pause = React.useCallback(() => {
35
- if (!autoplay) return;
36
-
37
- if (timer.current) {
38
- clearTimeout(timer.current);
39
- timer.current = null;
40
- }
41
- stopped.current = true;
42
- }, [autoplay]);
43
-
44
- const start = React.useCallback(() => {
45
- if (!autoplay) return;
46
-
47
- stopped.current = false;
48
- play();
49
- }, [autoplay, play]);
50
-
51
- React.useEffect(() => {
52
- if (autoplay) start();
53
- else pause();
54
-
55
- return pause;
56
- }, [autoplay, pause, start]);
57
-
58
- return {
59
- pause,
60
- start,
61
- trigger: play,
62
- };
63
- }