@momo-kits/carousel 0.151.1-beta.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 -778
  3. package/package.json +20 -20
package/index.tsx CHANGED
@@ -1,840 +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;
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;
140
77
  }
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;
78
+ if (itemWidthProp) {
79
+ return itemWidthProp;
150
80
  }
81
+ const spacing = visibleItem > 1 ? 10 : 0;
82
+ return (containerWidth - spacing * (visibleItem - 1)) / visibleItem;
83
+ }, [containerWidth, full, itemWidthProp, visibleItem]);
151
84
 
152
- if (scrollEnabled !== prevProps.scrollEnabled) {
153
- this._setScrollEnabled(scrollEnabled);
85
+ const snapToInterval = useMemo(() => {
86
+ if (snapToIntervalProp !== undefined) {
87
+ return snapToIntervalProp;
154
88
  }
89
+ return itemWidth;
90
+ }, [itemWidth, snapToIntervalProp]);
155
91
 
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);
92
+ const dataWithClones = useMemo(() => {
93
+ if (!loop || data.length === 0) {
94
+ return data;
168
95
  }
169
96
 
170
- if (this.props.onScroll !== prevProps.onScroll) {
171
- this._setScrollHandler(this.props);
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]);
172
102
  }
173
- }
174
103
 
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
- }
104
+ const result = [...clones, ...data];
188
105
 
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);
106
+ for (let i = 0; i < loopClonesPerSide; i++) {
107
+ result.push(data[i % data.length]);
209
108
  }
210
- this._onScrollHandler = Animated.event(argMapping, scrollEventConfig);
211
- }
212
109
 
213
- _enableLoop() {
214
- const { data, enableSnap, loop } = this.props;
215
- return enableSnap && loop && data && data.length && data.length > 1;
216
- }
110
+ return result;
111
+ }, [data, loop, loopClonesPerSide]);
217
112
 
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)
231
- );
232
- }
233
-
234
- _isMultiple(x: number, y: number) {
235
- return Math.round(Math.round(x / y) / (1 / y)) === Math.round(x);
236
- }
237
-
238
- _getCustomData(props = this.props) {
239
- const { data, loopClonesPerSide = 3 } = props;
240
- const dataLength = data && data.length;
241
-
242
- if (!dataLength) {
243
- return [];
244
- }
245
-
246
- if (!this._enableLoop()) {
247
- return data;
248
- }
249
-
250
- let previousItems = [];
251
- let nextItems = [];
252
-
253
- if (loopClonesPerSide > dataLength) {
254
- const dataMultiplier = Math.floor(loopClonesPerSide / dataLength);
255
- const remainder = loopClonesPerSide % dataLength;
113
+ const getRealIndex = useCallback(
114
+ (index: number) => {
115
+ if (!loop) return index;
116
+
117
+ const dataLength = data.length;
118
+ if (dataLength === 0) return 0;
256
119
 
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
- if (
402
- this._positions[lastIndex] &&
403
- offset >= this._positions[lastIndex].start
404
- ) {
405
- return lastIndex;
406
- }
407
-
408
- for (let i = 0; i < this._positions.length; i++) {
409
- const { start, end } = this._positions[i];
410
- if (
411
- center + activeSlideOffset >= start &&
412
- center - activeSlideOffset <= end
413
- ) {
414
- itemIndex = i;
415
- break;
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
+ }
416
272
  }
417
- }
273
+ },
274
+ [itemWidth, containerWidth, activeSlideOffset, getRealIndex, onScrollIndexChanged]
275
+ );
418
276
 
419
- return itemIndex || 0;
420
- }
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);
421
282
 
