@momo-kits/carousel 0.150.3-beta.20 → 0.151.1-beta.2

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 +17 -148
  2. package/index.tsx +700 -1330
  3. package/package.json +20 -20
  4. package/types.ts +18 -64
package/index.tsx CHANGED
@@ -1,1470 +1,840 @@
1
- import React, {
2
- forwardRef,
3
- useCallback,
4
- useEffect,
5
- useImperativeHandle,
6
- useRef,
7
- useState,
8
- } from 'react';
1
+ import React from 'react';
9
2
  import {
10
3
  Animated,
11
- I18nManager,
4
+ Dimensions,
5
+ GestureResponderEvent,
12
6
  LayoutChangeEvent,
13
- NativeSyntheticEvent,
14
7
  NativeScrollEvent,
8
+ NativeSyntheticEvent,
15
9
  Platform,
16
10
  View,
11
+ ViewStyle,
17
12
  } from 'react-native';
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';
13
+ import { defaultAnimatedStyles, defaultScrollInterpolator } from './animation';
14
+ import { CarouselProps, CarouselRef, CarouselState, Position } from './types';
15
+ import { Spacing } from '@momo-kits/foundation';
28
16
 
29
17
  const IS_ANDROID = Platform.OS === 'android';
30
18
  const IS_IOS = Platform.OS === 'ios';
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]);
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
+ };
143
47
 
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
- );
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
+ };
190
79
 
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
- }
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
+ }
206
101
 
207
- if (!enableLoop()) {
208
- return propsData;
209
- }
102
+ componentDidMount() {
103
+ const { apparitionDelay, autoplay } = this.props;
210
104
 
211
- let previousItems: any[] = [];
212
- let nextItems: any[] = [];
105
+ this._mounted = true;
106
+ this._initPositionsAndInterpolators();
213
107
 
214
- if (loopClones > dataLength) {
215
- const dataMultiplier = Math.floor(loopClones / dataLength);
216
- const remainder = loopClones % dataLength;
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
+ }
217
113
 
218
- for (let i = 0; i < dataMultiplier; i++) {
219
- previousItems.push(...propsData);
220
- nextItems.push(...propsData);
114
+ const apparitionCallback = () => {
115
+ if (apparitionDelay) {
116
+ this.setState({ hideCarousel: false });
117
+ }
118
+ if (autoplay) {
119
+ this.startAutoplay();
221
120
  }
121
+ };
222
122
 
223
- previousItems.unshift(...propsData.slice(-remainder));
224
- nextItems.push(...propsData.slice(0, remainder));
123
+ if (apparitionDelay) {
124
+ this._apparitionTimeout = setTimeout(() => {
125
+ apparitionCallback();
126
+ }, apparitionDelay);
225
127
  } else {
226
- previousItems = propsData.slice(-loopClones);
227
- nextItems = propsData.slice(0, loopClones);
128
+ apparitionCallback();
228
129
  }
130
+ }, 1);
131
+ }
229
132
 
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
- }
133
+ componentDidUpdate(prevProps: CarouselProps) {
134
+ const { interpolators } = this.state;
135
+ const { firstItem = 0, scrollEnabled } = this.props;
136
+ const itemsLength = this._getCustomDataLength(this.props);
246
137
 
247
- return enableLoop() ? dataLength + 2 * loopClones : dataLength;
248
- },
249
- [data, loopClonesPerSide, enableLoop],
250
- );
138
+ if (!itemsLength) {
139
+ return;
140
+ }
251
141
 
252
- const getCustomIndex = useCallback(
253
- (index: number, propsOverride = props) => {
254
- const itemsLength = getCustomDataLength(propsOverride);
142
+ const nextFirstItem = this._getFirstItem(firstItem, this.props);
143
+ let nextActiveItem =
144
+ typeof this._activeItem !== 'undefined'
145
+ ? this._activeItem
146
+ : nextFirstItem;
255
147
 
256
- if (!itemsLength || typeof index === 'undefined') {
257
- return 0;
258
- }
148
+ if (nextActiveItem > itemsLength - 1) {
149
+ nextActiveItem = itemsLength - 1;
150
+ }
259
151
 
260
- return needsRTLAdaptations() ? itemsLength - index - 1 : index;
261
- },
262
- [getCustomDataLength, needsRTLAdaptations],
263
- );
152
+ if (scrollEnabled !== prevProps.scrollEnabled) {
153
+ this._setScrollEnabled(scrollEnabled);
154
+ }
264
155
 
