@momo-kits/carousel 0.150.2-phuc.13 → 0.150.3-beta.20

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 (4) hide show
  1. package/animation.ts +148 -17
  2. package/index.tsx +1330 -700
  3. package/package.json +20 -20
  4. package/types.ts +64 -18
package/index.tsx CHANGED
@@ -1,840 +1,1470 @@
1
- import React from 'react';
1
+ import React, {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useImperativeHandle,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
2
9
  import {
3
10
  Animated,
4
- Dimensions,
5
- GestureResponderEvent,
11
+ I18nManager,
6
12
  LayoutChangeEvent,
7
- NativeScrollEvent,
8
13
  NativeSyntheticEvent,
14
+ NativeScrollEvent,
9
15
  Platform,
10
16
  View,
11
- ViewStyle,
12
17
  } from 'react-native';
13
- import { defaultAnimatedStyles, defaultScrollInterpolator } from './animation';
14
- import { CarouselProps, CarouselRef, CarouselState, Position } from './types';
15
- import { Spacing } from '@momo-kits/foundation';
18
+ import {
19
+ defaultAnimatedStyles,
20
+ defaultScrollInterpolator,
21
+ shiftAnimatedStyles,
22
+ stackAnimatedStyles,
23
+ stackScrollInterpolator,
24
+ tinderAnimatedStyles,
25
+ tinderScrollInterpolator,
26
+ } from './animation';
27
+ import { CarouselProps, CarouselRef, Position } from './types';
16
28
 
17
29
  const IS_ANDROID = Platform.OS === 'android';
18
30
  const IS_IOS = Platform.OS === 'ios';
19
- const screenWidth = Dimensions.get('window').width;
20
-
21
- class Carousel extends React.PureComponent<CarouselProps, CarouselState> {
22
- static defaultProps = {
23
- activeSlideAlignment: 'start',
24
- activeSlideOffset: 20,
25
- apparitionDelay: 0,
26
- autoplay: false,
27
- autoplayDelay: 1000,
28
- autoplayInterval: 3000,
29
- callbackOffsetMargin: 5,
30
- containerCustomStyle: {},
31
- contentContainerCustomStyle: {},
32
- enableSnap: true,
33
- firstItem: 0,
34
- hasParallaxImages: false,
35
- loop: false,
36
- loopClonesPerSide: 3,
37
- scrollEnabled: true,
38
- slideStyle: {},
39
- shouldOptimizeUpdates: true,
40
- vertical: false,
41
- isCustomScrollWidth: false,
42
- disableIntervalMomentum: IS_ANDROID,
43
- useExperimentalSnap: IS_ANDROID,
44
- visibleItem: 1,
45
- full: false,
46
- };
31
+ const IS_RTL = I18nManager.isRTL;
32
+
33
+ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
34
+ const {
35
+ activeSlideAlignment = 'center',
36
+ activeSlideOffset = 25,
37
+ apparitionDelay = 0,
38
+ autoplay = false,
39
+ autoplayDelay = 1000,
40
+ autoplayInterval = 3000,
41
+ data = [],
42
+ enableSnap = true,
43
+ firstItem = 0,
44
+ inactiveSlideOpacity = 1,
45
+ inactiveSlideScale = 0.9,
46
+ inactiveSlideShift = 0,
47
+ snapToInterval,
48
+ layout = 'default',
49
+ layoutCardOffset,
50
+ loop = false,
51
+ loopClonesPerSide = 3,
52
+ scrollEnabled: scrollEnabledProp = true,
53
+ useScrollView = false,
54
+ vertical = false,
55
+ useExperimentalSnap = false,
56
+ disableIntervalMomentum = false,
57
+ itemWidth,
58
+ itemHeight,
59
+ sliderWidth,
60
+ sliderHeight,
61
+ renderItem,
62
+ scrollInterpolator,
63
+ slideInterpolatedStyle,
64
+ slideStyle,
65
+ containerCustomStyle,
66
+ contentContainerCustomStyle,
67
+ style,
68
+ keyExtractor,
69
+ getItemLayout: getItemLayoutProp,
70
+ CellRendererComponent,
71
+ onScroll,
72
+ onScrollIndexChanged,
73
+ onSnapToItem,
74
+ onMomentumScrollEnd,
75
+ onLayout,
76
+ onTouchStart,
77
+ onTouchEnd,
78
+ } = props;
79
+
80
+ // State
81
+ const [hideCarousel, setHideCarousel] = useState(!!apparitionDelay);
82
+ const [interpolators, setInterpolators] = useState<
83
+ Animated.AnimatedInterpolation<number>[]
84
+ >([]);
85
+
86
+ // Refs for instance variables
87
+ const mountedRef = useRef(false);
88
+ const carouselRef = useRef<any>(null);
89
+ const scrollPosRef = useRef(new Animated.Value(0));
90
+ const onScrollHandlerRef = useRef<any>(null);
91
+ const positionsRef = useRef<Position[]>([]);
92
+ const currentScrollOffsetRef = useRef(0);
93
+ const scrollEnabledRef = useRef(scrollEnabledProp !== false);
94
+ const activeItemRef = useRef(0);
95
+ const onScrollActiveItemRef = useRef(0);
96
+ const previousFirstItemRef = useRef(0);
97
+ const previousItemsLengthRef = useRef(0);
98
+ const onLayoutInitDoneRef = useRef(false);
99
+
100
+ // Autoplay refs
101
+ const autoplayRef = useRef(false);
102
+ const autoplayingRef = useRef(false);
103
+ const autoplayTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
104
+ const autoplayIntervalRef = useRef<ReturnType<typeof setInterval> | null>(
105
+ null,
106
+ );
107
+ const enableAutoplayTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
108
+ null,
109
+ );
110
+
111
+ // Other timeouts
112
+ const initTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
113
+ const apparitionTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
114
+ null,
115
+ );
116
+ const hackSlideAnimationTimeoutRef = useRef<ReturnType<
117
+ typeof setTimeout
118
+ > | null>(null);
119
+ const snapNoMomentumTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
120
+ null,
121
+ );
122
+ const androidRepositioningTimeoutRef = useRef<ReturnType<
123
+ typeof setTimeout
124
+ > | null>(null);
125
+
126
+ // Helper functions
127
+ const needsScrollView = useCallback(() => {
128
+ return IS_ANDROID
129
+ ? useScrollView ||
130
+ !Animated.FlatList ||
131
+ shouldUseStackLayout() ||
132
+ shouldUseTinderLayout()
133
+ : useScrollView || !Animated.FlatList;
134
+ }, [useScrollView, layout]);
135
+
136
+ const needsRTLAdaptations = useCallback(() => {
137
+ return IS_RTL && IS_ANDROID && !vertical;
138
+ }, [vertical]);
139
+
140
+ const enableLoop = useCallback(() => {
141
+ return enableSnap && loop && data && data.length && data.length > 1;
142
+ }, [enableSnap, loop, data]);
47
143
 
