@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.
Files changed (40) hide show
  1. package/core/LICENSE +21 -0
  2. package/core/README.md +16 -0
  3. package/core/components/Carousel.tsx +28 -0
  4. package/core/components/CarouselLayout.tsx +231 -0
  5. package/core/components/ItemLayout.tsx +215 -0
  6. package/core/components/ItemRenderer.tsx +126 -0
  7. package/core/components/LazyView.tsx +14 -0
  8. package/core/components/ScrollViewGesture.tsx +443 -0
  9. package/core/constants/index.ts +11 -0
  10. package/core/hooks/useAutoPlay.ts +63 -0
  11. package/core/hooks/useCarouselController.tsx +295 -0
  12. package/core/hooks/useCheckMounted.ts +14 -0
  13. package/core/hooks/useCommonVariables.ts +261 -0
  14. package/core/hooks/useInitProps.ts +177 -0
  15. package/core/hooks/useLayoutConfig.ts +35 -0
  16. package/core/hooks/useOffsetX.ts +81 -0
  17. package/core/hooks/useOnProgressChange.ts +33 -0
  18. package/core/hooks/usePanGestureProxy.ts +144 -0
  19. package/core/hooks/usePropsErrorBoundary.ts +41 -0
  20. package/core/hooks/useUpdateGestureConfig.ts +17 -0
  21. package/core/hooks/useVisibleRanges.tsx +141 -0
  22. package/core/layouts/index.tsx +10 -0
  23. package/core/layouts/normal.ts +32 -0
  24. package/core/layouts/parallax.ts +50 -0
  25. package/core/layouts/stack.ts +266 -0
  26. package/core/public-types.ts +209 -0
  27. package/core/store/index.tsx +63 -0
  28. package/core/types.ts +32 -0
  29. package/core/utils/carousel-direction.ts +39 -0
  30. package/core/utils/carousel-math.ts +156 -0
  31. package/core/utils/compute-gesture-translation.ts +29 -0
  32. package/core/utils/compute-offset-if-size-changed.ts +14 -0
  33. package/core/utils/computed-with-auto-fill-data.ts +92 -0
  34. package/core/utils/deal-with-animation.ts +19 -0
  35. package/core/utils/log.ts +12 -0
  36. package/core/utils/sanitize-animation-style.ts +20 -0
  37. package/index.tsx +390 -455
  38. package/package.json +6 -5
  39. package/types.ts +123 -48
  40. package/animation.ts +0 -62
package/index.tsx CHANGED
@@ -9,548 +9,483 @@ import React, {
9
9
  } from 'react';
10
10
  import {
11
11
  Dimensions,
12
- FlatList,
13
- GestureResponderEvent,
14
- LayoutChangeEvent,
15
- NativeScrollEvent,
16
- NativeSyntheticEvent,
17
- Platform,
12
+ I18nManager,
18
13
  StyleSheet,
19
14
  View,
15
+ type LayoutChangeEvent,
16
+ type NativeScrollEvent,
17
+ type NativeSyntheticEvent,
20
18
  } from 'react-native';
21
- import Animated, {
19
+ import {
22
20
  Extrapolation,
23
21
  interpolate,
24
22
  runOnJS,
25
- useAnimatedScrollHandler,
26
- useAnimatedStyle,
27
- useSharedValue,
28
23
  type SharedValue,
29
24
  } 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
- };
91
25
 
92
- const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
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) => {
93
65
  const {
66
+ // engine props
94
67
  data,
95
68
  renderItem,
96
69
  keyExtractor,
97
- firstItem = 0,
98
- itemWidth: itemWidthProp,
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,
99
98
  visibleItem = 1,
100
99
  full = false,
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,
100
+ enableSnap,
101
+ autoplayDelay = 0,
111
102
  inactiveSlideOpacity = 1,
112
103
  inactiveSlideScale = 1,
113
104
  apparitionDelay = 0,
114
- style,
115
105
  slideStyle,
116
- contentContainerStyle,
117
- onSnapToItem,
118
106
  onScrollIndexChanged,
119
- onScroll: onScrollProp,
120
- onMomentumScrollEnd: onMomentumScrollEndProp,
121
- onTouchStart: onTouchStartProp,
122
- onTouchEnd: onTouchEndProp,
123
- onLayout: onLayoutProp,
124
- getItemLayout: getItemLayoutProp,
107
+ onScroll,
108
+ onTouchStart,
109
+ onTouchEnd,
125
110
  } = props;
126
111
 
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;
142
- }
143
- if (itemWidthProp) {
144
- return itemWidthProp;
145
- }
146
- const spacing = visibleItem > 1 ? 10 : 0;
147
- return (containerWidth - spacing * (visibleItem - 1)) / visibleItem;
148
- }, [containerWidth, full, itemWidthProp, visibleItem]);
149
-
150
- const snapToInterval = useMemo(() => {
151
- if (snapToIntervalProp !== undefined) {
152
- return snapToIntervalProp;
153
- }
154
- return itemWidth;
155
- }, [itemWidth, snapToIntervalProp]);
156
-
157
- const dataWithClones = useMemo(() => {
158
- if (!loop || data.length === 0) {
159
- return data;
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
+ }
160
138
  }
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]);
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
+ }
167
145
  }
