@momo-kits/carousel 0.151.1-test.4 → 0.151.1-test.5

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 (3) hide show
  1. package/PROPS.md +188 -0
  2. package/index.tsx +465 -771
  3. package/package.json +20 -20
package/index.tsx CHANGED
@@ -1,833 +1,527 @@
1
- import React from 'react';
1
+ import React, {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useImperativeHandle,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from 'react';
2
10
  import {
3
11
  Animated,
4
12
  Dimensions,
13
+ FlatList,
5
14
  GestureResponderEvent,
6
15
  LayoutChangeEvent,
7
16
  NativeScrollEvent,
8
17
  NativeSyntheticEvent,
9
18
  Platform,
10
- View,
11
- ViewStyle,
19
+ StyleSheet,
20
+ View
12
21
  } from 'react-native';
13
- import { defaultAnimatedStyles, defaultScrollInterpolator } from './animation';
14
- import { CarouselProps, CarouselRef, CarouselState, Position } from './types';
15
- import { Spacing } from '@momo-kits/foundation';
16
-
17
- const IS_ANDROID = Platform.OS === 'android';
18
- 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
- };
47
-
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
- };
79
-
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
- }
101
-
102
- componentDidMount() {
103
- const { apparitionDelay, autoplay } = this.props;
104
-
105
- this._mounted = true;
106
- this._initPositionsAndInterpolators();
107
-
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
- }
113
-
114
- const apparitionCallback = () => {
115
- if (apparitionDelay) {
116
- this.setState({ hideCarousel: false });
117
- }
118
- if (autoplay) {
119
- this.startAutoplay();
120
- }
121
- };
122
-
123
- if (apparitionDelay) {
124
- this._apparitionTimeout = setTimeout(() => {
125
- apparitionCallback();
126
- }, apparitionDelay);
127
- } else {
128
- apparitionCallback();
129
- }
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
-
138
- if (!itemsLength) {
139
- return;
140
- }
141
-
142
- const nextFirstItem = this._getFirstItem(firstItem, this.props);
143
- let nextActiveItem =
144
- typeof this._activeItem !== 'undefined'
145
- ? this._activeItem
146
- : nextFirstItem;
147
-
148
- if (nextActiveItem > itemsLength - 1) {
149
- nextActiveItem = itemsLength - 1;
22
+ import { CarouselProps, CarouselRef } from './types';
23
+
24
+ const { width: viewportWidth } = Dimensions.get('window');
25
+
26
+ const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
27
+
28
+ const Carousel = forwardRef<CarouselRef, CarouselProps>((props, ref) => {
29
+ const {
30
+ data,
31
+ renderItem,
32
+ keyExtractor,
33
+ firstItem = 0,
34
+ itemWidth: itemWidthProp,
35
+ visibleItem = 1,
36
+ full = false,
37
+ activeSlideOffset = 20,
38
+ scrollEnabled = true,
39
+ enableSnap = true,
40
+ snapToInterval: snapToIntervalProp,
41
+ disableIntervalMomentum = Platform.OS === 'android',
42
+ loop = false,
43
+ loopClonesPerSide = 3,
44
+ autoplay = false,
45
+ autoplayDelay = 1000,
46
+ autoplayInterval = 3000,
47
+ inactiveSlideOpacity = 1,
48
+ inactiveSlideScale = 1,
49
+ apparitionDelay = 0,
50
+ style,
51
+ slideStyle,
52
+ contentContainerStyle,
53
+ onSnapToItem,
54
+ onScrollIndexChanged,
55
+ onScroll: onScrollProp,
56
+ onMomentumScrollEnd: onMomentumScrollEndProp,
57
+ onTouchStart: onTouchStartProp,
58
+ onTouchEnd: onTouchEndProp,
59
+ onLayout: onLayoutProp,
60
+ getItemLayout: getItemLayoutProp,
61
+ } = props;
62
+
63
+ const flatListRef = useRef<FlatList>(null);
64
+ const autoplayTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
65
+ const scrollXRef = useRef(new Animated.Value(0)).current;
66
+ const containerWidthRef = useRef(viewportWidth);
67
+ const isAutoplayPausedRef = useRef(false);
68
+ const currentIndexRef = useRef(firstItem);
69
+
70
+ const [currentIndex, setCurrentIndex] = useState(firstItem);
71
+ const [containerWidth, setContainerWidth] = useState(viewportWidth);
72
+ const [isVisible, setIsVisible] = useState(apparitionDelay === 0);
73
+
74
+ const itemWidth = useMemo(() => {
75
+ if (full) {
76
+ return containerWidth;
150
77
  }
151
-
152
- if (scrollEnabled !== prevProps.scrollEnabled) {
153
- this._setScrollEnabled(scrollEnabled);
78
+ if (itemWidthProp) {
79
+ return itemWidthProp;
154
80
  }
81
+ const spacing = visibleItem > 1 ? 10 : 0;
82
+ return (containerWidth - spacing * (visibleItem - 1)) / visibleItem;
83
+ }, [containerWidth, full, itemWidthProp, visibleItem]);
155
84
 
156
- if (interpolators.length !== itemsLength) {
157
- this._activeItem = nextActiveItem;
158
- this._previousItemsLength = itemsLength;
159
-
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);
85
+ const snapToInterval = useMemo(() => {
86
+ if (snapToIntervalProp !== undefined) {
87
+ return snapToIntervalProp;
168
88
  }
89
+ return itemWidth;
90
+ }, [itemWidth, snapToIntervalProp]);
169
91
 
170
- if (this.props.onScroll !== prevProps.onScroll) {
171
- this._setScrollHandler(this.props);
92
+ const dataWithClones = useMemo(() => {
93
+ if (!loop || data.length === 0) {
94
+ return data;
172
95
  }
173
- }
174
-
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
- }
188
96
 
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
- ];
198
-
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);
97
+ const clones = [];
98
+ for (let i = 0; i < loopClonesPerSide; i++) {
99
+ const startIndex = data.length - loopClonesPerSide + i;
100
+ const endIndex = i;
101
+ clones.push(data[startIndex >= 0 ? startIndex : data.length + startIndex]);
209
102
  }
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
103
 
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)
231
- );
232
- }
233
-
234
- _isMultiple(x: number, y: number) {
235
- return Math.round(Math.round(x / y) / (1 / y)) === Math.round(x);
236
- }
104
+ const result = [...clones, ...data];
237
105
 
