@momo-kits/carousel 0.151.2-beta.2 → 0.152.1-beta.1

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 (2) hide show
  1. package/index.tsx +454 -777
  2. package/package.json +1 -1
package/index.tsx CHANGED
@@ -1,840 +1,517 @@
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
+ const [isLayoutReady, setIsLayoutReady] = useState(false);
74
+
75
+ const itemWidth = useMemo(() => {
76
+ if (full) {
77
+ return containerWidth;
150
78
  }
151
-
152
- if (scrollEnabled !== prevProps.scrollEnabled) {
153
- this._setScrollEnabled(scrollEnabled);
79
+ if (itemWidthProp) {
80
+ return itemWidthProp;
154
81
  }
82
+ const spacing = visibleItem > 1 ? 10 : 0;
83
+ return (containerWidth - spacing * (visibleItem - 1)) / visibleItem;
84
+ }, [containerWidth, full, itemWidthProp, visibleItem]);
155
85
 
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);
86
+ const snapToInterval = useMemo(() => {
87
+ if (snapToIntervalProp !== undefined) {
88
+ return snapToIntervalProp;
168
89
  }
90
+ return itemWidth;
91
+ }, [itemWidth, snapToIntervalProp]);
169
92
 
170
- if (this.props.onScroll !== prevProps.onScroll) {
171
- this._setScrollHandler(this.props);
93
+ const dataWithClones = useMemo(() => {
94
+ if (!loop || data.length === 0) {
95
+ return data;
172
96
  }
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
97
 
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);
98
+ const clones = [];
99
+ for (let i = 0; i < loopClonesPerSide; i++) {
100
+ const startIndex = data.length - loopClonesPerSide + i;
101
+ const endIndex = i;
102
+ clones.push(data[startIndex >= 0 ? startIndex : data.length + startIndex]);
209
103
  }
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
104
 
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
- }
105
+ const result = [...clones, ...data];
237
106
 
238
- _getCustomData(props = this.props) {
239
- const { data, loopClonesPerSide = 3 } = props;
240
- const dataLength = data && data.length;
241
-
242
- if (!dataLength) {
243
- return [];
107
+ for (let i = 0; i < loopClonesPerSide; i++) {
108
+ result.push(data[i % data.length]);
244
109
  }
245
110
 
246
- if (!this._enableLoop()) {
247
- return data;
248
- }
111
+ return result;
112
+ }, [data, loop, loopClonesPerSide]);
249
113
 
250
- let previousItems = [];
251
- let nextItems = [];
114
+ const getRealIndex = useCallback(
115
+ (index: number) => {
116
+ if (!loop) return index;
117
+
118
+ const dataLength = data.length;
119
+ if (dataLength === 0) return 0;
252
120
 
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);
121
+ let realIndex = index - loopClonesPerSide;
122
+
123
+ if (realIndex < 0) {
124
+ realIndex = dataLength + realIndex;
125
+ } else if (realIndex >= dataLength) {
126
+ realIndex = realIndex % dataLength;
260
127
  }
261
128
 
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
- }
129
+ return realIndex;
130
+ },
131
+ [loop, loopClonesPerSide, data.length]
132
+ );
133
+
134
+ const getLoopIndex = useCallback(
135
+ (realIndex: number) => {
136
+ if (!loop) return realIndex;
137
+ return realIndex + loopClonesPerSide;
138
+ },
139
+ [loop, loopClonesPerSide]
140
+ );
141
+
142
+ const snapToItem = useCallback(
143
+ (index: number, animated = true, fireCallback = true) => {
144
+ if (!flatListRef.current || index < 0 || index >= data.length) {
145
+ return;
146
+ }
282
147
 
283
- _getCustomIndex(index: number, props = this.props) {
284
- const itemsLength = this._getCustomDataLength(props);
148
+ const loopIndex = getLoopIndex(index);
149
+ const offset = loopIndex * itemWidth;
285
150
 
286
- if (!itemsLength || typeof index === 'undefined') {
287
- return 0;
288
- }
151
+ flatListRef.current.scrollToOffset({
152
+ offset,
153
+ animated,
154
+ });
289
155
 
290
- return index;
291
- }
156
+ currentIndexRef.current = index;
157
+ setCurrentIndex(index);
292
158
 
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;
159
+ if (fireCallback && onSnapToItem) {
160
+ onSnapToItem(index);
161
+ }
162
+ },
163
+ [data.length, getLoopIndex, itemWidth, onSnapToItem]
164
+ );
165
+
166
+ const snapToNext = useCallback(
167
+ (animated = true, fireCallback = true) => {
168
+ const nextIndex = (currentIndexRef.current + 1) % data.length;
169
+ snapToItem(nextIndex, animated, fireCallback);
170
+ },
171
+ [data.length, snapToItem]
172
+ );
173
+
174
+ const snapToPrev = useCallback(
175
+ (animated = true, fireCallback = true) => {
176
+ const prevIndex =
177
+ currentIndexRef.current === 0 ? data.length - 1 : currentIndexRef.current - 1;
178
+ snapToItem(prevIndex, animated, fireCallback);
179
+ },
180
+ [data.length, snapToItem]
181
+ );
182
+
183
+ const stopAutoplay = useCallback(() => {
184
+ if (autoplayTimerRef.current) {
185
+ clearInterval(autoplayTimerRef.current);
186
+ autoplayTimerRef.current = null;
187
+ }
188
+ }, []);
189
+
190
+ const pauseAutoPlay = useCallback(() => {
191
+ isAutoplayPausedRef.current = true;
192
+ stopAutoplay();
193
+ }, [stopAutoplay]);
194
+
195
+ const startAutoplay = useCallback(() => {
196
+ stopAutoplay();
197
+ isAutoplayPausedRef.current = false;
198
+
199
+ if (!autoplay || data.length <= 1) {
200
+ return;
298
201
  }
299
202
 
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);
203
+ const startTimer = () => {
204
+ autoplayTimerRef.current = setInterval(() => {
205
+ if (!isAutoplayPausedRef.current) {
206
+ snapToNext(true, true);
313
207
  }
208
+ }, autoplayInterval);
209
+ };
314
210
 
315
- for (let j = 0; j < dataMultiplier; j++) {
316
- dataIndexes.push(...baseDataIndexes);
211
+ if (autoplayDelay > 0) {
212
+ setTimeout(startTimer, autoplayDelay);
213
+ } else {
214
+ startTimer();
215
+ }
216
+ }, [autoplay, autoplayDelay, autoplayInterval, data.length, snapToNext, stopAutoplay]);
217
+
218
+ const handleLoopReposition = useCallback(
219
+ (index: number) => {
220
+ if (!loop || !flatListRef.current) return;
221
+
222
+ const dataLength = data.length;
223
+ if (dataLength === 0) return;
224
+
225
+ if (index < loopClonesPerSide) {
226
+ const realIndex = dataLength - (loopClonesPerSide - index);
227
+ const loopIndex = getLoopIndex(realIndex);
228
+ setTimeout(() => {
229
+ flatListRef.current?.scrollToOffset({
230
+ offset: loopIndex * itemWidth,
231
+ animated: false,
232
+ });
233
+ }, 50);
234
+ } else if (index >= dataLength + loopClonesPerSide) {
235
+ const realIndex = index - (dataLength + loopClonesPerSide);
236
+ const loopIndex = getLoopIndex(realIndex);
237
+ setTimeout(() => {
238
+ flatListRef.current?.scrollToOffset({
239
+ offset: loopIndex * itemWidth,
240
+ animated: false,
241
+ });
242
+ }, 50);
243
+ }
244
+ },
245
+ [loop, data.length, loopClonesPerSide, itemWidth, getLoopIndex]
246
+ );
247
+
248
+ const handleScroll = useCallback(
249
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
250
+ const offsetX = event.nativeEvent.contentOffset.x;
251
+ const index = Math.round(offsetX / itemWidth);
252
+ const realIndex = getRealIndex(index);
253
+
254
+ if (realIndex !== currentIndexRef.current) {
255
+ currentIndexRef.current = realIndex;
256
+ setCurrentIndex(realIndex);
257
+
258
+ if (onScrollIndexChanged) {
259
+ onScrollIndexChanged(realIndex);
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
- }
262
+ },
263
+ [itemWidth, getRealIndex, onScrollIndexChanged]
264
+ );
363
265
 
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;
266
+ const handleMomentumScrollEnd = useCallback(
267
+ (event: NativeSyntheticEvent<NativeScrollEvent>) => {
268
+ const offsetX = event.nativeEvent.contentOffset.x;
269
+ const index = Math.round(offsetX / itemWidth);
270
+ const realIndex = getRealIndex(index);
396
271
 
397
- if (offset <= 0) {
398
- return 0;
399
- }
272
+ currentIndexRef.current = realIndex;
273
+ setCurrentIndex(realIndex);
400
274
 
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;
275
+ if (onSnapToItem) {
276
+ onSnapToItem(realIndex);
416
277
  }
417
- }
418
278
 
419
- return itemIndex || 0;
420
- }
279
+ handleLoopReposition(index);
421
280
 