48
- _activeItem;
49
- _onScrollActiveItem;
50
- _previousFirstItem;
51
- _previousItemsLength;
52
- _mounted;
53
- _positions: Position[];
54
- _currentScrollOffset;
55
- _scrollEnabled;
56
- _initTimeout?: ReturnType<typeof setTimeout>;
57
- _apparitionTimeout?: ReturnType<typeof setTimeout>;
58
- _enableAutoplayTimeout?: ReturnType<typeof setTimeout>;
59
- _autoplayTimeout?: ReturnType<typeof setTimeout>;
60
- _snapNoMomentumTimeout?: ReturnType<typeof setTimeout>;
61
- _androidRepositioningTimeout?: ReturnType<typeof setTimeout>;
62
- _scrollPos?: Animated.Value;
63
- _onScrollHandler?: (...args: any[]) => void;
64
- _autoplay?: boolean;
65
- _autoplaying?: boolean;
66
- _autoplayInterval?: ReturnType<typeof setInterval>;
67
- _carouselRef: any;
68
- _onLayoutInitDone?: boolean;
69
-
70
- constructor(props: CarouselProps) {
71
- super(props);
72
-
73
- this.state = {
74
- hideCarousel: !!props.apparitionDelay,
75
- interpolators: [],
76
- containerWidth: screenWidth,
77
- itemWidth: screenWidth - Spacing.L * 2,
78
- };
144
+ const shouldAnimateSlides = useCallback(() => {
145
+ return (
146
+ inactiveSlideOpacity < 1 ||
147
+ inactiveSlideScale < 1 ||
148
+ !!scrollInterpolator ||
149
+ !!slideInterpolatedStyle ||
150
+ shouldUseShiftLayout() ||
151
+ shouldUseStackLayout() ||
152
+ shouldUseTinderLayout()
153
+ );
154
+ }, [
155
+ inactiveSlideOpacity,
156
+ inactiveSlideScale,
157
+ scrollInterpolator,
158
+ slideInterpolatedStyle,
159
+ layout,
160
+ inactiveSlideShift,
161
+ ]);
162
+
163
+ const shouldUseShiftLayout = useCallback(() => {
164
+ return layout === 'default' && inactiveSlideShift !== 0;
165
+ }, [layout, inactiveSlideShift]);
166
+
167
+ const shouldUseStackLayout = useCallback(() => {
168
+ return layout === 'stack';
169
+ }, [layout]);
170
+
171
+ const shouldUseTinderLayout = useCallback(() => {
172
+ return layout === 'tinder';
173
+ }, [layout]);
174
+
175
+ const shouldRepositionScroll = useCallback(
176
+ (index: number) => {
177
+ const dataLength = data && data.length;
178
+ if (
179
+ !enableSnap ||
180
+ !dataLength ||
181
+ !enableLoop() ||
182
+ (index >= loopClonesPerSide && index < dataLength + loopClonesPerSide)
183
+ ) {
184
+ return false;
185
+ }
186
+ return true;
187
+ },
188
+ [data, enableSnap, loopClonesPerSide, enableLoop],
189
+ );
79
190
 
80
- const initialActiveItem = this._getFirstItem(props.firstItem || 0);
81
- this._activeItem = initialActiveItem;
82
- this._onScrollActiveItem = initialActiveItem;
83
- this._previousFirstItem = initialActiveItem;
84
- this._previousItemsLength = initialActiveItem;
85
-
86
- this._mounted = false;
87
- this._positions = [];
88
- this._currentScrollOffset = 0;
89
- this._scrollEnabled = props.scrollEnabled;
90
-
91
- this._getItemLayout = this._getItemLayout.bind(this);
92
- this._getKeyExtractor = this._getKeyExtractor.bind(this);
93
- this._onLayout = this._onLayout.bind(this);
94
- this._onScroll = this._onScroll.bind(this);
95
- this._onMomentumScrollEnd = this._onMomentumScrollEnd.bind(this);
96
- this._onTouchStart = this._onTouchStart.bind(this);
97
- this._onTouchEnd = this._onTouchEnd.bind(this);
98
- this._renderItem = this._renderItem.bind(this);
99
- this._setScrollHandler(props);
100
- }
191
+ const isMultiple = useCallback((x: number, y: number) => {
192
+ return Math.round(Math.round(x / y) / (1 / y)) === Math.round(x);
193
+ }, []);
194
+
195
+ const getCustomData = useCallback(
196
+ (propsOverride = props) => {
197
+ const {
198
+ data: propsData = [],
199
+ loopClonesPerSide: loopClones = loopClonesPerSide,
200
+ } = propsOverride;
201
+ const dataLength = propsData && propsData.length;
202
+
203
+ if (!dataLength) {
204
+ return [];
205
+ }
101
206
 
102
- componentDidMount() {
103
- const { apparitionDelay, autoplay } = this.props;
207
+ if (!enableLoop()) {
208
+ return propsData;
209
+ }
104
210
 
105
- this._mounted = true;
106
- this._initPositionsAndInterpolators();
211
+ let previousItems: any[] = [];
212
+ let nextItems: any[] = [];
107
213
 
108
- // Without 'requestAnimationFrame' or a `0` timeout, images will randomly not be rendered on Android...
109
- this._initTimeout = setTimeout(() => {
110
- if (!this._mounted) {
111
- return;
112
- }
214
+ if (loopClones > dataLength) {
215
+ const dataMultiplier = Math.floor(loopClones / dataLength);
216
+ const remainder = loopClones % dataLength;
113
217
 
114
- const apparitionCallback = () => {
115
- if (apparitionDelay) {
116
- this.setState({ hideCarousel: false });
117
- }
118
- if (autoplay) {
119
- this.startAutoplay();
218
+ for (let i = 0; i < dataMultiplier; i++) {
219
+ previousItems.push(...propsData);
220
+ nextItems.push(...propsData);
120
221
  }
121
- };
122
222
 
123
- if (apparitionDelay) {
124
- this._apparitionTimeout = setTimeout(() => {
125
- apparitionCallback();
126
- }, apparitionDelay);
223
+ previousItems.unshift(...propsData.slice(-remainder));
224
+ nextItems.push(...propsData.slice(0, remainder));
127
225
  } else {
128
- apparitionCallback();
226
+ previousItems = propsData.slice(-loopClones);
227
+ nextItems = propsData.slice(0, loopClones);
129
228
  }
130
- }, 1);
131
- }
132
-
133
- componentDidUpdate(prevProps: CarouselProps) {
134
- const { interpolators } = this.state;
135
- const { firstItem = 0, scrollEnabled } = this.props;
136
- const itemsLength = this._getCustomDataLength(this.props);
137
229
 
138
- if (!itemsLength) {
139
- return;
140
- }
230
+ return previousItems.concat(propsData, nextItems);
231
+ },
232
+ [data, loopClonesPerSide, enableLoop],
233
+ );
234
+
235
+ const getCustomDataLength = useCallback(
236
+ (propsOverride = props) => {
237
+ const {
238
+ data: propsData = [],
239
+ loopClonesPerSide: loopClones = loopClonesPerSide,
240
+ } = propsOverride;
241
+ const dataLength = propsData && propsData.length;
242
+
243
+ if (!dataLength) {
244
+ return 0;
245
+ }
141
246
 
142
- const nextFirstItem = this._getFirstItem(firstItem, this.props);
143
- let nextActiveItem =
144
- typeof this._activeItem !== 'undefined'
145
- ? this._activeItem
146
- : nextFirstItem;
247
+ return enableLoop() ? dataLength + 2 * loopClones : dataLength;
248
+ },
249
+ [data, loopClonesPerSide, enableLoop],
250
+ );
147
251
 
148
- if (nextActiveItem > itemsLength - 1) {
149
- nextActiveItem = itemsLength - 1;
150
- }
252
+ const getCustomIndex = useCallback(
253
+ (index: number, propsOverride = props) => {
254
+ const itemsLength = getCustomDataLength(propsOverride);
151
255
 
152
- if (scrollEnabled !== prevProps.scrollEnabled) {
153
- this._setScrollEnabled(scrollEnabled);
154
- }
256
+ if (!itemsLength || typeof index === 'undefined') {
257
+ return 0;
258
+ }
155
259
 
156
- if (interpolators.length !== itemsLength) {
157
- this._activeItem = nextActiveItem;
158
- this._previousItemsLength = itemsLength;
260
+ return needsRTLAdaptations() ? itemsLength - index - 1 : index;
261
+ },
262
+ [getCustomDataLength, needsRTLAdaptations],
263
+ );
159
264
 
160
- this._initPositionsAndInterpolators(this.props);
161
- } else if (
162
- nextFirstItem !== this._previousFirstItem &&
163
- nextFirstItem !== this._activeItem
164
- ) {
165
- this._activeItem = nextFirstItem;
166
- this._previousFirstItem = nextFirstItem;
167
- this._snapToItem(nextFirstItem, false, true, true);
168
- }
169
-
170
- if (this.props.onScroll !== prevProps.onScroll) {
171
- this._setScrollHandler(this.props);
172
- }
173
- }
265
+ const getDataIndex = useCallback(
266
+ (index: number) => {
267
+ const dataLength = data && data.length;
268
+ if (!enableLoop() || !dataLength) {
269
+ return index;
270
+ }
174
271
 
175
- componentWillUnmount() {
176
- this._mounted = false;
177
- this.stopAutoplay();
178
- if (this._initTimeout != null) clearTimeout(this._initTimeout);
179
- if (this._apparitionTimeout != null) clearTimeout(this._apparitionTimeout);
180
- if (this._enableAutoplayTimeout != null)
181
- clearTimeout(this._enableAutoplayTimeout);
182
- if (this._autoplayTimeout != null) clearTimeout(this._autoplayTimeout);
183
- if (this._snapNoMomentumTimeout != null)
184
- clearTimeout(this._snapNoMomentumTimeout);
185
- if (this._androidRepositioningTimeout != null)
186
- clearTimeout(this._androidRepositioningTimeout);
187
- }
272
+ if (index >= dataLength + loopClonesPerSide) {
273
+ return loopClonesPerSide > dataLength
274
+ ? (index - loopClonesPerSide) % dataLength
275
+ : index - dataLength - loopClonesPerSide;
276
+ } else if (index < loopClonesPerSide) {
277
+ if (loopClonesPerSide > dataLength) {
278
+ const baseDataIndexes: number[] = [];
279
+ const dataIndexes: number[] = [];
280
+ const dataMultiplier = Math.floor(loopClonesPerSide / dataLength);
281
+ const remainder = loopClonesPerSide % dataLength;
282
+
283
+ for (let i = 0; i < dataLength; i++) {
284
+ baseDataIndexes.push(i);
285
+ }
286
+
287
+ for (let j = 0; j < dataMultiplier; j++) {
288
+ dataIndexes.push(...baseDataIndexes);
289
+ }
290
+
291
+ dataIndexes.unshift(...baseDataIndexes.slice(-remainder));
292
+ return dataIndexes[index];
293
+ } else {
294
+ return index + dataLength - loopClonesPerSide;
295
+ }
296
+ } else {
297
+ return index - loopClonesPerSide;
298
+ }
299
+ },
300
+ [data, loopClonesPerSide, enableLoop],
301
+ );
302
+
303
+ const getPositionIndex = useCallback(
304
+ (index: number) => {
305
+ return loop ? index + loopClonesPerSide : index;
306
+ },
307
+ [loop, loopClonesPerSide],
308
+ );
309
+
310
+ const getSnapOffsets = useCallback(() => {
311
+ const offset = getItemMainDimension();
312
+ return [...Array(getCustomDataLength())].map((_, i) => {
313
+ return i * offset;
314
+ });
315
+ }, [getCustomDataLength, itemWidth, itemHeight, vertical]);
188
316
 
189
- _setScrollHandler(props: CarouselProps) {
190
- const scrollEventConfig = {
191
- listener: this._onScroll,
192
- useNativeDriver: true,
193
- };
194
- this._scrollPos = new Animated.Value(0);
195
- const argMapping = [
196
- { nativeEvent: { contentOffset: { x: this._scrollPos } } },
197
- ];
317
+ const getFirstItem = useCallback(
318
+ (index: number, propsOverride = props) => {
319
+ const { loopClonesPerSide: loopClones = loopClonesPerSide } =
320
+ propsOverride;
321
+ const itemsLength = getCustomDataLength(propsOverride);
198
322
 
199
- if (props.onScroll && Array.isArray(props.onScroll._argMapping)) {
200
- argMapping.pop();
201
- const [argMap] = props.onScroll._argMapping;
202
- if (argMap && argMap.nativeEvent && argMap.nativeEvent.contentOffset) {
203
- this._scrollPos =
204
- argMap.nativeEvent.contentOffset.x ||
205
- argMap.nativeEvent.contentOffset.y ||
206
- this._scrollPos;
323
+ if (!itemsLength || index > itemsLength - 1 || index < 0) {
324
+ return 0;
207
325
  }
208
- argMapping.push(...props.onScroll._argMapping);
209
- }
210
- this._onScrollHandler = Animated.event(argMapping, scrollEventConfig);
211
- }
212
-
213
- _enableLoop() {
214
- const { data, enableSnap, loop } = this.props;
215
- return enableSnap && loop && data && data.length && data.length > 1;
216
- }
217
326
 
218
- _shouldAnimateSlides(props = this.props) {
219
- const { inactiveSlideOpacity = 1, inactiveSlideScale = 1 } = props;
220
- return inactiveSlideOpacity < 1 || inactiveSlideScale < 1;
221
- }
327
+ return enableLoop() ? index + loopClones : index;
328
+ },
329
+ [getCustomDataLength, enableLoop, loopClonesPerSide],
330
+ );
222
331
 
223
- _shouldRepositionScroll(index: number) {
224
- const { data, enableSnap, loopClonesPerSide = 3 } = this.props;
225
- const dataLength = data && data.length;
226
- return !(
227
- !enableSnap ||
228
- !dataLength ||
229
- !this._enableLoop() ||
230
- (index >= loopClonesPerSide && index < dataLength + loopClonesPerSide)
332
+ const getWrappedRef = useCallback(() => {
333
+ if (
334
+ carouselRef.current &&
335
+ ((needsScrollView() && carouselRef.current.scrollTo) ||
336
+ (!needsScrollView() && carouselRef.current.scrollToOffset))
337
+ ) {
338
+ return carouselRef.current;
339
+ }
340
+ return (
341
+ carouselRef.current &&
342
+ carouselRef.current.getNode &&
343
+ carouselRef.current.getNode()
231
344
  );
232
- }
345
+ }, [needsScrollView]);
233
346
 
234
- _isMultiple(x: number, y: number) {
235
- return Math.round(Math.round(x / y) / (1 / y)) === Math.round(x);
236
- }
347
+ const getScrollEnabled = useCallback(() => {
348
+ return scrollEnabledRef.current;
349
+ }, []);
237
350
 
238
- _getCustomData(props = this.props) {
239
- const { data, loopClonesPerSide = 3 } = props;
240
- const dataLength = data && data.length;
241
-
242
- if (!dataLength) {
243
- return [];
244
- }
351
+ const setScrollEnabled = useCallback(
352
+ (enabled = true) => {
353
+ const wrappedRef = getWrappedRef();
245
354
 
246
- if (!this._enableLoop()) {
247
- return data;
248
- }
355
+ if (!wrappedRef || !wrappedRef.setNativeProps) {
356
+ return;
357
+ }
249
358
 
250
- let previousItems = [];
251
- let nextItems = [];
359
+ wrappedRef.setNativeProps({ scrollEnabled: enabled });
360
+ scrollEnabledRef.current = enabled;
361
+ },
362
+ [getWrappedRef],
363
+ );
252
364
 
253
- if (loopClonesPerSide > dataLength) {
254
- const dataMultiplier = Math.floor(loopClonesPerSide / dataLength);
255
- const remainder = loopClonesPerSide % dataLength;
365
+ const getItemMainDimension = useCallback(() => {
366
+ return vertical ? itemHeight : itemWidth;
367
+ }, [vertical, itemHeight, itemWidth]);
256
368
 
257
- for (let i = 0; i < dataMultiplier; i++) {
258
- previousItems.push(...data);
259
- nextItems.push(...data);
369
+ const getItemScrollOffset = useCallback((index: number) => {
370
+ return (
371
+ positionsRef.current &&
372
+ positionsRef.current[index] &&
373
+ positionsRef.current[index].start
374
+ );
375
+ }, []);
376
+
377
+ const getItemLayout = useCallback(
378
+ (_: any, index: number) => {
379
+ const itemMainDimension = getItemMainDimension();
380
+ return {
381
+ index,
382
+ length: itemMainDimension,
383
+ offset: itemMainDimension * index,
384
+ };
385
+ },
386
+ [getItemMainDimension],
387
+ );
388
+
389
+ const getCellRendererComponent = useCallback(
390
+ ({ children, index, style: cellStyle, ...cellProps }: any) => {
391
+ const customStyle = [
392
+ cellStyle,
393
+ !IS_ANDROID ? { zIndex: getCustomDataLength() - index } : {},
394
+ ];
395
+
396
+ return (
397
+ <View style={customStyle} key={index} {...cellProps}>
398
+ {children}
399
+ </View>
400
+ );
401
+ },
402
+ [getCustomDataLength],
403
+ );
404
+
405
+ const getKeyExtractor = useCallback(
406
+ (_: any, index: number) => {
407
+ return needsScrollView()
408
+ ? `scrollview-item-${index}`
409
+ : `flatlist-item-${index}`;
410
+ },
411
+ [needsScrollView],
412
+ );
413
+
414
+ const getScrollOffset = useCallback(
415
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
416
+ return (
417
+ (event &&
418
+ event.nativeEvent &&
419
+ event.nativeEvent.contentOffset &&
420
+ event.nativeEvent.contentOffset[vertical ? 'y' : 'x']) ||
421
+ 0
422
+ );
423
+ },
424
+ [vertical],
425
+ );
426
+
427
+ const getContainerInnerMargin = useCallback(
428
+ (opposite = false) => {
429
+ if (
430
+ (activeSlideAlignment === 'start' && !opposite) ||
431
+ (activeSlideAlignment === 'end' && opposite)
432
+ ) {
433
+ return 0;
434
+ } else if (
435
+ (activeSlideAlignment === 'end' && !opposite) ||
436
+ (activeSlideAlignment === 'start' && opposite)
437
+ ) {
438
+ return vertical ? sliderHeight - itemHeight : sliderWidth - itemWidth;
439
+ } else {
440
+ return vertical
441
+ ? (sliderHeight - itemHeight) / 2
442
+ : (sliderWidth - itemWidth) / 2;
443
+ }
444
+ },
445
+ [
446
+ activeSlideAlignment,
447
+ vertical,
448
+ sliderHeight,
449
+ itemHeight,
450
+ sliderWidth,
451
+ itemWidth,
452
+ ],
453
+ );
454
+
455
+ const getActiveSlideOffset = useCallback(() => {
456
+ const itemMainDimension = getItemMainDimension();
457
+ const minOffset = 10;
458
+ return itemMainDimension / 2 - activeSlideOffset >= minOffset
459
+ ? activeSlideOffset
460
+ : minOffset;
461
+ }, [getItemMainDimension, activeSlideOffset]);
462
+
463
+ const getActiveItem = useCallback(
464
+ (offset: number) => {
465
+ const itemMainDimension = getItemMainDimension();
466
+ const center = offset + itemMainDimension / 2;
467
+ const activeOffset = getActiveSlideOffset();
468
+ const lastIndex = positionsRef.current.length - 1;
469
+ let itemIndex;
470
+
471
+ if (offset <= 0) {
472
+ return 0;
260
473
  }
261
474
 
262
- previousItems.unshift(...data.slice(-remainder));
263
- nextItems.push(...data.slice(0, remainder));
264
- } else {
265
- previousItems = data.slice(-loopClonesPerSide);
266
- nextItems = data.slice(0, loopClonesPerSide);
267
- }
268
-
269
- return previousItems.concat(data, nextItems);
270
- }
271
-
272
- _getCustomDataLength(props = this.props) {
273
- const { data, loopClonesPerSide = 3 } = props;
274
- const dataLength = data && data.length;
475
+ if (
476
+ positionsRef.current[lastIndex] &&
477
+ offset >= positionsRef.current[lastIndex].start
478
+ ) {
479
+ return lastIndex;
480
+ }
275
481
 
276
- if (!dataLength) {
277
- return 0;
278
- }
482
+ for (let i = 0; i < positionsRef.current.length; i++) {
483
+ const { start, end } = positionsRef.current[i];
484
+ if (center + activeOffset >= start && center - activeOffset <= end) {
485
+ itemIndex = i;
486
+ break;
487
+ }
488
+ }
279
489
 
280
- return this._enableLoop() ? dataLength + 2 * loopClonesPerSide : dataLength;
281
- }
490
+ return itemIndex || 0;
491
+ },
492
+ [getItemMainDimension, getActiveSlideOffset],
493
+ );
494
+
495
+ const getSlideInterpolatedStyle = useCallback(
496
+ (index: number, animatedValue: Animated.AnimatedInterpolation<number>) => {
497
+ if (slideInterpolatedStyle) {
498
+ return slideInterpolatedStyle(index, animatedValue, props);
499
+ } else if (shouldUseTinderLayout()) {
500
+ return tinderAnimatedStyles(
501
+ index,
502
+ animatedValue,
503
+ props,
504
+ layoutCardOffset,
505
+ );
506
+ } else if (shouldUseStackLayout()) {
507
+ return stackAnimatedStyles(
508
+ index,
509
+ animatedValue,
510
+ props,
511
+ layoutCardOffset,
512
+ );
513
+ } else if (shouldUseShiftLayout()) {
514
+ return shiftAnimatedStyles(index, animatedValue, props);
515
+ } else {
516
+ return defaultAnimatedStyles(index, animatedValue, props);
517
+ }
518
+ },
519
+ [
520
+ slideInterpolatedStyle,
521
+ shouldUseTinderLayout,
522
+ shouldUseStackLayout,
523
+ shouldUseShiftLayout,
524
+ layoutCardOffset,
525
+ props,
526
+ ],
527
+ );
528
+
529
+ const initPositionsAndInterpolators = useCallback(
530
+ (propsOverride = props) => {
531
+ const { data: propsData = [], scrollInterpolator: scrollInterp } =
532
+ propsOverride;
533
+ const itemMainDimension = getItemMainDimension();
534
+
535
+ if (!propsData || !propsData.length) {
536
+ return;
537
+ }
282
538
 
283
- _getCustomIndex(index: number, props = this.props) {
284
- const itemsLength = this._getCustomDataLength(props);
539
+ // Guard against invalid dimensions that would cause NaN in interpolators
540
+ if (
541
+ !itemMainDimension ||
542
+ itemMainDimension <= 0 ||
543
+ !isFinite(itemMainDimension)
544
+ ) {
545
+ return;
546
+ }
285
547
 
286
- if (!itemsLength || typeof index === 'undefined') {
287
- return 0;
288
- }
548
+ const newInterpolators: Animated.AnimatedInterpolation<number>[] = [];
549
+ positionsRef.current = [];
289
550
 
290
- return index;
291
- }
551
+ getCustomData(propsOverride).forEach((_itemData: any, index: number) => {
552
+ const _index = getCustomIndex(index, propsOverride);
553
+ let animatedValue: Animated.AnimatedInterpolation<number>;
292
554
 
293
- _getDataIndex(index: number) {
294
- const { data, loopClonesPerSide = 3 } = this.props;
295
- const dataLength = data && data.length;
296
- if (!this._enableLoop() || !dataLength) {
297
- return index;
298
- }
555
+ positionsRef.current[index] = {
556
+ start: index * itemMainDimension,
557
+ end: index * itemMainDimension + itemMainDimension,
558
+ };
299
559
 
300
- if (index >= dataLength + loopClonesPerSide) {
301
- return loopClonesPerSide > dataLength
302
- ? (index - loopClonesPerSide) % dataLength
303
- : index - dataLength - loopClonesPerSide;
304
- } else if (index < loopClonesPerSide) {
305
- if (loopClonesPerSide > dataLength) {
306
- const baseDataIndexes = [];
307
- const dataIndexes = [];
308
- const dataMultiplier = Math.floor(loopClonesPerSide / dataLength);
309
- const remainder = loopClonesPerSide % dataLength;
310
-
311
- for (let i = 0; i < dataLength; i++) {
312
- baseDataIndexes.push(i);
560
+ if (!shouldAnimateSlides() || !scrollPosRef.current) {
561
+ animatedValue = new Animated.Value(1) as any;
562
+ } else {
563
+ let interpolator;
564
+
565
+ if (scrollInterp) {
566
+ interpolator = scrollInterp(_index, propsOverride);
567
+ } else if (shouldUseStackLayout()) {
568
+ interpolator = stackScrollInterpolator(_index, propsOverride);
569
+ } else if (shouldUseTinderLayout()) {
570
+ interpolator = tinderScrollInterpolator(_index, propsOverride);
571
+ }
572
+
573
+ if (
574
+ !interpolator ||
575
+ !interpolator.inputRange ||
576
+ !interpolator.outputRange
577
+ ) {
578
+ interpolator = defaultScrollInterpolator(_index, propsOverride);
579
+ }
580
+
581
+ // Validate interpolator ranges to prevent NaN errors
582
+ const hasValidRange =
583
+ interpolator.inputRange &&
584
+ interpolator.inputRange.every((val: number) => isFinite(val)) &&
585
+ interpolator.outputRange &&
586
+ interpolator.outputRange.every((val: number) => isFinite(val));
587
+
588
+ if (!hasValidRange) {
589
+ animatedValue = new Animated.Value(1) as any;
590
+ } else {
591
+ animatedValue = scrollPosRef.current.interpolate({
592
+ ...interpolator,
593
+ extrapolate: 'clamp',
594
+ });
595
+ }
313
596
  }
314
597
 
315
- for (let j = 0; j < dataMultiplier; j++) {
316
- dataIndexes.push(...baseDataIndexes);
317
- }
598
+ newInterpolators.push(animatedValue);
599
+ });
600
+
601
+ setInterpolators(newInterpolators);
602
+ },
603
+ [
604
+ data,
605
+ getItemMainDimension,
606
+ getCustomData,
607
+ getCustomIndex,
608
+ shouldAnimateSlides,
609
+ shouldUseStackLayout,
610
+ shouldUseTinderLayout,
611
+ scrollInterpolator,
612
+ ],
613
+ );
614
+
615
+ const hackActiveSlideAnimation = useCallback(
616
+ (index: number, scrollValue = 1) => {
617
+ const offset = getItemScrollOffset(index);
318
618
 
319
- dataIndexes.unshift(...baseDataIndexes.slice(-remainder));
320
- return dataIndexes[index];
321
- } else {
322
- return index + dataLength - loopClonesPerSide;
619
+ if (
620
+ !mountedRef.current ||
621
+ !carouselRef.current ||
622
+ typeof offset === 'undefined'
623
+ ) {
624
+ return;
323
625
  }
324
- } else {
325
- return index - loopClonesPerSide;
326
- }
327
- }
328
-
329
- _getFirstItem(index: number, props = this.props) {
330
- const { loopClonesPerSide = 3 } = props;
331
- const itemsLength = this._getCustomDataLength(props);
332
626
 
333
- if (!itemsLength || index > itemsLength - 1 || index < 0) {
334
- return 0;
335
- }
627
+ const multiplier = currentScrollOffsetRef.current === 0 ? 1 : -1;
628
+ const scrollDelta = scrollValue * multiplier;
336
629
 
337
- return this._enableLoop() ? index + loopClonesPerSide : index;
338
- }
630
+ scrollTo({ offset: offset + scrollDelta, animated: false });
339
631
 
340
- _getWrappedRef() {
341
- return this._carouselRef;
342
- }
343
-
344
- _getScrollEnabled() {
345
- return this._scrollEnabled;
346
- }
632
+ if (hackSlideAnimationTimeoutRef.current) {
633
+ clearTimeout(hackSlideAnimationTimeoutRef.current);
634
+ }
635
+ hackSlideAnimationTimeoutRef.current = setTimeout(() => {
636
+ scrollTo({ offset, animated: false });
637
+ }, 1);
638
+ },
639
+ [getItemScrollOffset],
640
+ );
641
+
642
+ const repositionScroll = useCallback(
643
+ (index: number, animated = false) => {
644
+ const dataLength = data && data.length;
645
+
646
+ if (typeof index === 'undefined' || !shouldRepositionScroll(index)) {
647
+ return;
648
+ }
347
649
 
348
- _setScrollEnabled(scrollEnabled = true) {
349
- this._scrollEnabled = scrollEnabled;
350
- }
650
+ let repositionTo = index;
351
651
 
352
- _getItemMainDimension() {
353
- const { itemWidth } = this.state;
354
- const { full } = this.props;
355
- return full ? itemWidth : itemWidth + Spacing.S;
356
- }
652
+ if (index >= dataLength + loopClonesPerSide) {
653
+ repositionTo = index - dataLength;
654
+ } else if (index < loopClonesPerSide) {
655
+ repositionTo = index + dataLength;
656
+ }
357
657
 
358
- _getItemScrollOffset(index: number) {
359
- return (
360
- this._positions && this._positions[index] && this._positions[index].start
361
- );
362
- }
658
+ snapToItem(repositionTo, animated, false);
659
+ },
660
+ [data, loopClonesPerSide, shouldRepositionScroll],
661
+ );
363
662
 
364
- _getItemLayout(_: any, index: number) {
365
- const itemMainDimension = this._getItemMainDimension();
366
- return {
663
+ const scrollTo = useCallback(
664
+ ({
665
+ offset,
367
666
  index,
368
- length: itemMainDimension,
369
- offset: itemMainDimension * index,
370
- };
371
- }
667
+ animated = true,
668
+ }: {
669
+ offset?: number;
670
+ index?: number;
671
+ animated?: boolean;
672
+ }) => {
673
+ const wrappedRef = getWrappedRef();
674
+ if (
675
+ !mountedRef.current ||
676
+ !wrappedRef ||
677
+ (typeof offset === 'undefined' && typeof index === 'undefined')
678
+ ) {
679
+ return;
680
+ }
372
681
 
373
- _getKeyExtractor(_: any, index: any) {
374
- return `flatlist-item-${index}`;
375
- }
682
+ let scrollToOffset;
683
+ if (typeof index !== 'undefined') {
684
+ scrollToOffset = getItemScrollOffset(index);
685
+ } else {
686
+ scrollToOffset = offset;
687
+ }
376
688
 
377
- _getScrollOffset(event: NativeSyntheticEvent<NativeScrollEvent>) {
378
- return event.nativeEvent.contentOffset.x;
379
- }
689
+ if (typeof scrollToOffset === 'undefined') {
690
+ return;
691
+ }
380
692
 
381
- _getActiveSlideOffset() {
382
- const { activeSlideOffset = 0 } = this.props;
383
- const itemMainDimension = this._getItemMainDimension();
384
- const minOffset = 10;
385
- return itemMainDimension / 2 - activeSlideOffset >= minOffset
386
- ? activeSlideOffset
387
- : minOffset;
388
- }
693
+ const options = needsScrollView()
694
+ ? {
695
+ x: vertical ? 0 : scrollToOffset,
696
+ y: vertical ? scrollToOffset : 0,
697
+ animated,
698
+ }
699
+ : {
700
+ offset: scrollToOffset,
701
+ animated,
702
+ };
703
+
704
+ if (needsScrollView()) {
705
+ wrappedRef.scrollTo(options);
706
+ } else {
707
+ wrappedRef.scrollToOffset(options);
708
+ }
709
+ },
710
+ [getWrappedRef, getItemScrollOffset, needsScrollView, vertical],
711
+ );
712
+
713
+ const handleTouchStart = useCallback(
714
+ (event: any) => {
715
+ if (getScrollEnabled() !== false && autoplayingRef.current) {
716
+ pauseAutoPlay();
717
+ }
389
718
 
390
- _getActiveItem(offset: number) {
391
- const itemMainDimension = this._getItemMainDimension();
392
- const center = offset + itemMainDimension / 2;
393
- const activeSlideOffset = this._getActiveSlideOffset();
394
- const lastIndex = this._positions.length - 1;
395
- let itemIndex;
719
+ onTouchStart && onTouchStart(event);
720
+ },
721
+ [getScrollEnabled, onTouchStart],
722
+ );
396
723
 
397
- if (offset <= 0) {
398
- return 0;
399
- }
724
+ const handleTouchEnd = useCallback(
725
+ (event: any) => {
726
+ if (
727
+ getScrollEnabled() &&
728
+ autoplayRef.current &&
729
+ !autoplayingRef.current
730
+ ) {
731
+ startAutoplay();
732
+ }
400
733
 
401
- if (
402
- this._positions[lastIndex] &&
403
- offset >= this._positions[lastIndex].start
404
- ) {
405
- return lastIndex;
406
- }
734
+ onTouchEnd && onTouchEnd(event);
735
+ },
736
+ [getScrollEnabled, onTouchEnd],
737
+ );
738
+
739
+ const handleScroll = useCallback(
740
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
741
+ const scrollOffset = event
742
+ ? getScrollOffset(event)
743
+ : currentScrollOffsetRef.current;
744
+ const nextActiveItem = getActiveItem(scrollOffset);
745
+ const dataLength = getCustomDataLength();
746
+ const lastItemScrollOffset = getItemScrollOffset(dataLength - 1);
747
+
748
+ currentScrollOffsetRef.current = scrollOffset;
749
+
750
+ if (nextActiveItem !== onScrollActiveItemRef.current) {
751
+ onScrollActiveItemRef.current = nextActiveItem;
752
+ onScrollIndexChanged &&
753
+ onScrollIndexChanged(getDataIndex(nextActiveItem));
754
+ }
407
755
 
408
- for (let i = 0; i < this._positions.length; i++) {
409
- const { start, end } = this._positions[i];
410
756
  if (
411
- center + activeSlideOffset >= start &&
412
- center - activeSlideOffset <= end
757
+ (IS_IOS && scrollOffset >= lastItemScrollOffset) ||
758
+ (IS_ANDROID &&
759
+ Math.floor(scrollOffset) >= Math.floor(lastItemScrollOffset))
413
760
  ) {
414
- itemIndex = i;
415
- break;
761
+ activeItemRef.current = nextActiveItem;
762
+ repositionScroll(nextActiveItem);
416
763
  }
417
- }
418
764
 
419
- return itemIndex || 0;
420
- }
765
+ if (typeof onScroll === 'function' && event) {
766
+ onScroll(event);
767
+ }
768
+ },
769
+ [
770
+ getScrollOffset,
771
+ getActiveItem,
772
+ getCustomDataLength,
773
+ getItemScrollOffset,
774
+ getDataIndex,
775
+ onScrollIndexChanged,
776
+ onScroll,
777
+ repositionScroll,
778
+ ],
779
+ );
780
+
781
+ const handleMomentumScrollEnd = useCallback(
782
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
783
+ const scrollOffset = event
784
+ ? getScrollOffset(event)
785
+ : currentScrollOffsetRef.current;
786
+ const nextActiveItem = getActiveItem(scrollOffset);
787
+ const hasSnapped = isMultiple(
788
+ scrollOffset,
789
+ vertical ? itemHeight : itemWidth,
790
+ );
791
+
792
+ if (nextActiveItem !== activeItemRef.current) {
793
+ activeItemRef.current = nextActiveItem;
794
+ onSnapToItem && onSnapToItem(getDataIndex(nextActiveItem));
795
+
796
+ if (hasSnapped && IS_ANDROID) {
797
+ repositionScroll(nextActiveItem);
798
+ } else if (IS_IOS) {
799
+ repositionScroll(nextActiveItem);
800
+ }
801
+ }
421
802
 
422
- _initPositionsAndInterpolators(props = this.props) {
423
- const { data } = props;
424
- const itemMainDimension = this._getItemMainDimension();
803
+ onMomentumScrollEnd && onMomentumScrollEnd(event);
425
804
 
426
- if (!data || !data.length) {
427
- return;
428
- }
805
+ if (IS_ANDROID && autoplayRef.current && !autoplayingRef.current) {
806
+ if (enableAutoplayTimeoutRef.current) {
807
+ clearTimeout(enableAutoplayTimeoutRef.current);
808
+ }
809
+ enableAutoplayTimeoutRef.current = setTimeout(() => {
810
+ startAutoplay();
811
+ }, autoplayDelay);
812
+ }
813
+ },
814
+ [
815
+ getScrollOffset,
816
+ getActiveItem,
817
+ isMultiple,
818
+ vertical,
819
+ itemHeight,
820
+ itemWidth,
821
+ onSnapToItem,
822
+ getDataIndex,
823
+ repositionScroll,
824
+ onMomentumScrollEnd,
825
+ autoplayDelay,
826
+ ],
827
+ );
828
+
829
+ const handleLayout = useCallback(
830
+ (event: LayoutChangeEvent) => {
831
+ if (onLayoutInitDoneRef.current) {
832
+ initPositionsAndInterpolators();
833
+ snapToItem(activeItemRef.current, false, false, true);
834
+ } else {
835
+ onLayoutInitDoneRef.current = true;
836
+ }
429
837
 
430
- const interpolators: any[] = [];
431
- this._positions = [];
838
+ onLayout && onLayout(event);
839
+ },
840
+ [initPositionsAndInterpolators, onLayout],
841
+ );
842
+
843
+ const snapToItem = useCallback(
844
+ (
845
+ index: number,
846
+ animated = true,
847
+ fireCallback = true,
848
+ forceScrollTo = false,
849
+ ) => {
850
+ const itemsLength = getCustomDataLength();
851
+ const wrappedRef = getWrappedRef();
852
+ if (!itemsLength || !wrappedRef) {
853
+ return;
854
+ }
432
855
 
433
- this._getCustomData(props).forEach((_itemData, index) => {
434
- const _index = this._getCustomIndex(index, props);
435
- let animatedValue;
856
+ if (!index || index < 0) {
857
+ index = 0;
858
+ } else if (itemsLength > 0 && index >= itemsLength) {
859
+ index = itemsLength - 1;
860
+ }
436
861
 
437
- this._positions[index] = {
438
- start: index * itemMainDimension,
439
- end: index * itemMainDimension + itemMainDimension,
440
- };
862
+ if (index === activeItemRef.current && !forceScrollTo) {
863
+ return;
864
+ }
441
865
 
442
- if (!this._shouldAnimateSlides(props) || !this._scrollPos) {
443
- animatedValue = new Animated.Value(1);
444
- } else {
445
- let interpolator = defaultScrollInterpolator(
446
- _index,
447
- this.state.itemWidth,
448
- );
866
+ const offset = getItemScrollOffset(index);
449
867
 
450
- animatedValue = this._scrollPos.interpolate({
451
- ...interpolator,
452
- extrapolate: 'clamp',
453
- });
868
+ if (offset === undefined) {
869
+ return;
454
870
  }
455
871
 
456
- interpolators.push(animatedValue);
457
- });
872
+ scrollTo({ offset, animated });
458
873
 
459
- this.setState({ interpolators });
460
- }
874
+ const requiresManualTrigger = !animated || IS_ANDROID;
875
+ if (requiresManualTrigger) {
876
+ activeItemRef.current = index;
461
877
 
462
- _repositionScroll(index: number, animated = false) {
463
- const { data, loopClonesPerSide = 3 } = this.props;
464
- const dataLength = data && data.length;
878
+ if (fireCallback) {
879
+ onSnapToItem && onSnapToItem(getDataIndex(index));
880
+ }
465
881
 
466
- if (typeof index === 'undefined' || !this._shouldRepositionScroll(index)) {
882
+ if (IS_ANDROID && shouldRepositionScroll(index)) {
883
+ if (animated) {
884
+ if (androidRepositioningTimeoutRef.current) {
885
+ clearTimeout(androidRepositioningTimeoutRef.current);
886
+ }
887
+ androidRepositioningTimeoutRef.current = setTimeout(() => {
888
+ repositionScroll(index, false);
889
+ }, 400);
890
+ } else {
891
+ repositionScroll(index);
892
+ }
893
+ }
894
+ }
895
+ },
896
+ [
897
+ getCustomDataLength,
898
+ getWrappedRef,
899
+ getItemScrollOffset,
900
+ scrollTo,
901
+ onSnapToItem,
902
+ getDataIndex,
903
+ shouldRepositionScroll,
904
+ repositionScroll,
905
+ ],
906
+ );
907
+
908
+ const startAutoplay = useCallback(() => {
909
+ autoplayRef.current = true;
910
+
911
+ if (autoplayingRef.current) {
467
912
  return;
468
913
  }
469
914
 
470
- let repositionTo = index;
471
-
472
- if (index >= dataLength + loopClonesPerSide) {
473
- repositionTo = index - dataLength;
474
- } else if (index < loopClonesPerSide) {
475
- repositionTo = index + dataLength;
915
+ if (autoplayTimeoutRef.current) {
916
+ clearTimeout(autoplayTimeoutRef.current);
476
917
  }
918
+ autoplayTimeoutRef.current = setTimeout(() => {
919
+ autoplayingRef.current = true;
920
+ autoplayIntervalRef.current = setInterval(() => {
921
+ if (autoplayingRef.current) {
922
+ snapToNext();
923
+ }
924
+ }, autoplayInterval);
925
+ }, autoplayDelay);
926
+ }, [autoplayInterval, autoplayDelay]);
477
927
 
478
- this._snapToItem(repositionTo, animated, false);
479
- }
480
-
481
- _onTouchStart(event: any) {
482
- const { onTouchStart } = this.props;
483
-
484
- if (this._getScrollEnabled() !== false && this._autoplaying) {
485
- this.pauseAutoPlay();
928
+ const pauseAutoPlay = useCallback(() => {
929
+ autoplayingRef.current = false;
930
+ if (autoplayTimeoutRef.current) {
931
+ clearTimeout(autoplayTimeoutRef.current);
486
932
  }
487
-
488
- onTouchStart && onTouchStart(event);
489
- }
490
-
491
- _onTouchEnd(event: GestureResponderEvent) {
492
- const { onTouchEnd } = this.props;
493
-
494
- if (
495
- this._getScrollEnabled() !== false &&
496
- this._autoplay &&
497
- !this._autoplaying
498
- ) {
499
- this.startAutoplay();
933
+ if (enableAutoplayTimeoutRef.current) {
934
+ clearTimeout(enableAutoplayTimeoutRef.current);
935
+ }
936
+ if (autoplayIntervalRef.current) {
937
+ clearInterval(autoplayIntervalRef.current);
500
938
  }
939
+ }, []);
501
940
 
502
- onTouchEnd && onTouchEnd(event);
503
- }
941
+ const stopAutoplay = useCallback(() => {
942
+ autoplayRef.current = false;
943
+ pauseAutoPlay();
944
+ }, [pauseAutoPlay]);
945
+
946
+ const snapToItemPublic = useCallback(
947
+ (index: number, animated = true, fireCallback = true) => {
948
+ if (!index || index < 0) {
949
+ index = 0;
950
+ }
504
951
 
505
- _onScroll(event: NativeSyntheticEvent<NativeScrollEvent>) {
506
- const { onScroll, onScrollIndexChanged, onSnapToItem } = this.props;
507
- const scrollOffset = event
508
- ? this._getScrollOffset(event)
509
- : this._currentScrollOffset;
510
- const nextActiveItem = this._getActiveItem(scrollOffset);
511
- const dataLength = this._getCustomDataLength();
512
- const lastItemScrollOffset = this._getItemScrollOffset(dataLength - 1);
952
+ const positionIndex = getPositionIndex(index);
513
953
 
514
- this._currentScrollOffset = scrollOffset;
954
+ if (positionIndex === activeItemRef.current) {
955
+ return;
956
+ }
515
957
 
516
- if (nextActiveItem !== this._onScrollActiveItem) {
517
- this._onScrollActiveItem = nextActiveItem;
518
- onScrollIndexChanged &&
519
- onScrollIndexChanged(this._getDataIndex(nextActiveItem));
958
+ snapToItem(positionIndex, animated, fireCallback);
959
+ },
960
+ [getPositionIndex, snapToItem],
961
+ );
520
962
 
521
- onSnapToItem && onSnapToItem(this._getDataIndex(nextActiveItem));
522
- }
963
+ const snapToNext = useCallback(
964
+ (animated = true, fireCallback = true) => {
965
+ const itemsLength = getCustomDataLength();
523
966
 
524
- if (
525
- (IS_IOS && scrollOffset > lastItemScrollOffset) ||
526
- (IS_ANDROID &&
527
- Math.floor(scrollOffset) > Math.floor(lastItemScrollOffset))
528
- ) {
529
- this._activeItem = nextActiveItem;
530
- this._repositionScroll(nextActiveItem);
967
+ let newIndex = activeItemRef.current + 1;
968
+ if (newIndex > itemsLength - 1) {
969
+ newIndex = 0;
970
+ }
971
+ snapToItem(newIndex, animated, fireCallback);
972
+ },
973
+ [getCustomDataLength, snapToItem],
974
+ );
975
+
976
+ const snapToPrev = useCallback(
977
+ (animated = true, fireCallback = true) => {
978
+ const itemsLength = getCustomDataLength();
979
+
980
+ let newIndex = activeItemRef.current - 1;
981
+ if (newIndex < 0) {
982
+ newIndex = itemsLength - 1;
983
+ }
984
+ snapToItem(newIndex, animated, fireCallback);
985
+ },
986
+ [getCustomDataLength, snapToItem],
987
+ );
988
+
989
+ const triggerRenderingHack = useCallback(
990
+ (offset = 1) => {
991
+ hackActiveSlideAnimation(activeItemRef.current, offset);
992
+ },
993
+ [hackActiveSlideAnimation],
994
+ );
995
+
996
+ // Display warnings
997
+ const displayWarnings = useCallback(() => {
998
+ const pluginName = 'react-native-snap-carousel';
999
+
1000
+ if (!vertical && (!sliderWidth || !itemWidth)) {
1001
+ console.error(
1002
+ `${pluginName}: You need to specify both 'sliderWidth' and 'itemWidth' for horizontal carousels`,
1003
+ );
531
1004
  }
532
-
533
- if (typeof onScroll === 'function' && event) {
534
- onScroll(event);
1005
+ if (vertical && (!sliderHeight || !itemHeight)) {
1006
+ console.error(
1007
+ `${pluginName}: You need to specify both 'sliderHeight' and 'itemHeight' for vertical carousels`,
1008
+ );
535
1009
  }
536
- }
537
-
538
- _onMomentumScrollEnd(event: NativeSyntheticEvent<NativeScrollEvent>) {
539
- const { autoplayDelay, onMomentumScrollEnd, onSnapToItem } = this.props;
540
- const { itemWidth } = this.state;
541
- const scrollOffset = event
542
- ? this._getScrollOffset(event)
543
- : this._currentScrollOffset;
544
- const nextActiveItem = this._getActiveItem(scrollOffset);
545
- const hasSnapped = this._isMultiple(scrollOffset, itemWidth);
1010
+ }, [vertical, sliderWidth, itemWidth, sliderHeight, itemHeight]);
546
1011
 
547
- if (nextActiveItem !== this._activeItem) {
548
- this._activeItem = nextActiveItem;
549
- onSnapToItem && onSnapToItem(this._getDataIndex(nextActiveItem));
1012
+ // Set scroll handler
1013
+ const setScrollHandler = useCallback(() => {
1014
+ const scrollEventConfig = {
1015
+ listener: handleScroll,
1016
+ useNativeDriver: true,
1017
+ };
1018
+ scrollPosRef.current = new Animated.Value(0);
1019
+ const argMapping = vertical
1020
+ ? [{ nativeEvent: { contentOffset: { y: scrollPosRef.current } } }]
1021
+ : [{ nativeEvent: { contentOffset: { x: scrollPosRef.current } } }];
550
1022
 
551
- if (hasSnapped && IS_ANDROID) {
552
- this._repositionScroll(nextActiveItem);
553
- } else if (IS_IOS) {
554
- this._repositionScroll(nextActiveItem);
1023
+ if (onScroll && Array.isArray((onScroll as any)._argMapping)) {
1024
+ argMapping.pop();
1025
+ const [argMap] = (onScroll as any)._argMapping;
1026
+ if (argMap && argMap.nativeEvent && argMap.nativeEvent.contentOffset) {
1027
+ scrollPosRef.current =
1028
+ argMap.nativeEvent.contentOffset.x ||
1029
+ argMap.nativeEvent.contentOffset.y ||
1030
+ scrollPosRef.current;
555
1031
  }
1032
+ argMapping.push(...(onScroll as any)._argMapping);
556
1033
  }
1034
+ onScrollHandlerRef.current = Animated.event(argMapping, scrollEventConfig);
1035
+ }, [handleScroll, vertical, onScroll]);
1036
+
1037
+ // Expose public methods via ref
1038
+ useImperativeHandle(
1039
+ ref,
1040
+ () => ({
1041
+ snapToItem: snapToItemPublic,
1042
+ snapToNext,
1043
+ snapToPrev,
1044
+ startAutoplay,
1045
+ pauseAutoPlay,
1046
+ stopAutoplay,
1047
+ triggerRenderingHack,
1048
+ get realIndex() {
1049
+ return activeItemRef.current;
1050
+ },
1051
+ get currentIndex() {
1052
+ return getDataIndex(activeItemRef.current);
1053
+ },
1054
+ get currentScrollPosition() {
1055
+ return currentScrollOffsetRef.current;
1056
+ },
1057
+ }),
1058
+ [
1059
+ snapToItemPublic,
1060
+ snapToNext,
1061
+ snapToPrev,
1062
+ startAutoplay,
1063
+ pauseAutoPlay,
1064
+ stopAutoplay,
1065
+ triggerRenderingHack,
1066
+ getDataIndex,
1067
+ ],
1068
+ );
1069
+
1070
+ // componentDidMount
1071
+ useEffect(() => {
1072
+ mountedRef.current = true;
1073
+ initPositionsAndInterpolators();
1074
+ displayWarnings();
1075
+ setScrollHandler();
1076
+
1077
+ initTimeoutRef.current = setTimeout(() => {
1078
+ if (!mountedRef.current) {
1079
+ return;
1080
+ }
557
1081
 
558
- onMomentumScrollEnd && onMomentumScrollEnd(event);
559
-
560
- if (IS_ANDROID && this._autoplay && !this._autoplaying) {
561
- if (this._enableAutoplayTimeout != null)
562
- clearTimeout(this._enableAutoplayTimeout);
563
- this._enableAutoplayTimeout = setTimeout(() => {
564
- this.startAutoplay();
565
- }, autoplayDelay);
566
- }
567
- }
1082
+ const apparitionCallback = () => {
1083
+ if (apparitionDelay) {
1084
+ setHideCarousel(false);
1085
+ }
1086
+ if (autoplay) {
1087
+ startAutoplay();
1088
+ }
1089
+ };
568
1090
 
569
- _onLayout(event: LayoutChangeEvent) {
570
- const { onLayout, visibleItem = 1 } = this.props;
1091
+ if (needsScrollView()) {
1092
+ const _firstItem = getFirstItem(firstItem);
1093
+ snapToItem(_firstItem, false, false, true);
1094
+ }
571
1095
 
572
- if (this._onLayoutInitDone) {
573
- this._initPositionsAndInterpolators();
574
- this._snapToItem(this._activeItem, false, false, true);
575
- } else {
576
- this._onLayoutInitDone = true;
577
- }
578
- const containerWidth = event.nativeEvent.layout.width;
579
- let itemWidth =
580
- this.props.visibleItem === 1
581
- ? screenWidth - Spacing.M * 2
582
- : Math.ceil(
583
- (containerWidth * 0.9 - visibleItem * Spacing.S) / visibleItem,
584
- );
585
- if (this.props.itemWidth) {
586
- itemWidth = this.props.itemWidth;
587
- }
588
- if (this.props.full) {
589
- itemWidth = containerWidth;
590
- }
1096
+ if (apparitionDelay) {
1097
+ apparitionTimeoutRef.current = setTimeout(() => {
1098
+ apparitionCallback();
1099
+ }, apparitionDelay);
1100
+ } else {
1101
+ apparitionCallback();
1102
+ }
1103
+ }, 1);
591
1104
 
592
- this.setState({ containerWidth, itemWidth });
1105
+ return () => {
1106
+ mountedRef.current = false;
1107
+ stopAutoplay();
1108
+ if (initTimeoutRef.current) clearTimeout(initTimeoutRef.current);
1109
+ if (apparitionTimeoutRef.current)
1110
+ clearTimeout(apparitionTimeoutRef.current);
1111
+ if (hackSlideAnimationTimeoutRef.current)
1112
+ clearTimeout(hackSlideAnimationTimeoutRef.current);
1113
+ if (enableAutoplayTimeoutRef.current)
1114
+ clearTimeout(enableAutoplayTimeoutRef.current);
1115
+ if (autoplayTimeoutRef.current) clearTimeout(autoplayTimeoutRef.current);
1116
+ if (snapNoMomentumTimeoutRef.current)
1117
+ clearTimeout(snapNoMomentumTimeoutRef.current);
1118
+ if (androidRepositioningTimeoutRef.current)
1119
+ clearTimeout(androidRepositioningTimeoutRef.current);
1120
+ };
1121
+ }, []);
593
1122
 
594
- onLayout && onLayout(event);
595
- }
1123
+ // componentDidUpdate - handle scrollEnabled changes
1124
+ useEffect(() => {
1125
+ if (scrollEnabledProp !== scrollEnabledRef.current) {
1126
+ setScrollEnabled(scrollEnabledProp);
1127
+ }
1128
+ }, [scrollEnabledProp, setScrollEnabled]);
596
1129
 
597
- _getPositionIndex(index: number) {
598
- const { loop, loopClonesPerSide = 3 } = this.props;
599
- return loop ? index + loopClonesPerSide : index;
600
- }
1130
+ // componentDidUpdate - handle data/size changes
1131
+ useEffect(() => {
1132
+ const itemsLength = getCustomDataLength();
601
1133
 
602
- _snapToItem(
603
- index: number,
604
- animated = true,
605
- fireCallback = true,
606
- forceScrollTo = false,
607
- ) {
608
- const { onSnapToItem } = this.props;
609
- const itemsLength = this._getCustomDataLength();
610
- const wrappedRef = this._getWrappedRef();
611
- if (!itemsLength || !wrappedRef) {
1134
+ if (!itemsLength) {
612
1135
  return;
613
1136
  }
614
1137
 
615
- if (!index || index < 0) {
616
- index = 0;
617
- } else if (itemsLength > 0 && index >= itemsLength) {
618
- index = itemsLength - 1;
619
- }
1138
+ const nextFirstItem = getFirstItem(firstItem);
1139
+ let nextActiveItem =
1140
+ typeof activeItemRef.current !== 'undefined'
1141
+ ? activeItemRef.current
1142
+ : nextFirstItem;
620
1143
 
621
- if (index === this._activeItem && !forceScrollTo) {
622
- return;
623
- }
1144
+ const hasNewSize =
1145
+ (vertical &&
1146
+ (previousItemsLengthRef.current !== itemsLength ||
1147
+ previousItemsLengthRef.current === 0)) ||
1148
+ (!vertical &&
1149
+ (previousItemsLengthRef.current !== itemsLength ||
1150
+ previousItemsLengthRef.current === 0));
624
1151
 
625
- this._carouselRef.scrollToIndex({
626
- index,
627
- animated: true,
628
- });
1152
+ if (nextActiveItem > itemsLength - 1) {
1153
+ nextActiveItem = itemsLength - 1;
1154
+ }
629
1155
 
630
- const requiresManualTrigger = !animated || IS_ANDROID;
631
- if (requiresManualTrigger) {
632
- this._activeItem = index;
1156
+ if (interpolators.length !== itemsLength || hasNewSize) {
1157
+ activeItemRef.current = nextActiveItem;
1158
+ previousItemsLengthRef.current = itemsLength;
633
1159
 
634
- if (fireCallback) {
635
- onSnapToItem && onSnapToItem(this._getDataIndex(index));
1160
+ initPositionsAndInterpolators();
1161
+ if (previousItemsLengthRef.current > itemsLength) {
1162
+ hackActiveSlideAnimation(nextActiveItem);
636
1163
  }
637
1164
 
638
- if (IS_ANDROID && this._shouldRepositionScroll(index)) {
639
- if (animated) {
640
- this._androidRepositioningTimeout = setTimeout(() => {
641
- this._repositionScroll(index, false);
642
- }, 400);
643
- } else {
644
- this._repositionScroll(index);
645
- }
1165
+ if (hasNewSize) {
1166
+ snapToItem(nextActiveItem, false, false, true);
646
1167
  }
1168
+ } else if (
1169
+ nextFirstItem !== previousFirstItemRef.current &&
1170
+ nextFirstItem !== activeItemRef.current
1171
+ ) {
1172
+ activeItemRef.current = nextFirstItem;
1173
+ previousFirstItemRef.current = nextFirstItem;
1174
+ snapToItem(nextFirstItem, false, true, true);
647
1175
  }
648
- }
1176
+ }, [
1177
+ data,
1178
+ firstItem,
1179
+ interpolators.length,
1180
+ vertical,
1181
+ itemWidth,
1182
+ itemHeight,
1183
+ sliderWidth,
1184
+ sliderHeight,
1185
+ ]);
1186
+
1187
+ // componentDidUpdate - handle onScroll prop changes
1188
+ useEffect(() => {
1189
+ setScrollHandler();
1190
+ }, [onScroll]);
1191
+
1192
+ // Render item
1193
+ const renderItemComponent = useCallback(
1194
+ ({ item, index }: { item: any; index: number }) => {
1195
+ const animatedValue = interpolators && interpolators[index];
1196
+
1197
+ if (typeof animatedValue === 'undefined') {
1198
+ return null;
1199
+ }
649
1200
 
650
- _renderItem(info: { item: any; index: number }) {
651
- const { item, index } = info;
652
- const { interpolators, itemWidth } = this.state;
653
- const { slideStyle, full } = this.props;
654
- const animatedValue = interpolators && interpolators[index];
1201
+ const animate = shouldAnimateSlides();
1202
+ const Component = animate ? Animated.View : View;
1203
+ const animatedStyle = animate
1204
+ ? getSlideInterpolatedStyle(index, animatedValue)
1205
+ : {};
1206
+ const dataIndex = getDataIndex(index);
1207
+
1208
+ const mainDimension = vertical
1209
+ ? { height: itemHeight }
1210
+ : { width: itemWidth };
1211
+ const specificProps = needsScrollView()
1212
+ ? {
1213
+ key: keyExtractor
1214
+ ? keyExtractor(item, index)
1215
+ : getKeyExtractor(item, index),
1216
+ }
1217
+ : {};
1218
+
1219
+ return (
1220
+ <Component
1221
+ style={[mainDimension, slideStyle, animatedStyle]}
1222
+ pointerEvents="box-none"
1223
+ {...specificProps}
1224
+ >
1225
+ {vertical
1226
+ ? renderItem(
1227
+ {
1228
+ item,
1229
+ index,
1230
+ dataIndex,
1231
+ realIndex: getDataIndex(index),
1232
+ activeIndex: getDataIndex(activeItemRef.current),
1233
+ },
1234
+ {
1235
+ scrollPosition: scrollPosRef.current,
1236
+ carouselRef: carouselRef.current,
1237
+ vertical: vertical,
1238
+ sliderHeight: sliderHeight,
1239
+ itemHeight: itemHeight,
1240
+ },
1241
+ )
1242
+ : renderItem(
1243
+ {
1244
+ item,
1245
+ index,
1246
+ dataIndex,
1247
+ realIndex: getDataIndex(index),
1248
+ activeIndex: getDataIndex(activeItemRef.current),
1249
+ },
1250
+ {
1251
+ scrollPosition: scrollPosRef.current,
1252
+ carouselRef: carouselRef.current,
1253
+ vertical: false,
1254
+ sliderWidth: sliderWidth,
1255
+ itemWidth: itemWidth,
1256
+ },
1257
+ )}
1258
+ </Component>
1259
+ );
1260
+ },
1261
+ [
1262
+ interpolators,
1263
+ shouldAnimateSlides,
1264
+ getSlideInterpolatedStyle,
1265
+ getDataIndex,
1266
+ vertical,
1267
+ itemHeight,
1268
+ itemWidth,
1269
+ needsScrollView,
1270
+ keyExtractor,
1271
+ getKeyExtractor,
1272
+ slideStyle,
1273
+ renderItem,
1274
+ sliderHeight,
1275
+ sliderWidth,
1276
+ ],
1277
+ );
1278
+
1279
+ // Get component props
1280
+ const getComponentOverridableProps = useCallback(() => {
1281
+ const visibleItems =
1282
+ Math.max(
1283
+ 1,
1284
+ Math.ceil(
1285
+ vertical
1286
+ ? (sliderHeight || 1) / (itemHeight || 1)
1287
+ : (sliderWidth || 1) / (itemWidth || 1),
1288
+ ),
1289
+ ) + 1;
1290
+ const initialNumPerSide = enableLoop() ? loopClonesPerSide : 2;
1291
+ const initialNumToRender = Math.max(
1292
+ 1,
1293
+ visibleItems + initialNumPerSide * 2,
1294
+ );
1295
+ const maxToRenderPerBatch = Math.max(
1296
+ 1,
1297
+ initialNumToRender + initialNumPerSide * 2,
1298
+ );
1299
+ const windowSize = Math.max(2, maxToRenderPerBatch);
655
1300
 
656
- if (typeof animatedValue === 'undefined') {
657
- return null;
658
- }
1301
+ const specificProps = !needsScrollView()
1302
+ ? {
1303
+ initialNumToRender,
1304
+ maxToRenderPerBatch,
1305
+ windowSize,
1306
+ }
1307
+ : {};
659
1308
 
660
- const animate = this._shouldAnimateSlides();
661
- const Component = animate ? Animated.View : View;
662
- const mainDimension = { width: itemWidth };
1309
+ return {
1310
+ ...specificProps,
1311
+ automaticallyAdjustContentInsets: false,
1312
+ decelerationRate: 'fast' as const,
1313
+ directionalLockEnabled: true,
1314
+ disableScrollViewPanResponder: false,
1315
+ inverted: needsRTLAdaptations(),
1316
+ overScrollMode: 'never' as const,
1317
+ pinchGestureEnabled: false,
1318
+ pointerEvents: hideCarousel ? ('none' as const) : ('auto' as const),
1319
+ scrollsToTop: false,
1320
+ showsHorizontalScrollIndicator: false,
1321
+ showsVerticalScrollIndicator: false,
1322
+ };
1323
+ }, [
1324
+ vertical,
1325
+ sliderHeight,
1326
+ itemHeight,
1327
+ sliderWidth,
1328
+ itemWidth,
1329
+ enableLoop,
1330
+ loopClonesPerSide,
1331
+ needsScrollView,
1332
+ needsRTLAdaptations,
1333
+ hideCarousel,
1334
+ ]);
1335
+
1336
+ const getComponentStaticProps = useCallback(() => {
1337
+ const containerStyle = [
1338
+ containerCustomStyle || style || {},
1339
+ hideCarousel ? { opacity: 0 } : {},
1340
+ vertical
1341
+ ? { height: sliderHeight, flexDirection: 'column' as const }
1342
+ : {
1343
+ width: sliderWidth,
1344
+ flexDirection: needsRTLAdaptations()
1345
+ ? ('row-reverse' as const)
1346
+ : ('row' as const),
1347
+ },
1348
+ ];
663
1349
 
664
- let spacingStyle: ViewStyle = this.props.loop
665
- ? { marginLeft: Spacing.S }
1350
+ const innerMarginStyle = vertical
1351
+ ? {
1352
+ paddingTop: getContainerInnerMargin(),
1353
+ paddingBottom: getContainerInnerMargin(true),
1354
+ }
666
1355
  : {
667
- marginLeft: index === 0 ? Spacing.M : 0,
668
- marginRight:
669
- index === this._getCustomDataLength() - 1 ? Spacing.M : Spacing.S,
1356
+ paddingLeft: getContainerInnerMargin(),
1357
+ paddingRight: getContainerInnerMargin(true),
670
1358
  };
671
1359
 
672
- if (full) {
673
- spacingStyle = {};
674
- }
675
- const animatedStyle = defaultAnimatedStyles(animatedValue, this.props);
676
- return (
677
- <Component
678
- style={[
679
- mainDimension,
680
- animatedStyle,
681
- { overflow: 'hidden' },
682
- spacingStyle,
683
- slideStyle,
684
- ]}
685
- pointerEvents="box-none"
686
- >
687
- {this.props.renderItem({
688
- item,
689
- index,
690
- })}
691
- </Component>
692
- );
693
- }
694
-
695
- startAutoplay() {
696
- const { autoplayInterval, autoplayDelay } = this.props;
697
- this._autoplay = true;
698
-
699
- if (this._autoplaying) {
700
- return;
701
- }
1360
+ const contentContainerStyle = [
1361
+ !useExperimentalSnap ? innerMarginStyle : {},
1362
+ contentContainerCustomStyle || {},
1363
+ ];
702
1364
 
703
- if (this._autoplayTimeout != null) clearTimeout(this._autoplayTimeout);
704
- this._autoplayTimeout = setTimeout(() => {
705
- this._autoplaying = true;
706
- this._autoplayInterval = setInterval(() => {
707
- if (this._autoplaying) {
708
- this.snapToNext();
1365
+ const snapProps = useExperimentalSnap
1366
+ ? {
1367
+ disableIntervalMomentum,
1368
+ snapToAlignment: activeSlideAlignment,
1369
+ snapToInterval: snapToInterval || getItemMainDimension(),
709
1370
  }
710
- }, autoplayInterval);
711
- }, autoplayDelay);
712
- }
713
-
714
- pauseAutoPlay() {
715
- this._autoplaying = false;
716
- if (this._autoplayTimeout != null) clearTimeout(this._autoplayTimeout);
717
- if (this._enableAutoplayTimeout != null)
718
- clearTimeout(this._enableAutoplayTimeout);
719
- if (this._autoplayInterval != null) clearInterval(this._autoplayInterval);
720
- }
721
-
722
- stopAutoplay() {
723
- this._autoplay = false;
724
- this.pauseAutoPlay();
725
- }
726
-
727
- snapToItem(index: number, animated = true, fireCallback = true) {
728
- if (!index || index < 0) {
729
- index = 0;
730
- }
731
-
732
- const positionIndex = this._getPositionIndex(index);
1371
+ : {
1372
+ snapToOffsets: getSnapOffsets(),
1373
+ };
733
1374
 
734
- if (positionIndex === this._activeItem) {
735
- return;
736
- }
1375
+ const specificProps = !needsScrollView()
1376
+ ? {
1377
+ CellRendererComponent:
1378
+ CellRendererComponent || getCellRendererComponent,
1379
+ getItemLayout: getItemLayoutProp || getItemLayout,
1380
+ initialScrollIndex: getFirstItem(firstItem),
1381
+ keyExtractor: keyExtractor || getKeyExtractor,
1382
+ numColumns: 1,
1383
+ renderItem: renderItemComponent as any, // Type cast for compatibility
1384
+ }
1385
+ : {};
737
1386
 
738
- this._snapToItem(positionIndex, animated, fireCallback);
1387
+ return {
1388
+ ...specificProps,
1389
+ ...snapProps,
1390
+ ref: carouselRef,
1391
+ contentContainerStyle: contentContainerStyle,
1392
+ data: getCustomData(),
1393
+ horizontal: !vertical,
1394
+ scrollEventThrottle: 1,
1395
+ style: containerStyle,
1396
+ onLayout: handleLayout,
1397
+ onMomentumScrollEnd: handleMomentumScrollEnd,
1398
+ onScroll: onScrollHandlerRef.current,
1399
+ onTouchStart: handleTouchStart,
1400
+ onTouchEnd: handleTouchEnd,
1401
+ };
1402
+ }, [
1403
+ containerCustomStyle,
1404
+ style,
1405
+ hideCarousel,
1406
+ vertical,
1407
+ sliderHeight,
1408
+ sliderWidth,
1409
+ needsRTLAdaptations,
1410
+ getContainerInnerMargin,
1411
+ useExperimentalSnap,
1412
+ contentContainerCustomStyle,
1413
+ disableIntervalMomentum,
1414
+ activeSlideAlignment,
1415
+ getItemMainDimension,
1416
+ getSnapOffsets,
1417
+ needsScrollView,
1418
+ CellRendererComponent,
1419
+ getCellRendererComponent,
1420
+ getItemLayout,
1421
+ getFirstItem,
1422
+ firstItem,
1423
+ keyExtractor,
1424
+ getKeyExtractor,
1425
+ renderItemComponent,
1426
+ getCustomData,
1427
+ handleLayout,
1428
+ handleMomentumScrollEnd,
1429
+ handleTouchStart,
1430
+ handleTouchEnd,
1431
+ ]);
1432
+
1433
+ // Render
1434
+ if (!data || !renderItem) {
1435
+ return null;
739
1436
  }
740
1437
 
741
- snapToNext(animated = true, fireCallback = true) {
742
- const itemsLength = this._getCustomDataLength();
1438
+ const overridableProps = getComponentOverridableProps();
1439
+ const staticProps = getComponentStaticProps();
743
1440
 
744
- let newIndex = this._activeItem + 1;
745
- if (newIndex > itemsLength - 1) {
746
- newIndex = 0;
747
- }
748
- this._snapToItem(newIndex, animated, fireCallback);
749
- }
1441
+ // Filter out props that should not be passed to FlatList/ScrollView
1442
+ const { renderItem: _, ...propsWithoutRenderItem } = props;
750
1443
 
751
- snapToPrev(animated = true, fireCallback = true) {
752
- const itemsLength = this._getCustomDataLength();
1444
+ const componentProps: any = {
1445
+ ...overridableProps,
1446
+ ...propsWithoutRenderItem,
1447
+ ...staticProps,
1448
+ };
753
1449
 
754
- let newIndex = this._activeItem - 1;
755
- if (newIndex < 0) {
756
- newIndex = itemsLength - 1;
757
- }
758
- this._snapToItem(newIndex, animated, fireCallback);
759
- }
1450
+ const ScrollViewComponent =
1451
+ typeof useScrollView === 'function' ? useScrollView : Animated.ScrollView;
760
1452
 
761
- render() {
762
- const {
763
- loopClonesPerSide = 3,
764
- visibleItem = 1,
765
- firstItem = 0,
766
- getItemLayout,
767
- keyExtractor,
768
- style,
769
- disableIntervalMomentum,
770
- enableSnap,
771
- contentContainerStyle,
772
- } = this.props;
773
- const { hideCarousel } = this.state;
774
-
775
- const initialNumPerSide = this._enableLoop() ? loopClonesPerSide : 2;
776
- const initialNumToRender =
777
- visibleItem > 2
778
- ? visibleItem + initialNumPerSide * 2
779
- : initialNumPerSide * 2;
780
- const maxToRenderPerBatch = initialNumToRender;
781
- const windowSize = maxToRenderPerBatch;
782
-
783
- const snapToInterval = enableSnap
784
- ? this._getItemMainDimension()
785
- : undefined;
786
-
787
- const specificProps = {
788
- getItemLayout: getItemLayout || this._getItemLayout,
789
- initialScrollIndex: this._getFirstItem(firstItem),
790
- keyExtractor: keyExtractor || this._getKeyExtractor,
791
- renderItem: this._renderItem,
792
- };
1453
+ return needsScrollView() || !Animated.FlatList ? (
1454
+ <ScrollViewComponent {...componentProps}>
1455
+ {getCustomData().map((item: any, index: number) => {
1456
+ return renderItemComponent({
1457
+ item,
1458
+ index,
1459
+ });
1460
+ })}
1461
+ </ScrollViewComponent>
1462
+ ) : (
1463
+ <Animated.FlatList {...componentProps} />
1464
+ );
1465
+ });
793
1466
 
794
- return (
795
- <Animated.FlatList
796
- {...this.props}
797
- {...specificProps}
798
- ref={c => {
799
- this._carouselRef = c;
800
- }}
801
- overScrollMode={'never'}
802
- snapToInterval={
803
- this.props.snapToInterval ? this.props.snapToInterval : snapToInterval
804
- }
805
- disableIntervalMomentum={disableIntervalMomentum}
806
- pointerEvents={hideCarousel ? 'none' : 'auto'}
807
- decelerationRate={'fast'}
808
- numColumns={1}
809
- style={[
810
- style,
811
- { width: '100%', flexDirection: 'row' },
812
- hideCarousel ? { opacity: 0 } : {},
813
- ]}
814
- automaticallyAdjustContentInsets={false}
815
- directionalLockEnabled
816
- contentContainerStyle={contentContainerStyle}
817
- disableScrollViewPanResponder={false}
818
- pinchGestureEnabled={false}
819
- scrollsToTop={false}
820
- showsHorizontalScrollIndicator={false}
821
- showsVerticalScrollIndicator={false}
822
- initialNumToRender={initialNumToRender}
823
- maxToRenderPerBatch={maxToRenderPerBatch}
824
- windowSize={windowSize}
825
- pagingEnabled={enableSnap}
826
- data={this._getCustomData()}
827
- horizontal
828
- scrollEventThrottle={1}
829
- onLayout={this._onLayout}
830
- onMomentumScrollEnd={this._onMomentumScrollEnd}
831
- onScroll={this._onScrollHandler}
832
- onTouchStart={this._onTouchStart}
833
- onTouchEnd={this._onTouchEnd}
834
- />
835
- );
836
- }
837
- }
1467
+ Carousel.displayName = 'Carousel';
838
1468
 
839
1469
  export { Carousel };
840
- export type { CarouselProps, CarouselRef };
1470
+ export type { CarouselRef, CarouselProps };