238
- _getCustomData(props = this.props) {
239
- const { data, loopClonesPerSide = 3 } = props;
240
- const dataLength = data && data.length;
241
-
242
- if (!dataLength) {
243
- return [];
106
+ for (let i = 0; i < loopClonesPerSide; i++) {
107
+ result.push(data[i % data.length]);
244
108
  }
245
109
 
246
- if (!this._enableLoop()) {
247
- return data;
248
- }
110
+ return result;
111
+ }, [data, loop, loopClonesPerSide]);
249
112
 
250
- let previousItems = [];
251
- let nextItems = [];
113
+ const getRealIndex = useCallback(
114
+ (index: number) => {
115
+ if (!loop) return index;
116
+
117
+ const dataLength = data.length;
118
+ if (dataLength === 0) return 0;
252
119
 
253
- if (loopClonesPerSide > dataLength) {
254
- const dataMultiplier = Math.floor(loopClonesPerSide / dataLength);
255
- const remainder = loopClonesPerSide % dataLength;
256
-
257
- for (let i = 0; i < dataMultiplier; i++) {
258
- previousItems.push(...data);
259
- nextItems.push(...data);
120
+ let realIndex = index - loopClonesPerSide;
121
+
122
+ if (realIndex < 0) {
123
+ realIndex = dataLength + realIndex;
124
+ } else if (realIndex >= dataLength) {
125
+ realIndex = realIndex % dataLength;
260
126
  }
261
127
 
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;
275
-
276
- if (!dataLength) {
277
- return 0;
278
- }
279
-
280
- return this._enableLoop() ? dataLength + 2 * loopClonesPerSide : dataLength;
281
- }
128
+ return realIndex;
129
+ },
130
+ [loop, loopClonesPerSide, data.length]
131
+ );
132
+
133
+ const getLoopIndex = useCallback(
134
+ (realIndex: number) => {
135
+ if (!loop) return realIndex;
136
+ return realIndex + loopClonesPerSide;
137
+ },
138
+ [loop, loopClonesPerSide]
139
+ );
140
+
141
+ const snapToItem = useCallback(
142
+ (index: number, animated = true, fireCallback = true) => {
143
+ if (!flatListRef.current || index < 0 || index >= data.length) {
144
+ return;
145
+ }
282
146
 
283
- _getCustomIndex(index: number, props = this.props) {
284
- const itemsLength = this._getCustomDataLength(props);
147
+ const loopIndex = getLoopIndex(index);
148
+ const offset = loopIndex * itemWidth;
285
149
 
286
- if (!itemsLength || typeof index === 'undefined') {
287
- return 0;
288
- }
150
+ flatListRef.current.scrollToOffset({
151
+ offset,
152
+ animated,
153
+ });
289
154
 
290
- return index;
291
- }
155
+ currentIndexRef.current = index;
156
+ setCurrentIndex(index);
292
157
 
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;
158
+ if (fireCallback && onSnapToItem) {
159
+ onSnapToItem(index);
160
+ }
161
+ },
162
+ [data.length, getLoopIndex, itemWidth, onSnapToItem]
163
+ );
164
+
165
+ const snapToNext = useCallback(
166
+ (animated = true, fireCallback = true) => {
167
+ const nextIndex = (currentIndexRef.current + 1) % data.length;
168
+ snapToItem(nextIndex, animated, fireCallback);
169
+ },
170
+ [data.length, snapToItem]
171
+ );
172
+
173
+ const snapToPrev = useCallback(
174
+ (animated = true, fireCallback = true) => {
175
+ const prevIndex =
176
+ currentIndexRef.current === 0 ? data.length - 1 : currentIndexRef.current - 1;
177
+ snapToItem(prevIndex, animated, fireCallback);
178
+ },
179
+ [data.length, snapToItem]
180
+ );
181
+
182
+ const stopAutoplay = useCallback(() => {
183
+ if (autoplayTimerRef.current) {
184
+ clearInterval(autoplayTimerRef.current);
185
+ autoplayTimerRef.current = null;
186
+ }
187
+ }, []);
188
+
189
+ const pauseAutoPlay = useCallback(() => {
190
+ isAutoplayPausedRef.current = true;
191
+ stopAutoplay();
192
+ }, [stopAutoplay]);
193
+
194
+ const startAutoplay = useCallback(() => {
195
+ stopAutoplay();
196
+ isAutoplayPausedRef.current = false;
197
+
198
+ if (!autoplay || data.length <= 1) {
199
+ return;
298
200
  }
299
201
 
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);
202
+ const startTimer = () => {
203
+ autoplayTimerRef.current = setInterval(() => {
204
+ if (!isAutoplayPausedRef.current) {
205
+ snapToNext(true, true);
313
206
  }
207
+ }, autoplayInterval);
208
+ };
314
209
 
315
- for (let j = 0; j < dataMultiplier; j++) {
316
- dataIndexes.push(...baseDataIndexes);
210
+ if (autoplayDelay > 0) {
211
+ setTimeout(startTimer, autoplayDelay);
212
+ } else {
213
+ startTimer();
214
+ }
215
+ }, [autoplay, autoplayDelay, autoplayInterval, data.length, snapToNext, stopAutoplay]);
216
+
217
+ const handleLoopReposition = useCallback(
218
+ (index: number) => {
219
+ if (!loop || !flatListRef.current) return;
220
+
221
+ const dataLength = data.length;
222
+ if (dataLength === 0) return;
223
+
224
+ if (index < loopClonesPerSide) {
225
+ const realIndex = dataLength - (loopClonesPerSide - index);
226
+ const loopIndex = getLoopIndex(realIndex);
227
+ setTimeout(() => {
228
+ flatListRef.current?.scrollToOffset({
229
+ offset: loopIndex * itemWidth,
230
+ animated: false,
231
+ });
232
+ }, 50);
233
+ } else if (index >= dataLength + loopClonesPerSide) {
234
+ const realIndex = index - (dataLength + loopClonesPerSide);
235
+ const loopIndex = getLoopIndex(realIndex);
236
+ setTimeout(() => {
237
+ flatListRef.current?.scrollToOffset({
238
+ offset: loopIndex * itemWidth,
239
+ animated: false,
240
+ });
241
+ }, 50);
242
+ }
243
+ },
244
+ [loop, data.length, loopClonesPerSide, itemWidth, getLoopIndex]
245
+ );
246
+
247
+ const handleScroll = useCallback(
248
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
249
+ const offsetX = event.nativeEvent.contentOffset.x;
250
+ const centerX = offsetX + (containerWidth / 2);
251
+
252
+ let index = Math.floor(centerX / itemWidth);
253
+ const itemCenterX = (index * itemWidth) + (itemWidth / 2);
254
+ const distanceFromCenter = Math.abs(centerX - itemCenterX);
255
+
256
+ if (distanceFromCenter > activeSlideOffset) {
257
+ const nextItemCenterX = ((index + 1) * itemWidth) + (itemWidth / 2);
258
+ if (Math.abs(centerX - nextItemCenterX) < distanceFromCenter) {
259
+ index = index + 1;
317
260
  }
318
-
319
- dataIndexes.unshift(...baseDataIndexes.slice(-remainder));
320
- return dataIndexes[index];
321
- } else {
322
- return index + dataLength - loopClonesPerSide;
323
261
  }
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
-
333
- if (!itemsLength || index > itemsLength - 1 || index < 0) {
334
- return 0;
335
- }
336
-
337
- return this._enableLoop() ? index + loopClonesPerSide : index;
338
- }
339
-
340
- _getWrappedRef() {
341
- return this._carouselRef;
342
- }
343
-
344
- _getScrollEnabled() {
345
- return this._scrollEnabled;
346
- }
347
-
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
- }
357
-
358
- _getItemScrollOffset(index: number) {
359
- return (
360
- this._positions && this._positions[index] && this._positions[index].start
361
- );
362
- }
363
-
364
- _getItemLayout(_: any, index: number) {
365
- const itemMainDimension = this._getItemMainDimension();
366
- return {
367
- index,
368
- length: itemMainDimension,
369
- offset: itemMainDimension * index,
370
- };
371
- }
372
-
373
- _getKeyExtractor(_: any, index: any) {
374
- return `flatlist-item-${index}`;
375
- }
376
-
377
- _getScrollOffset(event: NativeSyntheticEvent<NativeScrollEvent>) {
378
- return event.nativeEvent.contentOffset.x;
379
- }
380
-
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
- }
389
-
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;
396
-
397
- if (offset <= 0) {
398
- return 0;
399
- }
400
-
401
- let minDistance = Infinity;
402
- for (let i = 0; i < this._positions.length; i++) {
403
- const { start, end } = this._positions[i];
404
- const itemCenter = (start + end) / 2;
405
- const distance = Math.abs(itemCenter - center);
406
- if (distance < minDistance) {
407
- minDistance = distance;
408
- itemIndex = i;
262
+
263
+ const realIndex = getRealIndex(index);
264
+
265
+ if (realIndex !== currentIndexRef.current) {
266
+ currentIndexRef.current = realIndex;
267
+ setCurrentIndex(realIndex);
268
+
269
+ if (onScrollIndexChanged) {
270
+ onScrollIndexChanged(realIndex);
271
+ }
409
272
  }
410
- }
273
+ },
274
+ [itemWidth, containerWidth, activeSlideOffset, getRealIndex, onScrollIndexChanged]
275
+ );
411
276
 