265
- const getDataIndex = useCallback(
266
- (index: number) => {
267
- const dataLength = data && data.length;
268
- if (!enableLoop() || !dataLength) {
269
- return index;
270
- }
156
+ if (interpolators.length !== itemsLength) {
157
+ this._activeItem = nextActiveItem;
158
+ this._previousItemsLength = itemsLength;
271
159
 
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]);
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
+ }
316
169
 
317
- const getFirstItem = useCallback(
318
- (index: number, propsOverride = props) => {
319
- const { loopClonesPerSide: loopClones = loopClonesPerSide } =
320
- propsOverride;
321
- const itemsLength = getCustomDataLength(propsOverride);
170
+ if (this.props.onScroll !== prevProps.onScroll) {
171
+ this._setScrollHandler(this.props);
172
+ }
173
+ }
322
174
 
323
- if (!itemsLength || index > itemsLength - 1 || index < 0) {
324
- return 0;
325
- }
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
+ }
326
188
 
327
- return enableLoop() ? index + loopClones : index;
328
- },
329
- [getCustomDataLength, enableLoop, loopClonesPerSide],
330
- );
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
+ ];
331
198
 
332
- const getWrappedRef = useCallback(() => {
333
- if (
334
- carouselRef.current &&
335
- ((needsScrollView() && carouselRef.current.scrollTo) ||
336
- (!needsScrollView() && carouselRef.current.scrollToOffset))
337
- ) {
338
- return carouselRef.current;
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;
207
+ }
208
+ argMapping.push(...props.onScroll._argMapping);
339
209
  }
340
- return (
341
- carouselRef.current &&
342
- carouselRef.current.getNode &&
343
- carouselRef.current.getNode()
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
+
218
+ _shouldAnimateSlides(props = this.props) {
219
+ const { inactiveSlideOpacity = 1, inactiveSlideScale = 1 } = props;
220
+ return inactiveSlideOpacity < 1 || inactiveSlideScale < 1;
221
+ }
222
+
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)
344
231
  );
345
- }, [needsScrollView]);
232
+ }
346
233
 
347
- const getScrollEnabled = useCallback(() => {
348
- return scrollEnabledRef.current;
349
- }, []);
234
+ _isMultiple(x: number, y: number) {
235
+ return Math.round(Math.round(x / y) / (1 / y)) === Math.round(x);
236
+ }
350
237
 
