@momo-kits/carousel 0.165.2-beta.2 → 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
package/index.tsx CHANGED
@@ -9,483 +9,548 @@ import React, {
9
9
  } from 'react';
10
10
  import {
11
11
  Dimensions,
12
- I18nManager,
12
+ FlatList,
13
+ GestureResponderEvent,
14
+ LayoutChangeEvent,
15
+ NativeScrollEvent,
16
+ NativeSyntheticEvent,
17
+ Platform,
13
18
  StyleSheet,
14
19
  View,
15
- type LayoutChangeEvent,
16
- type NativeScrollEvent,
17
- type NativeSyntheticEvent,
18
20
  } from 'react-native';
19
- import {
21
+ import Animated, {
20
22
  Extrapolation,
21
23
  interpolate,
22
24
  runOnJS,
25
+ useAnimatedScrollHandler,
26
+ useAnimatedStyle,
27
+ useSharedValue,
23
28
  type SharedValue,
24
29
  } from 'react-native-reanimated';
30
+ import { CarouselProps, CarouselRef } from './types';
31
+
32
+ const { width: viewportWidth } = Dimensions.get('window');
33
+
34
+ const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
35
+
36
+ type CarouselItemProps = {
37
+ index: number;
38
+ itemWidth: number;
39
+ scrollX: SharedValue<number>;
40
+ inactiveSlideOpacity: number;
41
+ inactiveSlideScale: number;
42
+ slideStyle: any;
43
+ children: React.ReactNode;
44
+ };
45
+
46
+ const CarouselItem: React.FC<CarouselItemProps> = ({
47
+ index,
48
+ itemWidth,
49
+ scrollX,
50
+ inactiveSlideOpacity,
51
+ inactiveSlideScale,
52
+ slideStyle,
53
+ children,
54
+ }) => {
55
+ const animatedStyle = useAnimatedStyle(() => {
56
+ const inputRange = [
57
+ (index - 1) * itemWidth,
58
+ index * itemWidth,
59
+ (index + 1) * itemWidth,
60
+ ];
61
+ const opacity =
62
+ inactiveSlideOpacity < 1
63
+ ? interpolate(
64
+ scrollX.value,
65
+ inputRange,
66
+ [inactiveSlideOpacity, 1, inactiveSlideOpacity],
67
+ Extrapolation.CLAMP,
68
+ )
69
+ : 1;
70
+ const scale =
71
+ inactiveSlideScale < 1
72
+ ? interpolate(
73
+ scrollX.value,
74
+ inputRange,
75
+ [inactiveSlideScale, 1, inactiveSlideScale],
76
+ Extrapolation.CLAMP,
77
+ )
78
+ : 1;
79
+ return {
80
+ opacity,
81
+ transform: [{ scale }],
82
+ };
83
+ });
84
+
85
+ return (
86
+ <Animated.View style={[{ width: itemWidth }, slideStyle, animatedStyle]}>
87
+ {children}
88
+ </Animated.View>
89
+ );
90
+ };
25
91
 
26
- import { Carousel as CoreCarousel } from './core/components/Carousel';
27
- import type {
28
- CarouselItemAnimation,
29
- CarouselPanGesture,
30
- CarouselProps as CoreCarouselProps,
31
- CarouselRef as CoreCarouselRef,
32
- } from './core/public-types';
33
- import type { CarouselProps, CarouselRef } from './types';
34
-
35
- const { width: WINDOW_WIDTH } = Dimensions.get('window');
36
-
37
- /** Gap the FlatList implementation left between items when `visibleItem > 1`. */
38
- const MULTI_ITEM_SPACING = 10;
39
-
40
- /** How long a `fireCallback: false` command suppresses `onSnapToItem`. */
41
- const SUPPRESS_CALLBACK_WINDOW = 1000;
42
-
43
- const IGNORED_LEGACY_PROPS = [
44
- 'loopClonesPerSide',
45
- 'snapToInterval',
46
- 'disableIntervalMomentum',
47
- 'getItemLayout',
48
- 'activeSlideOffset',
49
- 'onMomentumScrollEnd',
50
- ] as const;
51
-
52
- function positiveModulo(value: number, divisor: number) {
53
- if (divisor <= 0) return 0;
54
- const remainder = value % divisor;
55
- return remainder < 0 ? remainder + divisor : remainder;
56
- }
57
-
58
- function scrollEvent(x: number) {
59
- return {
60
- nativeEvent: { contentOffset: { x, y: 0 } },
61
- } as NativeSyntheticEvent<NativeScrollEvent>;
62
- }
63
-
64
- const CarouselImpl = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
92
+ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
65
93
  const {
66
- // engine props
67
94
  data,
68
95
  renderItem,
69
96
  keyExtractor,
70
- defaultIndex,
71
- loop,
72
- autoplay = false,
73
- autoplayInterval,
74
- autoplayDirection,
75
- orientation = 'horizontal',
76
- itemSize,
77
- scrollEnabled,
78
- snapMode,
79
- overscrollEnabled,
80
- renderWindowSize,
81
- animation,
82
- progress,
83
- onProgressChange,
84
- scrollOffsetValue,
85
- style,
86
- contentContainerStyle,
87
- testID,
88
- onLayout,
89
- onConfigurePanGesture,
90
- onScrollStart,
91
- onSnapToItem,
92
- layout,
93
- itemAnimation,
94
-
95
- // legacy props
96
- firstItem,
97
- itemWidth,
97
+ firstItem = 0,
98
+ itemWidth: itemWidthProp,
98
99
  visibleItem = 1,
99
100
  full = false,
100
- enableSnap,
101
- autoplayDelay = 0,
101
+ activeSlideOffset = 20,
102
+ scrollEnabled = true,
103
+ enableSnap = true,
104
+ snapToInterval: snapToIntervalProp,
105
+ disableIntervalMomentum = Platform.OS === 'android',
106
+ loop = false,
107
+ loopClonesPerSide = 3,
108
+ autoplay = false,
109
+ autoplayDelay = 1000,
110
+ autoplayInterval = 3000,
102
111
  inactiveSlideOpacity = 1,
103
112
  inactiveSlideScale = 1,
104
113
  apparitionDelay = 0,
114
+ style,
105
115
  slideStyle,
116
+ contentContainerStyle,
117
+ onSnapToItem,
106
118
  onScrollIndexChanged,
107
- onScroll,
108
- onTouchStart,
109
- onTouchEnd,
119
+ onScroll: onScrollProp,
120
+ onMomentumScrollEnd: onMomentumScrollEndProp,
121
+ onTouchStart: onTouchStartProp,
122
+ onTouchEnd: onTouchEndProp,
123
+ onLayout: onLayoutProp,
124
+ getItemLayout: getItemLayoutProp,
110
125
  } = props;
111
126
 
112
- const coreRef = useRef<CoreCarouselRef>(null);
113
- const suppressUntilRef = useRef(0);
114
- const autoplayDelayTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
115
-
116
- const [containerSize, setContainerSize] = useState(WINDOW_WIDTH);
117
- const [measuredItemSize, setMeasuredItemSize] = useState<number | null>(null);
118
- const [isVisible, setIsVisible] = useState(apparitionDelay <= 0);
119
- const [isAutoplayRunning, setIsAutoplayRunning] = useState(false);
120
-
121
- const isVertical = orientation === 'vertical';
122
- const flattenedStyle = StyleSheet.flatten(style) ?? {};
123
- const sizeField = isVertical ? 'width' : 'height';
124
- const hasExplicitSize =
125
- flattenedStyle[sizeField] !== undefined ||
126
- flattenedStyle.flex !== undefined ||
127
- flattenedStyle.flexGrow !== undefined;
128
-
129
- const warnedRef = useRef(false);
130
- if (__DEV__ && !warnedRef.current) {
131
- warnedRef.current = true;
132
- for (const name of IGNORED_LEGACY_PROPS) {
133
- if (props[name] !== undefined) {
134
- console.warn(
135
- `[@momo-kits/carousel] \`${name}\` is no longer supported and is ignored.`
136
- );
137
- }
127
+ const flatListRef = useRef<FlatList>(null);
128
+ const autoplayTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
129
+ const scrollX = useSharedValue(0);
130
+ const containerWidthRef = useRef(viewportWidth);
131
+ const isAutoplayPausedRef = useRef(false);
132
+ const currentIndexRef = useRef(firstItem);
133
+
134
+ const [currentIndex, setCurrentIndex] = useState(firstItem);
135
+ const [containerWidth, setContainerWidth] = useState(viewportWidth);
136
+ const [isVisible, setIsVisible] = useState(apparitionDelay === 0);
137
+ const [isLayoutReady, setIsLayoutReady] = useState(false);
138
+
139
+ const itemWidth = useMemo(() => {
140
+ if (full) {
141
+ return containerWidth;
138
142
  }
139
- for (const name of ['onTouchStart', 'onTouchEnd'] as const) {
140
- if (props[name] && props[name]!.length > 0) {
141
- console.warn(
142
- `[@momo-kits/carousel] \`${name}\` is called without an event object; the gesture handler has no GestureResponderEvent to pass.`
143
- );
144
- }
143
+ if (itemWidthProp) {
144
+ return itemWidthProp;
145
145
  }
146
- }
146
+ const spacing = visibleItem > 1 ? 10 : 0;
147
+ return (containerWidth - spacing * (visibleItem - 1)) / visibleItem;
148
+ }, [containerWidth, full, itemWidthProp, visibleItem]);
147
149
 
148
- const resolvedItemSize = useMemo(() => {
149
- if (itemSize && itemSize > 0) return itemSize;
150
- if (itemWidth && itemWidth > 0) return itemWidth;
151
- if (!full && visibleItem > 1) {
152
- const size =
153
- (containerSize - MULTI_ITEM_SPACING * (visibleItem - 1)) / visibleItem;
154
- return size > 0 ? size : undefined;
150
+ const snapToInterval = useMemo(() => {
151
+ if (snapToIntervalProp !== undefined) {
152
+ return snapToIntervalProp;
155
153
  }
156
- // Undefined lets the engine measure the container itself.
157
- return undefined;
158
- }, [containerSize, full, itemSize, itemWidth, visibleItem]);
159
-
160
- /** Size the animations interpolate against. */
161
- const pageSize = resolvedItemSize ?? containerSize;
162
-
163
- const resolvedDefaultIndex = useMemo(() => {
164
- const requested = defaultIndex ?? firstItem ?? 0;
165
- return Number.isInteger(requested) &&
166
- requested >= 0 &&
167
- requested < data.length
168
- ? requested
169
- : 0;
170
- }, [data.length, defaultIndex, firstItem]);
171
-
172
- const resolvedSnapMode = snapMode ?? (enableSnap === false ? 'none' : 'page');
173
-
174
- const hasLegacyItemAnimation =
175
- inactiveSlideOpacity < 1 || inactiveSlideScale < 1;
176
-
177
- const legacyItemAnimation = useMemo<CarouselItemAnimation | undefined>(() => {
178
- if (!hasLegacyItemAnimation) return undefined;
179
-
180
- // An `itemAnimation` replaces the built-in layout, so it has to carry the
181
- // slide translation as well as the legacy opacity/scale.
182
- const size = pageSize;
183
- const directionSign = I18nManager.isRTL ? -1 : 1;
184
- const minOpacity = inactiveSlideOpacity;
185
- const minScale = inactiveSlideScale;
186
-
187
- return (relativeProgress: number) => {
188
- 'worklet';
189
- const translate = interpolate(
190
- relativeProgress,
191
- [-1, 0, 1],
192
- [-size, 0, size]
193
- );
194
- const opacity =
195
- minOpacity < 1
196
- ? interpolate(
197
- relativeProgress,
198
- [-1, 0, 1],
199
- [minOpacity, 1, minOpacity],
200
- Extrapolation.CLAMP
201
- )
202
- : 1;
203
- const scale =
204
- minScale < 1
205
- ? interpolate(
206
- relativeProgress,
207
- [-1, 0, 1],
208
- [minScale, 1, minScale],
209
- Extrapolation.CLAMP
210
- )
211
- : 1;
154
+ return itemWidth;
155
+ }, [itemWidth, snapToIntervalProp]);
212
156
 
213
- return {
214
- opacity,
215
- transform: [
216
- isVertical
217
- ? { translateY: translate }
218
- : { translateX: translate * directionSign },
219
- { scale },
220
- ],
221
- };
222
- };
223
- }, [
224
- hasLegacyItemAnimation,
225
- inactiveSlideOpacity,
226
- inactiveSlideScale,
227
- isVertical,
228
- pageSize,
229
- ]);
230
-
231
- const resolvedItemAnimation =
232
- itemAnimation ?? (layout ? undefined : legacyItemAnimation);
233
-
234
- const handleContainerLayout = useCallback(
235
- (event: LayoutChangeEvent) => {
236
- const { width, height } = event.nativeEvent.layout;
237
- const size = isVertical ? height : width;
238
- if (size > 0) setContainerSize(size);
239
- onLayout?.(event);
157
+ const dataWithClones = useMemo(() => {
158
+ if (!loop || data.length === 0) {
159
+ return data;
160
+ }
161
+
162
+ const clones = [];
163
+ for (let i = 0; i < loopClonesPerSide; i++) {
164
+ const startIndex = data.length - loopClonesPerSide + i;
165
+ const endIndex = i;
166
+ clones.push(data[startIndex >= 0 ? startIndex : data.length + startIndex]);
167
+ }
168
+
169
+ const result = [...clones, ...data];
170
+
171
+ for (let i = 0; i < loopClonesPerSide; i++) {
172
+ result.push(data[i % data.length]);
173
+ }
174
+
175
+ return result;
176
+ }, [data, loop, loopClonesPerSide]);
177
+
178
+ const getRealIndex = useCallback(
179
+ (index: number) => {
180
+ if (!loop) return index;
181
+
182
+ const dataLength = data.length;
183
+ if (dataLength === 0) return 0;
184
+
185
+ let realIndex = index - loopClonesPerSide;
186
+
187
+ if (realIndex < 0) {
188
+ realIndex = dataLength + realIndex;
189
+ } else if (realIndex >= dataLength) {
190
+ realIndex = realIndex % dataLength;
191
+ }
192
+
193
+ return realIndex;
240
194
  },
241
- [isVertical, onLayout]
195
+ [loop, loopClonesPerSide, data.length]
242
196
  );
243
197
 
244
- const handleItemLayout = useCallback(
245
- (event: LayoutChangeEvent) => {
246
- if (hasExplicitSize) return;
247
- const { width, height } = event.nativeEvent.layout;
248
- const size = isVertical ? width : height;
249
- if (size > 0) {
250
- setMeasuredItemSize(prev => (prev === null || size > prev ? size : prev));
198
+ const getLoopIndex = useCallback(
199
+ (realIndex: number) => {
200
+ if (!loop) return realIndex;
201
+ return realIndex + loopClonesPerSide;
202
+ },
203
+ [loop, loopClonesPerSide]
204
+ );
205
+
206
+ const snapToItem = useCallback(
207
+ (index: number, animated = true, fireCallback = true) => {
208
+ if (!flatListRef.current || index < 0 || index >= data.length) {
209
+ return;
210
+ }
211
+
212
+ const loopIndex = getLoopIndex(index);
213
+ const offset = loopIndex * itemWidth;
214
+
215
+ flatListRef.current.scrollToOffset({
216
+ offset,
217
+ animated,
218
+ });
219
+
220
+ currentIndexRef.current = index;
221
+ setCurrentIndex(index);
222
+
223
+ if (fireCallback && onSnapToItem) {
224
+ onSnapToItem(index);
251
225
  }
252
226
  },
253
- [hasExplicitSize, isVertical]
227
+ [data.length, getLoopIndex, itemWidth, onSnapToItem]
254
228
  );
255
229
 
256
- const slideWrapperStyle = useMemo(
257
- () => (hasExplicitSize ? styles.slideFill : styles.slideAuto),
258
- [hasExplicitSize]
230
+ const snapToNext = useCallback(
231
+ (animated = true, fireCallback = true) => {
232
+ const nextIndex = (currentIndexRef.current + 1) % data.length;
233
+ snapToItem(nextIndex, animated, fireCallback);
234
+ },
235
+ [data.length, snapToItem]
259
236
  );
260
237
 
261
- const renderCoreItem = useCallback(
262
- (info: { item: any; index: number; relativeProgress: SharedValue<number> }) => (
263
- <View style={[slideWrapperStyle, slideStyle]} onLayout={handleItemLayout}>
264
- {renderItem(info)}
265
- </View>
266
- ),
267
- [handleItemLayout, renderItem, slideStyle, slideWrapperStyle]
238
+ const snapToPrev = useCallback(
239
+ (animated = true, fireCallback = true) => {
240
+ const prevIndex =
241
+ currentIndexRef.current === 0 ? data.length - 1 : currentIndexRef.current - 1;
242
+ snapToItem(prevIndex, animated, fireCallback);
243
+ },
244
+ [data.length, snapToItem]
268
245
  );
269
246
 
270
- const lastIndexRef = useRef(resolvedDefaultIndex);
247
+ const stopAutoplay = useCallback(() => {
248
+ if (autoplayTimerRef.current) {
249
+ clearInterval(autoplayTimerRef.current);
250
+ autoplayTimerRef.current = null;
251
+ }
252
+ }, []);
271
253
 
272
- const needsProgress = !!(onProgressChange || onScroll || onScrollIndexChanged);
254
+ const pauseAutoPlay = useCallback(() => {
255
+ isAutoplayPausedRef.current = true;
256
+ stopAutoplay();
257
+ }, [stopAutoplay]);
273
258
 
274
- const handleProgressChange = useCallback(
275
- (value: number) => {
276
- onProgressChange?.(value);
277
- onScroll?.(scrollEvent(value * pageSize));
259
+ const startAutoplay = useCallback(() => {
260
+ stopAutoplay();
261
+ isAutoplayPausedRef.current = false;
278
262
 
279
- if (onScrollIndexChanged && data.length > 0) {
280
- const index = positiveModulo(Math.round(value), data.length);
281
- if (index !== lastIndexRef.current) {
282
- lastIndexRef.current = index;
283
- onScrollIndexChanged(index);
263
+ if (!autoplay || data.length <= 1) {
264
+ return;
265
+ }
266
+
267
+ const startTimer = () => {
268
+ autoplayTimerRef.current = setInterval(() => {
269
+ if (!isAutoplayPausedRef.current) {
270
+ snapToNext(true, true);
284
271
  }
272
+ }, autoplayInterval);
273
+ };
274
+
275
+ if (autoplayDelay > 0) {
276
+ setTimeout(startTimer, autoplayDelay);
277
+ } else {
278
+ startTimer();
279
+ }
280
+ }, [autoplay, autoplayDelay, autoplayInterval, data.length, snapToNext, stopAutoplay]);
281
+
282
+ const handleLoopReposition = useCallback(
283
+ (index: number) => {
284
+ if (!loop || !flatListRef.current) return;
285
+
286
+ const dataLength = data.length;
287
+ if (dataLength === 0) return;
288
+
289
+ if (index < loopClonesPerSide) {
290
+ const realIndex = dataLength - (loopClonesPerSide - index);
291
+ const loopIndex = getLoopIndex(realIndex);
292
+ setTimeout(() => {
293
+ flatListRef.current?.scrollToOffset({
294
+ offset: loopIndex * itemWidth,
295
+ animated: false,
296
+ });
297
+ }, 50);
298
+ } else if (index >= dataLength + loopClonesPerSide) {
299
+ const realIndex = index - (dataLength + loopClonesPerSide);
300
+ const loopIndex = getLoopIndex(realIndex);
301
+ setTimeout(() => {
302
+ flatListRef.current?.scrollToOffset({
303
+ offset: loopIndex * itemWidth,
304
+ animated: false,
305
+ });
306
+ }, 50);
285
307
  }
286
308
  },
287
- [data.length, onProgressChange, onScroll, onScrollIndexChanged, pageSize]
309
+ [loop, data.length, loopClonesPerSide, itemWidth, getLoopIndex]
288
310
  );
289
311
 
290
- const handleSnapToItem = useCallback(
291
- (index: number) => {
292
- lastIndexRef.current = index;
312
+ const handleScroll = useCallback(
313
+ (offsetX: number) => {
314
+ const index = Math.round(offsetX / itemWidth);
315
+ const realIndex = getRealIndex(index);
293
316
 
294
- if (Date.now() < suppressUntilRef.current) {
295
- suppressUntilRef.current = 0;
296
- return;
317
+ if (realIndex !== currentIndexRef.current) {
318
+ currentIndexRef.current = realIndex;
319
+ setCurrentIndex(realIndex);
320
+
321
+ if (onScrollIndexChanged) {
322
+ onScrollIndexChanged(realIndex);
323
+ }
297
324
  }
325
+ },
326
+ [itemWidth, getRealIndex, onScrollIndexChanged]
327
+ );
328
+
329
+ const handleMomentumScrollEnd = useCallback(
330
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
331
+ const offsetX = event.nativeEvent.contentOffset.x;
332
+ const index = Math.round(offsetX / itemWidth);
333
+ const realIndex = getRealIndex(index);
298
334
 
299
- onSnapToItem?.(index);
335
+ currentIndexRef.current = realIndex;
336
+ setCurrentIndex(realIndex);
337
+
338
+ if (onSnapToItem) {
339
+ onSnapToItem(realIndex);
340
+ }
341
+
342
+ handleLoopReposition(index);
343
+
344
+ if (onMomentumScrollEndProp) {
345
+ onMomentumScrollEndProp(event);
346
+ }
300
347
  },
301
- [onSnapToItem]
348
+ [itemWidth, getRealIndex, onSnapToItem, handleLoopReposition, onMomentumScrollEndProp]
302
349
  );
303
350
 
304
- const needsGestureConfig = !!(
305
- onConfigurePanGesture ||
306
- onTouchStart ||
307
- onTouchEnd
351
+ const handleTouchStart = useCallback(
352
+ (event: any) => {
353
+ if (autoplay) {
354
+ pauseAutoPlay();
355
+ }
356
+ if (onTouchStartProp) {
357
+ onTouchStartProp(event);
358
+ }
359
+ },
360
+ [autoplay, pauseAutoPlay, onTouchStartProp]
308
361
  );
309
362
 
310
- const configurePanGesture = useCallback(
311
- (gesture: CarouselPanGesture) => {
312
- if (onTouchStart) {
313
- gesture.onBegin(() => {
314
- 'worklet';
315
- runOnJS(onTouchStart)();
316
- });
363
+ const handleTouchEnd = useCallback(
364
+ (event: GestureResponderEvent) => {
365
+ if (autoplay && !isAutoplayPausedRef.current) {
366
+ startAutoplay();
317
367
  }
318
- if (onTouchEnd) {
319
- gesture.onFinalize(() => {
320
- 'worklet';
321
- runOnJS(onTouchEnd)();
322
- });
368
+ if (onTouchEndProp) {
369
+ onTouchEndProp(event);
323
370
  }
324
- onConfigurePanGesture?.(gesture);
325
371
  },
326
- [onConfigurePanGesture, onTouchEnd, onTouchStart]
372
+ [autoplay, startAutoplay, onTouchEndProp]
327
373
  );
328
374
 
329
- const clearAutoplayDelay = useCallback(() => {
330
- if (autoplayDelayTimerRef.current) {
331
- clearTimeout(autoplayDelayTimerRef.current);
332
- autoplayDelayTimerRef.current = null;
333
- }
334
- }, []);
375
+ const handleLayout = useCallback(
376
+ (event: LayoutChangeEvent) => {
377
+ const { width } = event.nativeEvent.layout;
378
+ containerWidthRef.current = width;
379
+ setContainerWidth(width);
380
+ setIsLayoutReady(true);
335
381
 
336
- const startAutoplay = useCallback(() => {
337
- clearAutoplayDelay();
338
- if (!autoplay) return;
382
+ if (onLayoutProp) {
383
+ onLayoutProp(event);
384
+ }
385
+ },
386
+ [onLayoutProp]
387
+ );
339
388
 
340
- if (autoplayDelay > 0) {
341
- autoplayDelayTimerRef.current = setTimeout(
342
- () => setIsAutoplayRunning(true),
343
- autoplayDelay
389
+ const getItemLayout = useCallback(
390
+ (data: any, index: number) => {
391
+ if (getItemLayoutProp) {
392
+ return getItemLayoutProp(data, index);
393
+ }
394
+ return {
395
+ length: itemWidth,
396
+ offset: itemWidth * index,
397
+ index,
398
+ };
399
+ },
400
+ [itemWidth, getItemLayoutProp]
401
+ );
402
+
403
+ const renderAnimatedItem = useCallback(
404
+ ({ item, index }: { item: any; index: number }) => {
405
+ const realIndex = getRealIndex(index);
406
+ const hasOpacityAnimation = inactiveSlideOpacity < 1;
407
+ const hasScaleAnimation = inactiveSlideScale < 1;
408
+
409
+ if (!hasOpacityAnimation && !hasScaleAnimation) {
410
+ return (
411
+ <View style={[{ width: itemWidth }, slideStyle]}>
412
+ {renderItem({ item, index: realIndex })}
413
+ </View>
414
+ );
415
+ }
416
+
417
+ return (
418
+ <CarouselItem
419
+ index={index}
420
+ itemWidth={itemWidth}
421
+ scrollX={scrollX}
422
+ inactiveSlideOpacity={inactiveSlideOpacity}
423
+ inactiveSlideScale={inactiveSlideScale}
424
+ slideStyle={slideStyle}
425
+ >
426
+ {renderItem({ item, index: realIndex })}
427
+ </CarouselItem>
344
428
  );
345
- } else {
346
- setIsAutoplayRunning(true);
347
- }
348
- }, [autoplay, autoplayDelay, clearAutoplayDelay]);
429
+ },
430
+ [
431
+ getRealIndex,
432
+ inactiveSlideOpacity,
433
+ inactiveSlideScale,
434
+ itemWidth,
435
+ slideStyle,
436
+ renderItem,
437
+ scrollX,
438
+ ]
439
+ );
349
440
 
350
- const pauseAutoplay = useCallback(() => {
351
- clearAutoplayDelay();
352
- setIsAutoplayRunning(false);
353
- }, [clearAutoplayDelay]);
441
+ const handleKeyExtractor = useCallback(
442
+ (item: any, index: number) => {
443
+ if (keyExtractor) {
444
+ return keyExtractor(item, index);
445
+ }
446
+ return `carousel-item-${index}`;
447
+ },
448
+ [keyExtractor]
449
+ );
450
+
451
+ const emitScrollProp = useCallback(
452
+ (offsetX: number) => {
453
+ if (onScrollProp) {
454
+ onScrollProp({
455
+ nativeEvent: { contentOffset: { x: offsetX, y: 0 } },
456
+ } as NativeSyntheticEvent<NativeScrollEvent>);
457
+ }
458
+ },
459
+ [onScrollProp],
460
+ );
461
+
462
+ const onScrollEvent = useAnimatedScrollHandler({
463
+ onScroll: (event) => {
464
+ scrollX.value = event.contentOffset.x;
465
+ runOnJS(handleScroll)(event.contentOffset.x);
466
+ runOnJS(emitScrollProp)(event.contentOffset.x);
467
+ },
468
+ });
354
469
 
355
470
  useEffect(() => {
356
- if (autoplay) startAutoplay();
357
- else pauseAutoplay();
471
+ if (isLayoutReady && firstItem > 0 && firstItem < data.length) {
472
+ setTimeout(() => {
473
+ snapToItem(firstItem, false, false);
474
+ }, 50);
475
+ }
476
+ }, [isLayoutReady, firstItem, data.length, snapToItem]);
358
477
 
359
- return clearAutoplayDelay;
360
- }, [autoplay, clearAutoplayDelay, pauseAutoplay, startAutoplay]);
478
+ useEffect(() => {
479
+ if (autoplay && data.length > 1) {
480
+ startAutoplay();
481
+ }
482
+
483
+ return () => {
484
+ stopAutoplay();
485
+ };
486
+ }, [autoplay, data.length, startAutoplay, stopAutoplay]);
361
487
 
362
488
  useEffect(() => {
363
- if (apparitionDelay <= 0) return;
489
+ if (apparitionDelay > 0) {
490
+ const timer = setTimeout(() => {
491
+ setIsVisible(true);
492
+ }, apparitionDelay);
364
493
 
365
- const timer = setTimeout(() => setIsVisible(true), apparitionDelay);
366
- return () => clearTimeout(timer);
494
+ return () => clearTimeout(timer);
495
+ }
367
496
  }, [apparitionDelay]);
368
497
 
369
- const suppressNextCallback = useCallback(() => {
370
- suppressUntilRef.current = Date.now() + SUPPRESS_CALLBACK_WINDOW;
371
- }, []);
372
-
373
498
  useImperativeHandle(
374
499
  ref,
375
500
  () => ({
376
- next: options => coreRef.current?.next(options),
377
- prev: options => coreRef.current?.prev(options),
378
- scrollTo: options => coreRef.current?.scrollTo(options),
379
- getCurrentIndex: () => coreRef.current?.getCurrentIndex() ?? 0,
380
-
381
- snapToItem: (index, animated = true, fireCallback = true) => {
382
- if (!fireCallback) suppressNextCallback();
383
- coreRef.current?.scrollTo({ index, animated });
384
- },
385
- snapToNext: (animated = true, fireCallback = true) => {
386
- if (!fireCallback) suppressNextCallback();
387
- coreRef.current?.next({ animated });
388
- },
389
- snapToPrev: (animated = true, fireCallback = true) => {
390
- if (!fireCallback) suppressNextCallback();
391
- coreRef.current?.prev({ animated });
392
- },
501
+ snapToItem,
502
+ snapToNext,
503
+ snapToPrev,
393
504
  startAutoplay,
394
- pauseAutoplay,
395
- stopAutoplay: pauseAutoplay,
505
+ pauseAutoplay: pauseAutoPlay,
506
+ stopAutoplay,
396
507
  }),
397
- [pauseAutoplay, startAutoplay, suppressNextCallback]
508
+ [snapToItem, snapToNext, snapToPrev, startAutoplay, pauseAutoPlay, stopAutoplay] // eslint-disable-line react-hooks/exhaustive-deps
398
509
  );
399
510
 
400
- // A FlatList grows into the space it is given and otherwise takes the height
401
- // of its content. Items are absolutely positioned here, so both halves of
402
- // that have to be restored when the caller sets no size of its own.
403
- const resolvedStyle = hasExplicitSize
404
- ? style
405
- : [
406
- styles.autoSize,
407
- measuredItemSize === null
408
- ? null
409
- : { [isVertical ? 'minWidth' : 'minHeight']: measuredItemSize },
410
- style,
411
- ];
412
-
413
511
  if (!isVisible) {
414
- return <View style={resolvedStyle} testID={testID} />;
512
+ return <View style={[styles.container, style]} />;
415
513
  }
416
514
 
417
- // `layout` and `itemAnimation` are a union in the engine props; both are
418
- // optional here, so the object is assembled before the cast.
419
- const coreProps = {
420
- data,
421
- renderItem: renderCoreItem,
422
- defaultIndex: resolvedDefaultIndex,
423
- orientation,
424
- snapMode: resolvedSnapMode,
425
- autoplay: isAutoplayRunning,
426
- style: resolvedStyle,
427
- onLayout: handleContainerLayout,
428
- onSnapToItem: handleSnapToItem,
429
- ...(keyExtractor ? { keyExtractor } : null),
430
- ...(loop !== undefined ? { loop } : null),
431
- ...(autoplayInterval !== undefined ? { autoplayInterval } : null),
432
- ...(autoplayDirection !== undefined ? { autoplayDirection } : null),
433
- ...(resolvedItemSize ? { itemSize: resolvedItemSize } : null),
434
- ...(scrollEnabled !== undefined ? { scrollEnabled } : null),
435
- ...(overscrollEnabled !== undefined ? { overscrollEnabled } : null),
436
- ...(renderWindowSize !== undefined ? { renderWindowSize } : null),
437
- ...(animation ? { animation } : null),
438
- ...(progress ? { progress } : null),
439
- ...(scrollOffsetValue ? { scrollOffsetValue } : null),
440
- ...(contentContainerStyle ? { contentContainerStyle } : null),
441
- ...(testID ? { testID } : null),
442
- ...(onScrollStart ? { onScrollStart } : null),
443
- ...(layout ? { layout } : null),
444
- ...(resolvedItemAnimation
445
- ? { itemAnimation: resolvedItemAnimation }
446
- : null),
447
- ...(needsProgress ? { onProgressChange: handleProgressChange } : null),
448
- ...(needsGestureConfig
449
- ? { onConfigurePanGesture: configurePanGesture }
450
- : null),
451
- } as CoreCarouselProps<any>;
452
-
453
- return <CoreCarousel ref={coreRef} {...coreProps} />;
515
+ return (
516
+ <View style={[styles.container, style]} onLayout={handleLayout}>
517
+ <AnimatedFlatList
518
+ ref={flatListRef}
519
+ data={dataWithClones}
520
+ renderItem={renderAnimatedItem}
521
+ keyExtractor={handleKeyExtractor}
522
+ horizontal
523
+ showsHorizontalScrollIndicator={false}
524
+ scrollEnabled={scrollEnabled}
525
+ scrollEventThrottle={16}
526
+ onScroll={onScrollEvent}
527
+ onMomentumScrollEnd={handleMomentumScrollEnd}
528
+ onTouchStart={handleTouchStart}
529
+ onTouchEnd={handleTouchEnd}
530
+ snapToInterval={enableSnap ? (snapToInterval as number) : undefined}
531
+ snapToAlignment={enableSnap ? 'start' : undefined}
532
+ decelerationRate={enableSnap ? 'fast' : 'normal'}
533
+ disableIntervalMomentum={disableIntervalMomentum}
534
+ getItemLayout={getItemLayout}
535
+ contentContainerStyle={contentContainerStyle}
536
+ removeClippedSubviews={false}
537
+ initialNumToRender={Math.min(10, dataWithClones.length)}
538
+ maxToRenderPerBatch={5}
539
+ windowSize={5}
540
+ />
541
+ </View>
542
+ );
454
543
  });
455
544
 
456
545
  const styles = StyleSheet.create({
457
- autoSize: {
458
- flexGrow: 1,
459
- flexShrink: 1,
460
- },
461
- slideAuto: {
462
- width: '100%',
463
- },
464
- slideFill: {
465
- width: '100%',
466
- height: '100%',
546
+ container: {
547
+ overflow: 'hidden',
467
548
  },
468
549
  });
469
550
 
470
- CarouselImpl.displayName = 'Carousel';
471
-
472
- type CarouselComponent = <Item>(
473
- props: CarouselProps<Item> & { ref?: React.Ref<CarouselRef> }
474
- ) => React.ReactElement;
551
+ Carousel.displayName = 'Carousel';
475
552
 
476
- export const Carousel = CarouselImpl as CarouselComponent;
477
553
  export default Carousel;
554
+ export { Carousel };
555
+ export type { CarouselProps, CarouselRef };
478
556
 
479
- export type {
480
- CarouselAnimation,
481
- CarouselItemAnimation,
482
- CarouselLayout,
483
- CarouselPanGesture,
484
- CarouselProgressChangeHandler,
485
- CarouselProps,
486
- CarouselRef,
487
- CarouselRenderItem,
488
- CarouselRenderItemInfo,
489
- CarouselScrollToOptions,
490
- CarouselStepOptions,
491
- } from './types';