146
+ }
168
147
 
169
- const result = [...clones, ...data];
170
-
171
- for (let i = 0; i < loopClonesPerSide; i++) {
172
- result.push(data[i % data.length]);
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;
173
155
  }
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;
174
212
 
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;
194
- },
195
- [loop, loopClonesPerSide, data.length]
196
- );
197
-
198
- const getLoopIndex = useCallback(
199
- (realIndex: number) => {
200
- if (!loop) return realIndex;
201
- return realIndex + loopClonesPerSide;
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);
202
240
  },
203
- [loop, loopClonesPerSide]
241
+ [isVertical, onLayout]
204
242
  );
205
243
 
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);
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));
225
251
  }
226
252
  },
227
- [data.length, getLoopIndex, itemWidth, onSnapToItem]
253
+ [hasExplicitSize, isVertical]
228
254
  );
229
255
 
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]
256
+ const slideWrapperStyle = useMemo(
257
+ () => (hasExplicitSize ? styles.slideFill : styles.slideAuto),
258
+ [hasExplicitSize]
236
259
  );
237
260
 
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]
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]
245
268
  );
246
269
 
247
- const stopAutoplay = useCallback(() => {
248
- if (autoplayTimerRef.current) {
249
- clearInterval(autoplayTimerRef.current);
250
- autoplayTimerRef.current = null;
251
- }
252
- }, []);
270
+ const lastIndexRef = useRef(resolvedDefaultIndex);
253
271
 
254
- const pauseAutoPlay = useCallback(() => {
255
- isAutoplayPausedRef.current = true;
256
- stopAutoplay();
257
- }, [stopAutoplay]);
272
+ const needsProgress = !!(onProgressChange || onScroll || onScrollIndexChanged);
258
273
 
259
- const startAutoplay = useCallback(() => {
260
- stopAutoplay();
261
- isAutoplayPausedRef.current = false;
274
+ const handleProgressChange = useCallback(
275
+ (value: number) => {
276
+ onProgressChange?.(value);
277
+ onScroll?.(scrollEvent(value * pageSize));
262
278
 
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);
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);
271
284
  }
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);
307
285
  }
308
286
  },
309
- [loop, data.length, loopClonesPerSide, itemWidth, getLoopIndex]
287
+ [data.length, onProgressChange, onScroll, onScrollIndexChanged, pageSize]
310
288
  );
311
289
 
312
- const handleScroll = useCallback(
313
- (offsetX: number) => {
314
- const index = Math.round(offsetX / itemWidth);
315
- const realIndex = getRealIndex(index);
316
-
317
- if (realIndex !== currentIndexRef.current) {
318
- currentIndexRef.current = realIndex;
319
- setCurrentIndex(realIndex);
320
-
321
- if (onScrollIndexChanged) {
322
- onScrollIndexChanged(realIndex);
323
- }
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);
334
-
335
- currentIndexRef.current = realIndex;
336
- setCurrentIndex(realIndex);
290
+ const handleSnapToItem = useCallback(
291
+ (index: number) => {
292
+ lastIndexRef.current = index;
337
293
 
338
- if (onSnapToItem) {
339
- onSnapToItem(realIndex);
294
+ if (Date.now() < suppressUntilRef.current) {
295
+ suppressUntilRef.current = 0;
296
+ return;
340
297
  }
341
298
 
342
- handleLoopReposition(index);
343
-
344
- if (onMomentumScrollEndProp) {
345
- onMomentumScrollEndProp(event);
346
- }
299
+ onSnapToItem?.(index);
347
300
  },
348
- [itemWidth, getRealIndex, onSnapToItem, handleLoopReposition, onMomentumScrollEndProp]
301
+ [onSnapToItem]
349
302
  );
350
303
 
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]
304
+ const needsGestureConfig = !!(
305
+ onConfigurePanGesture ||
306
+ onTouchStart ||
307
+ onTouchEnd
361
308
  );
362
309
 
363
- const handleTouchEnd = useCallback(
364
- (event: GestureResponderEvent) => {
365
- if (autoplay && !isAutoplayPausedRef.current) {
366
- startAutoplay();
310
+ const configurePanGesture = useCallback(
311
+ (gesture: CarouselPanGesture) => {
312
+ if (onTouchStart) {
313
+ gesture.onBegin(() => {
314
+ 'worklet';
315
+ runOnJS(onTouchStart)();
316
+ });
367
317
  }
368
- if (onTouchEndProp) {
369
- onTouchEndProp(event);
318
+ if (onTouchEnd) {
319
+ gesture.onFinalize(() => {
320
+ 'worklet';
321
+ runOnJS(onTouchEnd)();
322
+ });
370
323
  }
324
+ onConfigurePanGesture?.(gesture);
371
325
  },
372
- [autoplay, startAutoplay, onTouchEndProp]
326
+ [onConfigurePanGesture, onTouchEnd, onTouchStart]
373
327
  );
374
328
 
375
- const handleLayout = useCallback(
376
- (event: LayoutChangeEvent) => {
377
- const { width } = event.nativeEvent.layout;
378
- containerWidthRef.current = width;
379
- setContainerWidth(width);
380
- setIsLayoutReady(true);
381
-
382
- if (onLayoutProp) {
383
- onLayoutProp(event);
384
- }
385
- },
386
- [onLayoutProp]
387
- );
388
-
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
- );
329
+ const clearAutoplayDelay = useCallback(() => {
330
+ if (autoplayDelayTimerRef.current) {
331
+ clearTimeout(autoplayDelayTimerRef.current);
332
+ autoplayDelayTimerRef.current = null;
333
+ }
334
+ }, []);
402
335
 
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
- }
336
+ const startAutoplay = useCallback(() => {
337
+ clearAutoplayDelay();
338
+ if (!autoplay) return;
416
339
 
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>
340
+ if (autoplayDelay > 0) {
341
+ autoplayDelayTimerRef.current = setTimeout(
342
+ () => setIsAutoplayRunning(true),
343
+ autoplayDelay
428
344
  );
429
- },
430
- [
431
- getRealIndex,
432
- inactiveSlideOpacity,
433
- inactiveSlideScale,
434
- itemWidth,
435
- slideStyle,
436
- renderItem,
437
- scrollX,
438
- ]
439
- );
440
-
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
- });
469
-
470
- useEffect(() => {
471
- if (isLayoutReady && firstItem > 0 && firstItem < data.length) {
472
- setTimeout(() => {
473
- snapToItem(firstItem, false, false);
474
- }, 50);
345
+ } else {
346
+ setIsAutoplayRunning(true);
475
347
  }
476
- }, [isLayoutReady, firstItem, data.length, snapToItem]);
348
+ }, [autoplay, autoplayDelay, clearAutoplayDelay]);
349
+
350
+ const pauseAutoplay = useCallback(() => {
351
+ clearAutoplayDelay();
352
+ setIsAutoplayRunning(false);
353
+ }, [clearAutoplayDelay]);
477
354
 
478
355
  useEffect(() => {
479
- if (autoplay && data.length > 1) {
480
- startAutoplay();
481
- }
356
+ if (autoplay) startAutoplay();
357
+ else pauseAutoplay();
482
358
 
483
- return () => {
484
- stopAutoplay();
485
- };
486
- }, [autoplay, data.length, startAutoplay, stopAutoplay]);
359
+ return clearAutoplayDelay;
360
+ }, [autoplay, clearAutoplayDelay, pauseAutoplay, startAutoplay]);
487
361
 