351
- const setScrollEnabled = useCallback(
352
- (enabled = true) => {
353
- const wrappedRef = getWrappedRef();
238
+ _getCustomData(props = this.props) {
239
+ const { data, loopClonesPerSide = 3 } = props;
240
+ const dataLength = data && data.length;
354
241
 
355
- if (!wrappedRef || !wrappedRef.setNativeProps) {
356
- return;
357
- }
242
+ if (!dataLength) {
243
+ return [];
244
+ }
358
245
 
359
- wrappedRef.setNativeProps({ scrollEnabled: enabled });
360
- scrollEnabledRef.current = enabled;
361
- },
362
- [getWrappedRef],
363
- );
246
+ if (!this._enableLoop()) {
247
+ return data;
248
+ }
364
249
 
365
- const getItemMainDimension = useCallback(() => {
366
- return vertical ? itemHeight : itemWidth;
367
- }, [vertical, itemHeight, itemWidth]);
250
+ let previousItems = [];
251
+ let nextItems = [];
368
252
 
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;
473
- }
253
+ if (loopClonesPerSide > dataLength) {
254
+ const dataMultiplier = Math.floor(loopClonesPerSide / dataLength);
255
+ const remainder = loopClonesPerSide % dataLength;
474
256
 
475
- if (
476
- positionsRef.current[lastIndex] &&
477
- offset >= positionsRef.current[lastIndex].start
478
- ) {
479
- return lastIndex;
257
+ for (let i = 0; i < dataMultiplier; i++) {
258
+ previousItems.push(...data);
259
+ nextItems.push(...data);
480
260
  }
481
261
 
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
- }
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
+ }
489
268
 
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
- }
269
+ return previousItems.concat(data, nextItems);
270
+ }
538
271
 
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
- }
272
+ _getCustomDataLength(props = this.props) {
273
+ const { data, loopClonesPerSide = 3 } = props;
274
+ const dataLength = data && data.length;
547
275
 
548
- const newInterpolators: Animated.AnimatedInterpolation<number>[] = [];
549
- positionsRef.current = [];
276
+ if (!dataLength) {
277
+ return 0;
278
+ }
550
279
 
551
- getCustomData(propsOverride).forEach((_itemData: any, index: number) => {
552
- const _index = getCustomIndex(index, propsOverride);
553
- let animatedValue: Animated.AnimatedInterpolation<number>;
280
+ return this._enableLoop() ? dataLength + 2 * loopClonesPerSide : dataLength;
281
+ }
554
282
 
555
- positionsRef.current[index] = {
556
- start: index * itemMainDimension,
557
- end: index * itemMainDimension + itemMainDimension,
558
- };
283
+ _getCustomIndex(index: number, props = this.props) {
284
+ const itemsLength = this._getCustomDataLength(props);
559
285
 
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
- }
286
+ if (!itemsLength || typeof index === 'undefined') {
287
+ return 0;
288
+ }
289
+
290
+ return index;
291
+ }
292
+
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
+ }
299
+
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);
596
313
  }
597
314
 
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);
315
+ for (let j = 0; j < dataMultiplier; j++) {
316
+ dataIndexes.push(...baseDataIndexes);
317
+ }
618
318
 
619
- if (
620
- !mountedRef.current ||
621
- !carouselRef.current ||
622
- typeof offset === 'undefined'
623
- ) {
624
- return;
319
+ dataIndexes.unshift(...baseDataIndexes.slice(-remainder));
320
+ return dataIndexes[index];
321
+ } else {
322
+ return index + dataLength - loopClonesPerSide;
625
323
  }
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);
626
332
 
627
- const multiplier = currentScrollOffsetRef.current === 0 ? 1 : -1;
628
- const scrollDelta = scrollValue * multiplier;
333
+ if (!itemsLength || index > itemsLength - 1 || index < 0) {
334
+ return 0;
335
+ }
629
336
 
630
- scrollTo({ offset: offset + scrollDelta, animated: false });
337
+ return this._enableLoop() ? index + loopClonesPerSide : index;
338
+ }
631
339
 
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
- }
340
+ _getWrappedRef() {
341
+ return this._carouselRef;
342
+ }
649
343
 
650
- let repositionTo = index;
344
+ _getScrollEnabled() {
345
+ return this._scrollEnabled;
346
+ }
651
347
 
652
- if (index >= dataLength + loopClonesPerSide) {
653
- repositionTo = index - dataLength;
654
- } else if (index < loopClonesPerSide) {
655
- repositionTo = index + dataLength;
656
- }
348
+ _setScrollEnabled(scrollEnabled = true) {
349
+ this._scrollEnabled = scrollEnabled;
350
+ }
351
+
352
+ _getItemMainDimension() {
353
+ const { itemWidth } = this.state;
354
+ const { full } = this.props;
355
+ return full ? itemWidth : itemWidth + Spacing.S;
356
+ }
657
357
 
658
- snapToItem(repositionTo, animated, false);
659
- },
660
- [data, loopClonesPerSide, shouldRepositionScroll],
661
- );
358
+ _getItemScrollOffset(index: number) {
359
+ return (
360
+ this._positions && this._positions[index] && this._positions[index].start
361
+ );
362
+ }
662
363
 