422
- _initPositionsAndInterpolators(props = this.props) {
423
- const { data } = props;
424
- const itemMainDimension = this._getItemMainDimension();
283
+ currentIndexRef.current = realIndex;
284
+ setCurrentIndex(realIndex);
425
285
 
426
- if (!data || !data.length) {
427
- return;
428
- }
429
-
430
- const interpolators: any[] = [];
431
- this._positions = [];
286
+ if (onSnapToItem) {
287
+ onSnapToItem(realIndex);
288
+ }
432
289
 
433
- this._getCustomData(props).forEach((_itemData, index) => {
434
- const _index = this._getCustomIndex(index, props);
435
- let animatedValue;
290
+ handleLoopReposition(index);
436
291
 
437
- this._positions[index] = {
438
- start: index * itemMainDimension,
439
- 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,
440
345
  };
441
-
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,
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>
448
361
  );
449
-
450
- animatedValue = this._scrollPos.interpolate({
451
- ...interpolator,
452
- extrapolate: 'clamp',
453
- });
454
362
  }
455
363
 
456
- interpolators.push(animatedValue);
457
- });
458
-
459
- this.setState({ interpolators });
460
- }
461
-
462
- _repositionScroll(index: number, animated = false) {
463
- const { data, loopClonesPerSide = 3 } = this.props;
464
- const dataLength = data && data.length;
465
-
466
- if (typeof index === 'undefined' || !this._shouldRepositionScroll(index)) {
467
- return;
468
- }
469
-
470
- let repositionTo = index;
471
-
472
- if (index >= dataLength + loopClonesPerSide) {
473
- repositionTo = index - dataLength;
474
- } else if (index < loopClonesPerSide) {
475
- repositionTo = index + dataLength;
476
- }
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();
486
- }
487
-
488
- onTouchStart && onTouchStart(event);
489
- }
490
-
491
- _onTouchEnd(event: GestureResponderEvent) {
492
- const { onTouchEnd } = this.props;
493
-
494
- if (
495
- this._getScrollEnabled() !== false &&
496
- this._autoplay &&
497
- !this._autoplaying
498
- ) {
499
- this.startAutoplay();
500
- }
501
-
502
- onTouchEnd && onTouchEnd(event);
503
- }
504
-
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);
513
-
514
- this._currentScrollOffset = scrollOffset;
515
-
516
- if (nextActiveItem !== this._onScrollActiveItem) {
517
- this._onScrollActiveItem = nextActiveItem;
518
- onScrollIndexChanged &&
519
- onScrollIndexChanged(this._getDataIndex(nextActiveItem));
520
-
521
- onSnapToItem && onSnapToItem(this._getDataIndex(nextActiveItem));
522
- }
523
-
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
- }
532
-
533
- if (typeof onScroll === 'function' && event) {
534
- onScroll(event);
535
- }
536
- }
537
-
538
- _onMomentumScrollEnd(event: NativeSyntheticEvent<NativeScrollEvent>) {
539
- const { autoplayDelay, onMomentumScrollEnd, onSnapToItem } = this.props;
540
- const { itemWidth } = this.state;
541
- const scrollOffset = event
542
- ? this._getScrollOffset(event)
543
- : this._currentScrollOffset;
544
- const nextActiveItem = this._getActiveItem(scrollOffset);
545
- const hasSnapped = this._isMultiple(scrollOffset, itemWidth);
546
-
547
- if (nextActiveItem !== this._activeItem) {
548
- this._activeItem = nextActiveItem;
549
- onSnapToItem && onSnapToItem(this._getDataIndex(nextActiveItem));
550
-
551
- if (hasSnapped && IS_ANDROID) {
552
- this._repositionScroll(nextActiveItem);
553
- } else if (IS_IOS) {
554
- this._repositionScroll(nextActiveItem);
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);
555
416
  }
556
- }
557
-
558
- onMomentumScrollEnd && onMomentumScrollEnd(event);
559
-
560
- if (IS_ANDROID && this._autoplay && !this._autoplaying) {
561
- if (this._enableAutoplayTimeout != null)
562
- clearTimeout(this._enableAutoplayTimeout);
563
- this._enableAutoplayTimeout = setTimeout(() => {
564
- this.startAutoplay();
565
- }, autoplayDelay);
566
- }
567
- }
568
-
569
- _onLayout(event: LayoutChangeEvent) {
570
- const { onLayout, visibleItem = 1 } = this.props;
571
-
572
- if (this._onLayoutInitDone) {
573
- this._initPositionsAndInterpolators();
574
- this._snapToItem(this._activeItem, false, false, true);
575
- } else {
576
- this._onLayoutInitDone = true;
577
- }
578
- const containerWidth = event.nativeEvent.layout.width;
579
- let itemWidth =
580
- this.props.visibleItem === 1
581
- ? screenWidth - Spacing.M * 2
582
- : Math.ceil(
583
- (containerWidth * 0.9 - visibleItem * Spacing.S) / visibleItem,
584
- );
585
- if (this.props.itemWidth) {
586
- itemWidth = this.props.itemWidth;
587
- }
588
- if (this.props.full) {
589
- itemWidth = containerWidth;
590
- }
591
-
592
- this.setState({ containerWidth, itemWidth });
593
-
594
- onLayout && onLayout(event);
595
- }
596
-
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;
613
- }
614
-
615
- if (!index || index < 0) {
616
- index = 0;
617
- } else if (itemsLength > 0 && index >= itemsLength) {
618
- index = itemsLength - 1;
619
- }
620
-
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));
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,
636
428
  }