422
- _initPositionsAndInterpolators(props = this.props) {
423
- const { data } = props;
424
- const itemMainDimension = this._getItemMainDimension();
425
-
426
- if (!data || !data.length) {
427
- return;
428
- }
429
-
430
- const interpolators: any[] = [];
431
- this._positions = [];
432
-
433
- this._getCustomData(props).forEach((_itemData, index) => {
434
- const _index = this._getCustomIndex(index, props);
435
- let animatedValue;
436
-
437
- this._positions[index] = {
438
- start: index * itemMainDimension,
439
- end: index * itemMainDimension + itemMainDimension,
281
+ if (onMomentumScrollEndProp) {
282
+ onMomentumScrollEndProp(event);
283
+ }
284
+ },
285
+ [itemWidth, getRealIndex, onSnapToItem, handleLoopReposition, onMomentumScrollEndProp]
286
+ );
287
+
288
+ const handleTouchStart = useCallback(
289
+ (event: any) => {
290
+ if (autoplay) {
291
+ pauseAutoPlay();
292
+ }
293
+ if (onTouchStartProp) {
294
+ onTouchStartProp(event);
295
+ }
296
+ },
297
+ [autoplay, pauseAutoPlay, onTouchStartProp]
298
+ );
299
+
300
+ const handleTouchEnd = useCallback(
301
+ (event: GestureResponderEvent) => {
302
+ if (autoplay && !isAutoplayPausedRef.current) {
303
+ startAutoplay();
304
+ }
305
+ if (onTouchEndProp) {
306
+ onTouchEndProp(event);
307
+ }
308
+ },
309
+ [autoplay, startAutoplay, onTouchEndProp]
310
+ );
311
+
312
+ const handleLayout = useCallback(
313
+ (event: LayoutChangeEvent) => {
314
+ const { width } = event.nativeEvent.layout;
315
+ containerWidthRef.current = width;
316
+ setContainerWidth(width);
317
+ setIsLayoutReady(true);
318
+
319
+ if (onLayoutProp) {
320
+ onLayoutProp(event);
321
+ }
322
+ },
323
+ [onLayoutProp]
324
+ );
325
+
326
+ const getItemLayout = useCallback(
327
+ (data: any, index: number) => {
328
+ if (getItemLayoutProp) {
329
+ return getItemLayoutProp(data, index);
330
+ }
331
+ return {
332
+ length: itemWidth,
333
+ offset: itemWidth * index,
334
+ index,
440
335
  };
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,
336
+ },
337
+ [itemWidth, getItemLayoutProp]
338
+ );
339
+
340
+ const renderAnimatedItem = useCallback(
341
+ ({ item, index }: { item: any; index: number }) => {
342
+ const realIndex = getRealIndex(index);
343
+ const hasOpacityAnimation = inactiveSlideOpacity < 1;
344
+ const hasScaleAnimation = inactiveSlideScale < 1;
345
+
346
+ if (!hasOpacityAnimation && !hasScaleAnimation) {
347
+ return (
348
+ <View style={[{ width: itemWidth }, slideStyle]}>
349
+ {renderItem({ item, index: realIndex })}
350
+ </View>
448
351
  );
449
-
450
- animatedValue = this._scrollPos.interpolate({
451
- ...interpolator,
452
- extrapolate: 'clamp',
453
- });
454
- }
455
-
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);
555
352
  }
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
353
 
630
- const requiresManualTrigger = !animated || IS_ANDROID;
631
- if (requiresManualTrigger) {
632
- this._activeItem = index;
633
-
634
- if (fireCallback) {
635
- onSnapToItem && onSnapToItem(this._getDataIndex(index));
354
+ const inputRange = [
355
+ (index - 1) * itemWidth,
356
+ index * itemWidth,
357
+ (index + 1) * itemWidth,
358
+ ];
359
+
360
+ const opacity = hasOpacityAnimation
361
+ ? scrollXRef.interpolate({
362
+ inputRange,
363
+ outputRange: [inactiveSlideOpacity, 1, inactiveSlideOpacity],
364
+ extrapolate: 'clamp',
365
+ })
366
+ : 1;
367
+
368
+ const scale = hasScaleAnimation
369
+ ? scrollXRef.interpolate({
370
+ inputRange,
371
+ outputRange: [inactiveSlideScale, 1, inactiveSlideScale],
372
+ extrapolate: 'clamp',
373
+ })
374
+ : 1;
375
+
376
+ return (
377
+ <Animated.View
378
+ style={[
379
+ { width: itemWidth },
380
+ slideStyle,
381
+ {
382
+ opacity,
383
+ transform: [{ scale }],
384
+ },
385
+ ]}
386
+ >
387
+ {renderItem({ item, index: realIndex })}
388
+ </Animated.View>
389
+ );
390
+ },
391
+ [
392
+ getRealIndex,
393
+ inactiveSlideOpacity,
394
+ inactiveSlideScale,
395
+ itemWidth,
396
+ slideStyle,
397
+ renderItem,
398
+ scrollXRef,
399
+ ]
400
+ );
401
+
402
+ const handleKeyExtractor = useCallback(
403
+ (item: any, index: number) => {
404
+ if (keyExtractor) {
405
+ return keyExtractor(item, index);
636
406
  }
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
- }
407
+ return `carousel-item-${index}`;
408
+ },
409
+ [keyExtractor]
410
+ );
411
+
412
+ const onScrollEvent = useMemo(() => {
413
+ const scrollHandler = Animated.event(
414
+ [{ nativeEvent: { contentOffset: { x: scrollXRef } } }],
415
+ {
416
+ useNativeDriver: true,
417
+ listener: handleScroll,
646
418
  }
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
419
  );
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
420
 
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;
421
+ if (onScrollProp) {
422
+ return (event: NativeSyntheticEvent<NativeScrollEvent>) => {
423
+ scrollHandler(event);
424
+ onScrollProp(event);
425
+ };
730
426
  }
731
427
 
732
- const positionIndex = this._getPositionIndex(index);
428
+ return scrollHandler;
429
+ }, [scrollXRef, handleScroll, onScrollProp]);
733
430
 
734
- if (positionIndex === this._activeItem) {
735
- return;
431
+ useEffect(() => {
432
+ if (isLayoutReady && firstItem > 0 && firstItem < data.length) {
433
+ setTimeout(() => {
434
+ snapToItem(firstItem, false, false);
435
+ }, 50);
736
436
  }
437
+ }, [isLayoutReady, firstItem, data.length, snapToItem]);
737
438
 
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;
439
+ useEffect(() => {
440
+ if (autoplay && data.length > 1) {
441
+ startAutoplay();
747
442
  }
748
- this._snapToItem(newIndex, animated, fireCallback);
749
- }
750
-
751
- snapToPrev(animated = true, fireCallback = true) {
752
- const itemsLength = this._getCustomDataLength();
753
443
 
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,
444
+ return () => {
445
+ stopAutoplay();
792
446
  };
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
- }
447
+ }, [autoplay, data.length, startAutoplay, stopAutoplay]);
448
+
449
+ useEffect(() => {
450
+ if (apparitionDelay > 0) {
451
+ const timer = setTimeout(() => {
452
+ setIsVisible(true);
453
+ }, apparitionDelay);
454
+
455
+ return () => clearTimeout(timer);
456
+ }
457
+ }, [apparitionDelay]);
458
+
459
+ useImperativeHandle(
460
+ ref,
461
+ () => ({
462
+ snapToItem,
463
+ snapToNext,
464
+ snapToPrev,
465
+ startAutoplay,
466
+ pauseAutoplay: pauseAutoPlay,
467
+ stopAutoplay,
468
+ }),
469
+ [snapToItem, snapToNext, snapToPrev, startAutoplay, pauseAutoPlay, stopAutoplay] // eslint-disable-line react-hooks/exhaustive-deps
470
+ );
471
+
472
+ if (!isVisible) {
473
+ return <View style={[styles.container, style]} />;
474
+ }
475
+
476
+ return (
477
+ <View style={[styles.container, style]} onLayout={handleLayout}>
478
+ <AnimatedFlatList
479
+ ref={flatListRef}
480
+ data={dataWithClones}
481
+ renderItem={renderAnimatedItem}
482
+ keyExtractor={handleKeyExtractor}
483
+ horizontal
484
+ showsHorizontalScrollIndicator={false}
485
+ scrollEnabled={scrollEnabled}
486
+ scrollEventThrottle={16}
487
+ onScroll={onScrollEvent}
488
+ onMomentumScrollEnd={handleMomentumScrollEnd}
489
+ onTouchStart={handleTouchStart}
490
+ onTouchEnd={handleTouchEnd}
491
+ snapToInterval={enableSnap ? (snapToInterval as number) : undefined}
492
+ snapToAlignment={enableSnap ? 'start' : undefined}
493
+ decelerationRate={enableSnap ? 'fast' : 'normal'}
805
494
  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
495
+ getItemLayout={getItemLayout}
816
496
  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}
497
+ removeClippedSubviews={false}
498
+ initialNumToRender={Math.min(10, dataWithClones.length)}
499
+ maxToRenderPerBatch={5}
500
+ windowSize={5}
834
501
  />
835
- );
836
- }
837
- }
502
+ </View>
503
+ );
504
+ });
838
505
 
506
+ const styles = StyleSheet.create({
507
+ container: {
508
+ overflow: 'hidden',
509
+ },
510
+ });
511
+
512
+ Carousel.displayName = 'Carousel';
513
+
514
+ export default Carousel;
839
515
  export { Carousel };
840
516
  export type { CarouselProps, CarouselRef };
517
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momo-kits/carousel",
3
- "version": "0.151.2-beta.2",
3
+ "version": "0.152.1-beta.1",
4
4
  "private": false,
5
5
  "main": "index.tsx",
6
6
  "peerDependencies": {