663
- const scrollTo = useCallback(
664
- ({
665
- offset,
364
+ _getItemLayout(_: any, index: number) {
365
+ const itemMainDimension = this._getItemMainDimension();
366
+ return {
666
367
  index,
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
- }
368
+ length: itemMainDimension,
369
+ offset: itemMainDimension * index,
370
+ };
371
+ }
681
372
 
682
- let scrollToOffset;
683
- if (typeof index !== 'undefined') {
684
- scrollToOffset = getItemScrollOffset(index);
685
- } else {
686
- scrollToOffset = offset;
687
- }
373
+ _getKeyExtractor(_: any, index: any) {
374
+ return `flatlist-item-${index}`;
375
+ }
688
376
 
689
- if (typeof scrollToOffset === 'undefined') {
690
- return;
691
- }
377
+ _getScrollOffset(event: NativeSyntheticEvent<NativeScrollEvent>) {
378
+ return event.nativeEvent.contentOffset.x;
379
+ }
692
380
 
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
- }
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
+ }
718
389
 
719
- onTouchStart && onTouchStart(event);
720
- },
721
- [getScrollEnabled, onTouchStart],
722
- );
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;
723
396
 
724
- const handleTouchEnd = useCallback(
725
- (event: any) => {
726
- if (
727
- getScrollEnabled() &&
728
- autoplayRef.current &&
729
- !autoplayingRef.current
730
- ) {
731
- startAutoplay();
732
- }
397
+ if (offset <= 0) {
398
+ return 0;
399
+ }
733
400
 
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
- }
401
+ if (
402
+ this._positions[lastIndex] &&
403
+ offset >= this._positions[lastIndex].start
404
+ ) {
405
+ return lastIndex;
406
+ }
755
407
 
408
+ for (let i = 0; i < this._positions.length; i++) {
409
+ const { start, end } = this._positions[i];
756
410
  if (
757
- (IS_IOS && scrollOffset >= lastItemScrollOffset) ||
758
- (IS_ANDROID &&
759
- Math.floor(scrollOffset) >= Math.floor(lastItemScrollOffset))
411
+ center + activeSlideOffset >= start &&
412
+ center - activeSlideOffset <= end
760
413
  ) {
761
- activeItemRef.current = nextActiveItem;
762
- repositionScroll(nextActiveItem);
414
+ itemIndex = i;
415
+ break;
763
416
  }
417
+ }
764
418
 
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
- }
419
+ return itemIndex || 0;
420
+ }
802
421
 
803
- onMomentumScrollEnd && onMomentumScrollEnd(event);
422
+ _initPositionsAndInterpolators(props = this.props) {
423
+ const { data } = props;
424
+ const itemMainDimension = this._getItemMainDimension();
804
425
 
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
- }
426
+ if (!data || !data.length) {
427
+ return;
428
+ }
837
429
 
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
- }
430
+ const interpolators: any[] = [];
431
+ this._positions = [];
855
432
 
856
- if (!index || index < 0) {
857
- index = 0;
858
- } else if (itemsLength > 0 && index >= itemsLength) {
859
- index = itemsLength - 1;
860
- }
433
+ this._getCustomData(props).forEach((_itemData, index) => {
434
+ const _index = this._getCustomIndex(index, props);
435
+ let animatedValue;
861
436
 
862
- if (index === activeItemRef.current && !forceScrollTo) {
863
- return;
864
- }
437
+ this._positions[index] = {
438
+ start: index * itemMainDimension,
439
+ end: index * itemMainDimension + itemMainDimension,
440
+ };
865
441
 
866
- const offset = getItemScrollOffset(index);
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
+ );
867
449
 
868
- if (offset === undefined) {
869
- return;
450
+ animatedValue = this._scrollPos.interpolate({
451
+ ...interpolator,
452
+ extrapolate: 'clamp',
453
+ });
870
454
  }
871
455
 
872
- scrollTo({ offset, animated });
456
+ interpolators.push(animatedValue);
457
+ });
873
458
 
874
- const requiresManualTrigger = !animated || IS_ANDROID;
875
- if (requiresManualTrigger) {
876
- activeItemRef.current = index;
459
+ this.setState({ interpolators });
460
+ }
877
461
 
878
- if (fireCallback) {
879
- onSnapToItem && onSnapToItem(getDataIndex(index));
880
- }
462
+ _repositionScroll(index: number, animated = false) {
463
+ const { data, loopClonesPerSide = 3 } = this.props;
464
+ const dataLength = data && data.length;
881
465
 
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) {
466
+ if (typeof index === 'undefined' || !this._shouldRepositionScroll(index)) {
912
467
  return;
913
468
  }
914
469
 
915
- if (autoplayTimeoutRef.current) {
916
- clearTimeout(autoplayTimeoutRef.current);
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]);
470
+ let repositionTo = index;
927
471
 