412
- return itemIndex !== undefined ? itemIndex : 0;
413
- }
277
+ const handleMomentumScrollEnd = useCallback(
278
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
279
+ const offsetX = event.nativeEvent.contentOffset.x;
280
+ const index = Math.round(offsetX / itemWidth);
281
+ const realIndex = getRealIndex(index);
414
282
 
415
- _initPositionsAndInterpolators(props = this.props) {
416
- const { data } = props;
417
- const itemMainDimension = this._getItemMainDimension();
283
+ currentIndexRef.current = realIndex;
284
+ setCurrentIndex(realIndex);
418
285
 
419
- if (!data || !data.length) {
420
- return;
421
- }
422
-
423
- const interpolators: any[] = [];
424
- this._positions = [];
286
+ if (onSnapToItem) {
287
+ onSnapToItem(realIndex);
288
+ }
425
289
 
426
- this._getCustomData(props).forEach((_itemData, index) => {
427
- const _index = this._getCustomIndex(index, props);
428
- let animatedValue;
290
+ handleLoopReposition(index);
429
291
 
430
- this._positions[index] = {
431
- start: index * itemMainDimension,
432
- end: index * itemMainDimension + itemMainDimension,
292
+ if (onMomentumScrollEndProp) {
293
+ onMomentumScrollEndProp(event);
294
+ }
295
+ },
296
+ [itemWidth, getRealIndex, onSnapToItem, handleLoopReposition, onMomentumScrollEndProp]
297
+ );
298
+
299
+ const handleTouchStart = useCallback(
300
+ (event: any) => {
301
+ if (autoplay) {
302
+ pauseAutoPlay();
303
+ }
304
+ if (onTouchStartProp) {
305
+ onTouchStartProp(event);
306
+ }
307
+ },
308
+ [autoplay, pauseAutoPlay, onTouchStartProp]
309
+ );
310
+
311
+ const handleTouchEnd = useCallback(
312
+ (event: GestureResponderEvent) => {
313
+ if (autoplay && !isAutoplayPausedRef.current) {
314
+ startAutoplay();
315
+ }
316
+ if (onTouchEndProp) {
317
+ onTouchEndProp(event);
318
+ }
319
+ },
320
+ [autoplay, startAutoplay, onTouchEndProp]
321
+ );
322
+
323
+ const handleLayout = useCallback(
324
+ (event: LayoutChangeEvent) => {
325
+ const { width } = event.nativeEvent.layout;
326
+ containerWidthRef.current = width;
327
+ setContainerWidth(width);
328
+
329
+ if (onLayoutProp) {
330
+ onLayoutProp(event);
331
+ }
332
+ },
333
+ [onLayoutProp]
334
+ );
335
+
336
+ const getItemLayout = useCallback(
337
+ (data: any, index: number) => {
338
+ if (getItemLayoutProp) {
339
+ return getItemLayoutProp(data, index);
340
+ }
341
+ return {
342
+ length: itemWidth,
343
+ offset: itemWidth * index,
344
+ index,
433
345
  };
434
-
435
- if (!this._shouldAnimateSlides(props) || !this._scrollPos) {
436
- animatedValue = new Animated.Value(1);
437
- } else {
438
- let interpolator = defaultScrollInterpolator(
439
- _index,
440
- this.state.itemWidth,
346
+ },
347
+ [itemWidth, getItemLayoutProp]
348
+ );
349
+
350
+ const renderAnimatedItem = useCallback(
351
+ ({ item, index }: { item: any; index: number }) => {
352
+ const realIndex = getRealIndex(index);
353
+ const hasOpacityAnimation = inactiveSlideOpacity < 1;
354
+ const hasScaleAnimation = inactiveSlideScale < 1;
355
+
356
+ if (!hasOpacityAnimation && !hasScaleAnimation) {
357
+ return (
358
+ <View style={[{ width: itemWidth }, slideStyle]}>
359
+ {renderItem({ item, index: realIndex })}
360
+ </View>
441
361
  );
442
-
443
- animatedValue = this._scrollPos.interpolate({
444
- ...interpolator,
445
- extrapolate: 'clamp',
446
- });
447
- }
448
-
449
- interpolators.push(animatedValue);
450
- });
451
-
452
- this.setState({ interpolators });
453
- }
454
-
455
- _repositionScroll(index: number, animated = false) {
456
- const { data, loopClonesPerSide = 3 } = this.props;
457
- const dataLength = data && data.length;
458
-
459
- if (typeof index === 'undefined' || !this._shouldRepositionScroll(index)) {
460
- return;
461
- }
462
-
463
- let repositionTo = index;
464
-
465
- if (index >= dataLength + loopClonesPerSide) {
466
- repositionTo = index - dataLength;
467
- } else if (index < loopClonesPerSide) {
468
- repositionTo = index + dataLength;
469
- }
470
-
471
- this._snapToItem(repositionTo, animated, false);
472
- }
473
-
474
- _onTouchStart(event: any) {
475
- const { onTouchStart } = this.props;
476
-
477
- if (this._getScrollEnabled() !== false && this._autoplaying) {
478
- this.pauseAutoPlay();
479
- }
480
-
481
- onTouchStart && onTouchStart(event);
482
- }
483
-
484
- _onTouchEnd(event: GestureResponderEvent) {
485
- const { onTouchEnd } = this.props;
486
-
487
- if (
488
- this._getScrollEnabled() !== false &&
489
- this._autoplay &&
490
- !this._autoplaying
491
- ) {
492
- this.startAutoplay();
493
- }
494
-
495
- onTouchEnd && onTouchEnd(event);
496
- }
497
-
498
- _onScroll(event: NativeSyntheticEvent<NativeScrollEvent>) {
499
- const { onScroll, onScrollIndexChanged, onSnapToItem } = this.props;
500
- const scrollOffset = event
501
- ? this._getScrollOffset(event)
502
- : this._currentScrollOffset;
503
- const nextActiveItem = this._getActiveItem(scrollOffset);
504
- const dataLength = this._getCustomDataLength();
505
- const lastItemScrollOffset = this._getItemScrollOffset(dataLength - 1);
506
-
507
- this._currentScrollOffset = scrollOffset;
508
-
509
- if (nextActiveItem !== this._onScrollActiveItem) {
510
- this._onScrollActiveItem = nextActiveItem;
511
- const dataIndex = this._getDataIndex(nextActiveItem);
512
-
513
- onScrollIndexChanged && onScrollIndexChanged(dataIndex);
514
- onSnapToItem && onSnapToItem(dataIndex);
515
- }
516
-
517
- if (
518
- (IS_IOS && scrollOffset > lastItemScrollOffset) ||
519
- (IS_ANDROID &&
520
- Math.floor(scrollOffset) > Math.floor(lastItemScrollOffset))
521
- ) {
522
- this._activeItem = nextActiveItem;
523
- this._repositionScroll(nextActiveItem);
524
- }
525
-
526
- if (typeof onScroll === 'function' && event) {
527
- onScroll(event);
528
- }
529
- }
530
-
531
- _onMomentumScrollEnd(event: NativeSyntheticEvent<NativeScrollEvent>) {
532
- const { autoplayDelay, onMomentumScrollEnd, onSnapToItem } = this.props;
533
- const { itemWidth } = this.state;
534
- const scrollOffset = event
535
- ? this._getScrollOffset(event)
536
- : this._currentScrollOffset;
537
- const nextActiveItem = this._getActiveItem(scrollOffset);
538
- const hasSnapped = this._isMultiple(scrollOffset, itemWidth);
539
-
540
- if (nextActiveItem !== this._activeItem) {
541
- this._activeItem = nextActiveItem;
542
- onSnapToItem && onSnapToItem(this._getDataIndex(nextActiveItem));
543
-
544
- if (hasSnapped && IS_ANDROID) {
545
- this._repositionScroll(nextActiveItem);
546
- } else if (IS_IOS) {
547
- this._repositionScroll(nextActiveItem);
548
362
  }
549
- }
550
-
551
- onMomentumScrollEnd && onMomentumScrollEnd(event);
552
-
553
- if (IS_ANDROID && this._autoplay && !this._autoplaying) {
554
- if (this._enableAutoplayTimeout != null)
555
- clearTimeout(this._enableAutoplayTimeout);
556
- this._enableAutoplayTimeout = setTimeout(() => {
557
- this.startAutoplay();
558
- }, autoplayDelay);
559
- }
560
- }
561
-
562
- _onLayout(event: LayoutChangeEvent) {
563
- const { onLayout, visibleItem = 1 } = this.props;
564
-
565
- if (this._onLayoutInitDone) {
566
- this._initPositionsAndInterpolators();
567
- this._snapToItem(this._activeItem, false, false, true);
568
- } else {
569
- this._onLayoutInitDone = true;
570
- }
571
- const containerWidth = event.nativeEvent.layout.width;
572
- let itemWidth =
573
- this.props.visibleItem === 1
574
- ? screenWidth - Spacing.M * 2
575
- : Math.ceil(
576
- (containerWidth * 0.9 - visibleItem * Spacing.S) / visibleItem,
577
- );
578
- if (this.props.itemWidth) {
579
- itemWidth = this.props.itemWidth;
580
- }
581
- if (this.props.full) {
582
- itemWidth = containerWidth;
583
- }
584
-
585
- this.setState({ containerWidth, itemWidth });
586
-
587
- onLayout && onLayout(event);
588
- }
589
-
590
- _getPositionIndex(index: number) {
591
- const { loop, loopClonesPerSide = 3 } = this.props;
592
- return loop ? index + loopClonesPerSide : index;
593
- }
594
-
595
- _snapToItem(
596
- index: number,
597
- animated = true,
598
- fireCallback = true,
599
- forceScrollTo = false,
600
- ) {
601
- const { onSnapToItem } = this.props;
602
- const itemsLength = this._getCustomDataLength();
603
- const wrappedRef = this._getWrappedRef();
604
- if (!itemsLength || !wrappedRef) {
605
- return;
606
- }
607
-
608
- if (!index || index < 0) {
609
- index = 0;
610
- } else if (itemsLength > 0 && index >= itemsLength) {
611
- index = itemsLength - 1;
612
- }
613
-
614
- if (index === this._activeItem && !forceScrollTo) {
615
- return;
616
- }
617
-
618
- this._carouselRef.scrollToIndex({
619
- index,
620
- animated: true,
621
- });
622
363
 
623
- const requiresManualTrigger = !animated || IS_ANDROID;
624
- if (requiresManualTrigger) {
625
- this._activeItem = index;
626
-
627
- if (fireCallback) {
628
- onSnapToItem && onSnapToItem(this._getDataIndex(index));
364
+ const inputRange = [
365
+ (index - 1) * itemWidth,
366
+ index * itemWidth,
367
+ (index + 1) * itemWidth,
368
+ ];
369
+
370
+ const opacity = hasOpacityAnimation
371
+ ? scrollXRef.interpolate({
372
+ inputRange,
373
+ outputRange: [inactiveSlideOpacity, 1, inactiveSlideOpacity],
374
+ extrapolate: 'clamp',
375
+ })
376
+ : 1;
377
+
378
+ const scale = hasScaleAnimation
379
+ ? scrollXRef.interpolate({
380
+ inputRange,
381
+ outputRange: [inactiveSlideScale, 1, inactiveSlideScale],
382
+ extrapolate: 'clamp',
383
+ })
384
+ : 1;
385
+
386
+ return (
387
+ <Animated.View
388
+ style={[
389
+ { width: itemWidth },
390
+ slideStyle,
391
+ {
392
+ opacity,
393
+ transform: [{ scale }],
394
+ },
395
+ ]}
396
+ >
397
+ {renderItem({ item, index: realIndex })}
398
+ </Animated.View>
399
+ );
400
+ },
401
+ [
402
+ getRealIndex,
403
+ inactiveSlideOpacity,
404
+ inactiveSlideScale,
405
+ itemWidth,
406
+ slideStyle,
407
+ renderItem,
408
+ scrollXRef,
409
+ ]
410
+ );
411
+
412
+ const handleKeyExtractor = useCallback(
413
+ (item: any, index: number) => {
414
+ if (keyExtractor) {
415
+ return keyExtractor(item, index);
629
416
  }
630
-
631
- if (IS_ANDROID && this._shouldRepositionScroll(index)) {
632
- if (animated) {
633
- this._androidRepositioningTimeout = setTimeout(() => {
634
- this._repositionScroll(index, false);
635
- }, 400);
636
- } else {
637
- this._repositionScroll(index);
638
- }
417
+ return `carousel-item-${index}`;
418
+ },
419
+ [keyExtractor]
420
+ );
421
+
422
+ const onScrollEvent = useMemo(() => {
423
+ const scrollHandler = Animated.event(
424
+ [{ nativeEvent: { contentOffset: { x: scrollXRef } } }],
425
+ {
426
+ useNativeDriver: true,
427
+ listener: handleScroll,
639
428
  }
640
- }
641
- }
642
-
643
- _renderItem(info: { item: any; index: number }) {
644
- const { item, index } = info;
645
- const { interpolators, itemWidth } = this.state;
646
- const { slideStyle, full } = this.props;
647
- const animatedValue = interpolators && interpolators[index];
648
-
649
- if (typeof animatedValue === 'undefined') {
650
- return null;
651
- }
652
-
653
- const animate = this._shouldAnimateSlides();
654
- const Component = animate ? Animated.View : View;
655
- const mainDimension = { width: itemWidth };
656
-
657
- let spacingStyle: ViewStyle = this.props.loop
658
- ? { marginLeft: Spacing.S }
659
- : {
660
- marginLeft: index === 0 ? Spacing.M : 0,
661
- marginRight:
662
- index === this._getCustomDataLength() - 1 ? Spacing.M : Spacing.S,
663
- };
664
-
665
- if (full) {
666
- spacingStyle = {};
667
- }
668
- const animatedStyle = defaultAnimatedStyles(animatedValue, this.props);
669
- return (
670
- <Component
671
- style={[
672
- mainDimension,
673
- animatedStyle,
674
- { overflow: 'hidden' },
675
- spacingStyle,
676
- slideStyle,
677
- ]}
678
- pointerEvents="box-none"
679
- >
680
- {this.props.renderItem({
681
- item,
682
- index,
683
- })}
684
- </Component>
685
429
  );
686
- }
687
-
688
- startAutoplay() {
689
- const { autoplayInterval, autoplayDelay } = this.props;
690
- this._autoplay = true;
691
-
692
- if (this._autoplaying) {
693
- return;
694
- }
695
-
696
- if (this._autoplayTimeout != null) clearTimeout(this._autoplayTimeout);
697
- this._autoplayTimeout = setTimeout(() => {
698
- this._autoplaying = true;
699
- this._autoplayInterval = setInterval(() => {
700
- if (this._autoplaying) {
701
- this.snapToNext();
702
- }
703
- }, autoplayInterval);
704
- }, autoplayDelay);
705
- }
706
-
707
- pauseAutoPlay() {
708
- this._autoplaying = false;
709
- if (this._autoplayTimeout != null) clearTimeout(this._autoplayTimeout);
710
- if (this._enableAutoplayTimeout != null)
711
- clearTimeout(this._enableAutoplayTimeout);
712
- if (this._autoplayInterval != null) clearInterval(this._autoplayInterval);
713
- }
714
430
 
715
- stopAutoplay() {
716
- this._autoplay = false;
717
- this.pauseAutoPlay();
718
- }
719
-
720
- snapToItem(index: number, animated = true, fireCallback = true) {
721
- if (!index || index < 0) {
722
- index = 0;
431
+ if (onScrollProp) {
432
+ return (event: NativeSyntheticEvent<NativeScrollEvent>) => {
433
+ scrollHandler(event);
434
+ onScrollProp(event);
435
+ };
723
436
  }
724
437
 
725
- const positionIndex = this._getPositionIndex(index);
438
+ return scrollHandler;
439
+ }, [scrollXRef, handleScroll, onScrollProp]);
726
440
 
727
- if (positionIndex === this._activeItem) {
728
- return;
441
+ useEffect(() => {
442
+ if (firstItem > 0 && firstItem < data.length) {
443
+ setTimeout(() => {
444
+ snapToItem(firstItem, false, false);
445
+ }, 100);
729
446
  }
447
+ }, []);
730
448
 
731
- this._snapToItem(positionIndex, animated, fireCallback);
732
- }
733
-
734
- snapToNext(animated = true, fireCallback = true) {
735
- const itemsLength = this._getCustomDataLength();
736
-
737
- let newIndex = this._activeItem + 1;
738
- if (newIndex > itemsLength - 1) {
739
- newIndex = 0;
449
+ useEffect(() => {
450
+ if (autoplay && data.length > 1) {
451
+ startAutoplay();
740
452
  }
741
- this._snapToItem(newIndex, animated, fireCallback);
742
- }
743
-
744
- snapToPrev(animated = true, fireCallback = true) {
745
- const itemsLength = this._getCustomDataLength();
746
453
 
747
- let newIndex = this._activeItem - 1;
748
- if (newIndex < 0) {
749
- newIndex = itemsLength - 1;
750
- }
751
- this._snapToItem(newIndex, animated, fireCallback);
752
- }
753
-
754
- render() {
755
- const {
756
- loopClonesPerSide = 3,
757
- visibleItem = 1,
758
- firstItem = 0,
759
- getItemLayout,
760
- keyExtractor,
761
- style,
762
- disableIntervalMomentum,
763
- enableSnap,
764
- contentContainerStyle,
765
- } = this.props;
766
- const { hideCarousel } = this.state;
767
-
768
- const initialNumPerSide = this._enableLoop() ? loopClonesPerSide : 2;
769
- const initialNumToRender =
770
- visibleItem > 2
771
- ? visibleItem + initialNumPerSide * 2
772
- : initialNumPerSide * 2;
773
- const maxToRenderPerBatch = initialNumToRender;
774
- const windowSize = maxToRenderPerBatch;
775
-
776
- const snapToInterval = enableSnap
777
- ? this._getItemMainDimension()
778
- : undefined;
779
-
780
- const specificProps = {
781
- getItemLayout: getItemLayout || this._getItemLayout,
782
- initialScrollIndex: this._getFirstItem(firstItem),
783
- keyExtractor: keyExtractor || this._getKeyExtractor,
784
- renderItem: this._renderItem,
454
+ return () => {
455
+ stopAutoplay();
785
456
  };
786
-
787
- return (
788
- <Animated.FlatList
789
- {...this.props}
790
- {...specificProps}
791
- ref={c => {
792
- this._carouselRef = c;
793
- }}
794
- overScrollMode={'never'}
795
- snapToInterval={
796
- this.props.snapToInterval ? this.props.snapToInterval : snapToInterval
797
- }
457
+ }, [autoplay, data.length, startAutoplay, stopAutoplay]);
458
+
459
+ useEffect(() => {
460
+ if (apparitionDelay > 0) {
461
+ const timer = setTimeout(() => {
462
+ setIsVisible(true);
463
+ }, apparitionDelay);
464
+
465
+ return () => clearTimeout(timer);
466
+ }
467
+ }, [apparitionDelay]);
468
+
469
+ useImperativeHandle(
470
+ ref,
471
+ () => ({
472
+ snapToItem,
473
+ snapToNext,
474
+ snapToPrev,
475
+ startAutoplay,
476
+ pauseAutoplay: pauseAutoPlay,
477
+ stopAutoplay,
478
+ }),
479
+ [snapToItem, snapToNext, snapToPrev, startAutoplay, pauseAutoPlay, stopAutoplay] // eslint-disable-line react-hooks/exhaustive-deps
480
+ );
481
+
482
+ if (!isVisible) {
483
+ return <View style={[styles.container, style]} />;
484
+ }
485
+
486
+ return (
487
+ <View style={[styles.container, style]} onLayout={handleLayout}>
488
+ <AnimatedFlatList
489
+ ref={flatListRef}
490
+ data={dataWithClones}
491
+ renderItem={renderAnimatedItem}
492
+ keyExtractor={handleKeyExtractor}
493
+ horizontal
494
+ showsHorizontalScrollIndicator={false}
495
+ scrollEnabled={scrollEnabled}
496
+ scrollEventThrottle={16}
497
+ onScroll={onScrollEvent}
498
+ onMomentumScrollEnd={handleMomentumScrollEnd}
499
+ onTouchStart={handleTouchStart}
500
+ onTouchEnd={handleTouchEnd}
501
+ snapToInterval={enableSnap ? (snapToInterval as number) : undefined}
502
+ snapToAlignment={enableSnap ? 'start' : undefined}
503
+ decelerationRate={enableSnap ? 'fast' : 'normal'}
798
504
  disableIntervalMomentum={disableIntervalMomentum}
799
- pointerEvents={hideCarousel ? 'none' : 'auto'}
800
- decelerationRate={'fast'}
801
- numColumns={1}
802
- style={[
803
- style,
804
- { width: '100%', flexDirection: 'row' },
805
- hideCarousel ? { opacity: 0 } : {},
806
- ]}
807
- automaticallyAdjustContentInsets={false}
808
- directionalLockEnabled
505
+ getItemLayout={getItemLayout}
809
506
  contentContainerStyle={contentContainerStyle}
810
- disableScrollViewPanResponder={false}
811
- pinchGestureEnabled={false}
812
- scrollsToTop={false}
813
- showsHorizontalScrollIndicator={false}
814
- showsVerticalScrollIndicator={false}
815
- initialNumToRender={initialNumToRender}
816
- maxToRenderPerBatch={maxToRenderPerBatch}
817
- windowSize={windowSize}
818
- pagingEnabled={enableSnap}
819
- data={this._getCustomData()}
820
- horizontal
821
- scrollEventThrottle={1}
822
- onLayout={this._onLayout}
823
- onMomentumScrollEnd={this._onMomentumScrollEnd}
824
- onScroll={this._onScrollHandler}
825
- onTouchStart={this._onTouchStart}
826
- onTouchEnd={this._onTouchEnd}
507
+ removeClippedSubviews={false}
508
+ initialScrollIndex={loop && firstItem > 0 ? getLoopIndex(firstItem) : undefined}
509
+ initialNumToRender={Math.min(10, dataWithClones.length)}
510
+ maxToRenderPerBatch={5}
511
+ windowSize={5}
827
512
  />
828
- );
829
- }
830
- }
513
+ </View>
514
+ );
515
+ });
831
516
 
517
+ const styles = StyleSheet.create({
518
+ container: {
519
+ overflow: 'hidden',
520
+ },
521
+ });
522
+
523
+ Carousel.displayName = 'Carousel';
524
+
525
+ export default Carousel;
832
526
  export { Carousel };
833
- export type { CarouselProps, CarouselRef };
527
+