488
362
  useEffect(() => {
489
- if (apparitionDelay > 0) {
490
- const timer = setTimeout(() => {
491
- setIsVisible(true);
492
- }, apparitionDelay);
363
+ if (apparitionDelay <= 0) return;
493
364
 
494
- return () => clearTimeout(timer);
495
- }
365
+ const timer = setTimeout(() => setIsVisible(true), apparitionDelay);
366
+ return () => clearTimeout(timer);
496
367
  }, [apparitionDelay]);
497
368
 
369
+ const suppressNextCallback = useCallback(() => {
370
+ suppressUntilRef.current = Date.now() + SUPPRESS_CALLBACK_WINDOW;
371
+ }, []);
372
+
498
373
  useImperativeHandle(
499
374
  ref,
500
375
  () => ({
501
- snapToItem,
502
- snapToNext,
503
- snapToPrev,
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
+ },
504
393
  startAutoplay,
505
- pauseAutoplay: pauseAutoPlay,
506
- stopAutoplay,
394
+ pauseAutoplay,
395
+ stopAutoplay: pauseAutoplay,
507
396
  }),
508
- [snapToItem, snapToNext, snapToPrev, startAutoplay, pauseAutoPlay, stopAutoplay] // eslint-disable-line react-hooks/exhaustive-deps
397
+ [pauseAutoplay, startAutoplay, suppressNextCallback]
509
398
  );
510
399
 
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
+
511
413
  if (!isVisible) {
512
- return <View style={[styles.container, style]} />;
414
+ return <View style={resolvedStyle} testID={testID} />;
513
415
  }
514
416
 
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
- );
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} />;
543
454
  });
544
455
 
545
456
  const styles = StyleSheet.create({
546
- container: {
547
- overflow: 'hidden',
457
+ autoSize: {
458
+ flexGrow: 1,
459
+ flexShrink: 1,
460
+ },
461
+ slideAuto: {
462
+ width: '100%',
463
+ },
464
+ slideFill: {
465
+ width: '100%',
466
+ height: '100%',
548
467
  },
549
468
  });
550
469
 
551
- Carousel.displayName = 'Carousel';
470
+ CarouselImpl.displayName = 'Carousel';
471
+
472
+ type CarouselComponent = <Item>(
473
+ props: CarouselProps<Item> & { ref?: React.Ref<CarouselRef> }
474
+ ) => React.ReactElement;
552
475
 
476
+ export const Carousel = CarouselImpl as CarouselComponent;
553
477
  export default Carousel;
554
- export { Carousel };
555
- export type { CarouselProps, CarouselRef };
556
478
 
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';