928
- const pauseAutoPlay = useCallback(() => {
929
- autoplayingRef.current = false;
930
- if (autoplayTimeoutRef.current) {
931
- clearTimeout(autoplayTimeoutRef.current);
932
- }
933
- if (enableAutoplayTimeoutRef.current) {
934
- clearTimeout(enableAutoplayTimeoutRef.current);
472
+ if (index >= dataLength + loopClonesPerSide) {
473
+ repositionTo = index - dataLength;
474
+ } else if (index < loopClonesPerSide) {
475
+ repositionTo = index + dataLength;
935
476
  }
936
- if (autoplayIntervalRef.current) {
937
- clearInterval(autoplayIntervalRef.current);
477
+
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();
938
486
  }
939
- }, []);
940
487
 
941
- const stopAutoplay = useCallback(() => {
942
- autoplayRef.current = false;
943
- pauseAutoPlay();
944
- }, [pauseAutoPlay]);
488
+ onTouchStart && onTouchStart(event);
489
+ }
945
490
 
946
- const snapToItemPublic = useCallback(
947
- (index: number, animated = true, fireCallback = true) => {
948
- if (!index || index < 0) {
949
- index = 0;
950
- }
491
+ _onTouchEnd(event: GestureResponderEvent) {
492
+ const { onTouchEnd } = this.props;
951
493
 
952
- const positionIndex = getPositionIndex(index);
494
+ if (
495
+ this._getScrollEnabled() !== false &&
496
+ this._autoplay &&
497
+ !this._autoplaying
498
+ ) {
499
+ this.startAutoplay();
500
+ }
953
501
 
954
- if (positionIndex === activeItemRef.current) {
955
- return;
956
- }
502
+ onTouchEnd && onTouchEnd(event);
503
+ }
957
504
 
958
- snapToItem(positionIndex, animated, fireCallback);
959
- },
960
- [getPositionIndex, snapToItem],
961
- );
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);
962
513
 
963
- const snapToNext = useCallback(
964
- (animated = true, fireCallback = true) => {
965
- const itemsLength = getCustomDataLength();
514
+ this._currentScrollOffset = scrollOffset;
966
515
 
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
- );
1004
- }
1005
- if (vertical && (!sliderHeight || !itemHeight)) {
1006
- console.error(
1007
- `${pluginName}: You need to specify both 'sliderHeight' and 'itemHeight' for vertical carousels`,
1008
- );
516
+ if (nextActiveItem !== this._onScrollActiveItem) {
517
+ this._onScrollActiveItem = nextActiveItem;
518
+ onScrollIndexChanged &&
519
+ onScrollIndexChanged(this._getDataIndex(nextActiveItem));
520
+
521
+ onSnapToItem && onSnapToItem(this._getDataIndex(nextActiveItem));
1009
522
  }
1010
- }, [vertical, sliderWidth, itemWidth, sliderHeight, itemHeight]);
1011
523
 
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 } } }];
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);
531
+ }
1022
532
 
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;
1031
- }
1032
- argMapping.push(...(onScroll as any)._argMapping);
533
+ if (typeof onScroll === 'function' && event) {
534
+ onScroll(event);
1033
535
  }
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
- }
536
+ }
1081
537
 
1082
- const apparitionCallback = () => {
1083
- if (apparitionDelay) {
1084
- setHideCarousel(false);
1085
- }
1086
- if (autoplay) {
1087
- startAutoplay();
1088
- }
1089
- };
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);
1090
546
 