637
-
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
- }
646
- }
647
- }
648
- }
649
-
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];
655
-
656
- if (typeof animatedValue === 'undefined') {
657
- return null;
658
- }
659
-
660
- const animate = this._shouldAnimateSlides();
661
- const Component = animate ? Animated.View : View;
662
- const mainDimension = { width: itemWidth };
663
-
664
- let spacingStyle: ViewStyle = this.props.loop
665
- ? { marginLeft: Spacing.S }
666
- : {
667
- marginLeft: index === 0 ? Spacing.M : 0,
668
- marginRight:
669
- index === this._getCustomDataLength() - 1 ? Spacing.M : Spacing.S,
670
- };
671
-
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
429
  );
693
- }
694
-
695
- startAutoplay() {
696
- const { autoplayInterval, autoplayDelay } = this.props;
697
- this._autoplay = true;
698
-
699
- if (this._autoplaying) {
700
- return;
701
- }
702
-
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();
709
- }
710
- }, autoplayInterval);
711
- }, autoplayDelay);
712
- }
713
-
714
- pauseAutoPlay() {
715
- this._autoplaying = false;
716
- if (this._autoplayTimeout != null) clearTimeout(this._autoplayTimeout);
717
- if (this._enableAutoplayTimeout != null)
718
- clearTimeout(this._enableAutoplayTimeout);
719
- if (this._autoplayInterval != null) clearInterval(this._autoplayInterval);
720
- }
721
430
 
722
- stopAutoplay() {
723
- this._autoplay = false;
724
- this.pauseAutoPlay();
725
- }
726
-
727
- snapToItem(index: number, animated = true, fireCallback = true) {
728
- if (!index || index < 0) {
729
- index = 0;
431
+ if (onScrollProp) {
432
+ return (event: NativeSyntheticEvent<NativeScrollEvent>) => {
433
+ scrollHandler(event);
434
+ onScrollProp(event);
435
+ };
730
436
  }
731
437
 
732
- const positionIndex = this._getPositionIndex(index);
438
+ return scrollHandler;
439
+ }, [scrollXRef, handleScroll, onScrollProp]);
733
440
 
734
- if (positionIndex === this._activeItem) {
735
- return;
441
+ useEffect(() => {
442
+ if (firstItem > 0 && firstItem < data.length) {
443
+ setTimeout(() => {
444
+ snapToItem(firstItem, false, false);
445
+ }, 100);
736
446
  }
447
+ }, []);
737
448
 
738
- this._snapToItem(positionIndex, animated, fireCallback);
739
- }
740
-
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;
449
+ useEffect(() => {
450
+ if (autoplay && data.length > 1) {
451
+ startAutoplay();
747
452
  }
748
- this._snapToItem(newIndex, animated, fireCallback);
749
- }
750
-
751
- snapToPrev(animated = true, fireCallback = true) {
752
- const itemsLength = this._getCustomDataLength();
753
453
 
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,
454
+ return () => {
455
+ stopAutoplay();
792
456
  };
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
- }
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'}
805
504
  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
505
+ getItemLayout={getItemLayout}
816
506
  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}
507
+ removeClippedSubviews={false}
508
+ initialScrollIndex={loop && firstItem > 0 ? getLoopIndex(firstItem) : undefined}
509
+ initialNumToRender={Math.min(10, dataWithClones.length)}
510
+ maxToRenderPerBatch={5}
511
+ windowSize={5}
834
512
  />
835
- );
836
- }
837
- }
513
+ </View>
514
+ );
515
+ });
838
516
 
517
+ const styles = StyleSheet.create({
518
+ container: {
519
+ overflow: 'hidden',
520
+ },
521
+ });
522
+
523
+ Carousel.displayName = 'Carousel';
524
+
525
+ export default Carousel;
839
526
  export { Carousel };
840
- export type { CarouselProps, CarouselRef };
527
+