1091
- if (needsScrollView()) {
1092
- const _firstItem = getFirstItem(firstItem);
1093
- snapToItem(_firstItem, false, false, true);
1094
- }
547
+ if (nextActiveItem !== this._activeItem) {
548
+ this._activeItem = nextActiveItem;
549
+ onSnapToItem && onSnapToItem(this._getDataIndex(nextActiveItem));
1095
550
 
1096
- if (apparitionDelay) {
1097
- apparitionTimeoutRef.current = setTimeout(() => {
1098
- apparitionCallback();
1099
- }, apparitionDelay);
1100
- } else {
1101
- apparitionCallback();
551
+ if (hasSnapped && IS_ANDROID) {
552
+ this._repositionScroll(nextActiveItem);
553
+ } else if (IS_IOS) {
554
+ this._repositionScroll(nextActiveItem);
1102
555
  }
1103
- }, 1);
556
+ }
1104
557
 
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
- }, []);
558
+ onMomentumScrollEnd && onMomentumScrollEnd(event);
1122
559
 
1123
- // componentDidUpdate - handle scrollEnabled changes
1124
- useEffect(() => {
1125
- if (scrollEnabledProp !== scrollEnabledRef.current) {
1126
- setScrollEnabled(scrollEnabledProp);
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);
1127
566
  }
1128
- }, [scrollEnabledProp, setScrollEnabled]);
567
+ }
1129
568
 
1130
- // componentDidUpdate - handle data/size changes
1131
- useEffect(() => {
1132
- const itemsLength = getCustomDataLength();
569
+ _onLayout(event: LayoutChangeEvent) {
570
+ const { onLayout, visibleItem = 1 } = this.props;
1133
571
 
1134
- if (!itemsLength) {
1135
- return;
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;
1136
590
  }
1137
591
 
1138
- const nextFirstItem = getFirstItem(firstItem);
1139
- let nextActiveItem =
1140
- typeof activeItemRef.current !== 'undefined'
1141
- ? activeItemRef.current
1142
- : nextFirstItem;
592
+ this.setState({ containerWidth, itemWidth });
1143
593
 
1144
- const hasNewSize =
1145
- (vertical &&
1146
- (previousItemsLengthRef.current !== itemsLength ||
1147
- previousItemsLengthRef.current === 0)) ||
1148
- (!vertical &&
1149
- (previousItemsLengthRef.current !== itemsLength ||
1150
- previousItemsLengthRef.current === 0));
594
+ onLayout && onLayout(event);
595
+ }
1151
596
 
1152
- if (nextActiveItem > itemsLength - 1) {
1153
- nextActiveItem = itemsLength - 1;
597
+ _getPositionIndex(index: number) {
598
+ const { loop, loopClonesPerSide = 3 } = this.props;
599
+ return loop ? index + loopClonesPerSide : index;
600
+ }
601
+
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) {
612
+ return;
1154
613
  }
1155
614
 
1156
- if (interpolators.length !== itemsLength || hasNewSize) {
1157
- activeItemRef.current = nextActiveItem;
1158
- previousItemsLengthRef.current = itemsLength;
615
+ if (!index || index < 0) {
616
+ index = 0;
617
+ } else if (itemsLength > 0 && index >= itemsLength) {
618
+ index = itemsLength - 1;
619
+ }
1159
620
 
1160
- initPositionsAndInterpolators();
1161
- if (previousItemsLengthRef.current > itemsLength) {
1162
- hackActiveSlideAnimation(nextActiveItem);
621
+ if (index === this._activeItem && !forceScrollTo) {
622
+ return;
623
+ }
624
+
625
+ this._carouselRef.scrollToIndex({
626
+ index,
627
+ animated: true,
628
+ });
629
+
630
+ const requiresManualTrigger = !animated || IS_ANDROID;
631
+ if (requiresManualTrigger) {
632
+ this._activeItem = index;
633
+
634
+ if (fireCallback) {
635
+ onSnapToItem && onSnapToItem(this._getDataIndex(index));
1163
636
  }
1164
637
 
1165
- if (hasNewSize) {
1166
- snapToItem(nextActiveItem, false, false, true);
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
+ }
1167
646
  }
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);
1175
647
  }
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
- }
648
+ }
1200
649
 
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);
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];
1300
655
 
1301
- const specificProps = !needsScrollView()
1302
- ? {
1303
- initialNumToRender,
1304
- maxToRenderPerBatch,
1305
- windowSize,
1306
- }
1307
- : {};
656
+ if (typeof animatedValue === 'undefined') {
657
+ return null;
658
+ }
1308
659
 
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
- ];
660
+ const animate = this._shouldAnimateSlides();
661
+ const Component = animate ? Animated.View : View;
662
+ const mainDimension = { width: itemWidth };
1349
663
 
1350
- const innerMarginStyle = vertical
1351
- ? {
1352
- paddingTop: getContainerInnerMargin(),
1353
- paddingBottom: getContainerInnerMargin(true),
1354
- }
664
+ let spacingStyle: ViewStyle = this.props.loop
665
+ ? { marginLeft: Spacing.S }
1355
666
  : {
1356
- paddingLeft: getContainerInnerMargin(),
1357
- paddingRight: getContainerInnerMargin(true),
667
+ marginLeft: index === 0 ? Spacing.M : 0,
668
+ marginRight:
669
+ index === this._getCustomDataLength() - 1 ? Spacing.M : Spacing.S,
1358
670
  };
1359
671
 
1360
- const contentContainerStyle = [
1361
- !useExperimentalSnap ? innerMarginStyle : {},
1362
- contentContainerCustomStyle || {},
1363
- ];
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
+ }
1364
694
 
1365
- const snapProps = useExperimentalSnap
1366
- ? {
1367
- disableIntervalMomentum,
1368
- snapToAlignment: activeSlideAlignment,
1369
- snapToInterval: snapToInterval || getItemMainDimension(),
1370
- }
1371
- : {
1372
- snapToOffsets: getSnapOffsets(),
1373
- };
695
+ startAutoplay() {
696
+ const { autoplayInterval, autoplayDelay } = this.props;
697
+ this._autoplay = true;
698
+
699
+ if (this._autoplaying) {
700
+ return;
701
+ }
1374
702
 
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
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();
1384
709
  }
1385
- : {};
710
+ }, autoplayInterval);
711
+ }, autoplayDelay);
712
+ }
1386
713
 
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;
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);
1436
720
  }
1437
721
 
1438
- const overridableProps = getComponentOverridableProps();
1439
- const staticProps = getComponentStaticProps();
722
+ stopAutoplay() {
723
+ this._autoplay = false;
724
+ this.pauseAutoPlay();
725
+ }
1440
726
 
1441
- // Filter out props that should not be passed to FlatList/ScrollView
1442
- const { renderItem: _, ...propsWithoutRenderItem } = props;
727
+ snapToItem(index: number, animated = true, fireCallback = true) {
728
+ if (!index || index < 0) {
729
+ index = 0;
730
+ }
1443
731
 
1444
- const componentProps: any = {
1445
- ...overridableProps,
1446
- ...propsWithoutRenderItem,
1447
- ...staticProps,
1448
- };
732
+ const positionIndex = this._getPositionIndex(index);
733
+
734
+ if (positionIndex === this._activeItem) {
735
+ return;
736
+ }
1449
737
 
1450
- const ScrollViewComponent =
1451
- typeof useScrollView === 'function' ? useScrollView : Animated.ScrollView;
738
+ this._snapToItem(positionIndex, animated, fireCallback);
739
+ }
1452
740
 
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
- });
741
+ snapToNext(animated = true, fireCallback = true) {
742
+ const itemsLength = this._getCustomDataLength();
743
+
744
+ let newIndex = this._activeItem + 1;
745
+ if (newIndex > itemsLength - 1) {
746
+ newIndex = 0;
747
+ }
748
+ this._snapToItem(newIndex, animated, fireCallback);
749
+ }
750
+
751
+ snapToPrev(animated = true, fireCallback = true) {
752
+ const itemsLength = this._getCustomDataLength();
1466
753
 
1467
- Carousel.displayName = 'Carousel';
754
+ let newIndex = this._activeItem - 1;
755
+ if (newIndex < 0) {
756
+ newIndex = itemsLength - 1;
757
+ }
758
+ this._snapToItem(newIndex, animated, fireCallback);
759
+ }
760
+
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
+ };
793
+
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
+ }
1468
838
 
1469
839
  export { Carousel };
1470
- export type { CarouselRef, CarouselProps };
840
+ export type { CarouselProps, CarouselRef };