@momo-kits/carousel 0.0.73-beta → 0.72.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.
- package/Carousel.js +242 -263
- package/CarouselV2.js +1363 -0
- package/index.js +3 -4
- package/package.json +15 -14
- package/pagination/NumberPagination.js +43 -0
- package/pagination/Pagination.js +64 -34
- package/pagination/PaginationDot.js +45 -39
- package/pagination/styles.js +6 -6
- package/publish.sh +1 -1
- package/utils/animationsV2.js +381 -0
package/CarouselV2.js
ADDED
|
@@ -0,0 +1,1363 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {Animated, I18nManager, Platform, View} from 'react-native';
|
|
3
|
+
// import shallowCompare from 'react-addons-shallow-compare';
|
|
4
|
+
import {
|
|
5
|
+
defaultScrollInterpolator,
|
|
6
|
+
stackScrollInterpolator,
|
|
7
|
+
tinderScrollInterpolator,
|
|
8
|
+
defaultAnimatedStyles,
|
|
9
|
+
shiftAnimatedStyles,
|
|
10
|
+
stackAnimatedStyles,
|
|
11
|
+
tinderAnimatedStyles,
|
|
12
|
+
} from './utils/animationsV2';
|
|
13
|
+
import PropTypes from 'prop-types';
|
|
14
|
+
|
|
15
|
+
// Metro doesn't support dynamic imports - i.e. require() done in the component itself
|
|
16
|
+
// But at the same time the following import will fail on Snack...
|
|
17
|
+
// TODO: find a way to get React Native's version without having to assume the file path
|
|
18
|
+
// import RN_PACKAGE from '../../../react-native/package.json';
|
|
19
|
+
|
|
20
|
+
const IS_ANDROID = Platform.OS === 'android';
|
|
21
|
+
const IS_IOS = Platform.OS === 'ios';
|
|
22
|
+
|
|
23
|
+
// React Native automatically handles RTL layouts; unfortunately, it's buggy with horizontal ScrollView
|
|
24
|
+
// See https://github.com/facebook/react-native/issues/11960
|
|
25
|
+
// NOTE: the following variable is not declared in the constructor
|
|
26
|
+
// otherwise it is undefined at init, which messes with custom indexes
|
|
27
|
+
const IS_RTL = I18nManager.isRTL;
|
|
28
|
+
|
|
29
|
+
export default class Carousel extends React.PureComponent {
|
|
30
|
+
constructor(props) {
|
|
31
|
+
super(props);
|
|
32
|
+
|
|
33
|
+
this.state = {
|
|
34
|
+
hideCarousel: !!props.apparitionDelay,
|
|
35
|
+
interpolators: [],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// this._RNVersionCode = this._getRNVersionCode();
|
|
39
|
+
|
|
40
|
+
// The following values are not stored in the state because 'setState()' is asynchronous
|
|
41
|
+
// and this results in an absolutely crappy behavior on Android while swiping (see #156)
|
|
42
|
+
const initialActiveItem = this._getFirstItem(props.firstItem);
|
|
43
|
+
this._activeItem = initialActiveItem;
|
|
44
|
+
this._onScrollActiveItem = initialActiveItem;
|
|
45
|
+
this._previousFirstItem = initialActiveItem;
|
|
46
|
+
this._previousItemsLength = initialActiveItem;
|
|
47
|
+
|
|
48
|
+
this._mounted = false;
|
|
49
|
+
this._positions = [];
|
|
50
|
+
this._currentScrollOffset = 0; // Store ScrollView's scroll position
|
|
51
|
+
this._scrollEnabled = props.scrollEnabled !== false;
|
|
52
|
+
|
|
53
|
+
this._getCellRendererComponent = this._getCellRendererComponent.bind(this);
|
|
54
|
+
this._getItemLayout = this._getItemLayout.bind(this);
|
|
55
|
+
this._getKeyExtractor = this._getKeyExtractor.bind(this);
|
|
56
|
+
this._onLayout = this._onLayout.bind(this);
|
|
57
|
+
this._onScroll = this._onScroll.bind(this);
|
|
58
|
+
this._onMomentumScrollEnd = this._onMomentumScrollEnd.bind(this);
|
|
59
|
+
this._onTouchStart = this._onTouchStart.bind(this);
|
|
60
|
+
this._onTouchEnd = this._onTouchEnd.bind(this);
|
|
61
|
+
this._renderItem = this._renderItem.bind(this);
|
|
62
|
+
|
|
63
|
+
// WARNING: call this AFTER binding _onScroll
|
|
64
|
+
this._setScrollHandler(props);
|
|
65
|
+
|
|
66
|
+
// Display warnings
|
|
67
|
+
this._displayWarnings(props);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get realIndex() {
|
|
71
|
+
return this._activeItem;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// shouldComponentUpdate (
|
|
75
|
+
// nextProps,
|
|
76
|
+
// nextState
|
|
77
|
+
// ) {
|
|
78
|
+
// if (this.props.shouldOptimizeUpdates === false) {
|
|
79
|
+
// return true;
|
|
80
|
+
// } else {
|
|
81
|
+
// return shallowCompare(this, nextProps, nextState);
|
|
82
|
+
// }
|
|
83
|
+
// }
|
|
84
|
+
|
|
85
|
+
get currentIndex() {
|
|
86
|
+
return this._getDataIndex(this._activeItem);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get currentScrollPosition() {
|
|
90
|
+
return this._currentScrollOffset;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
componentDidMount() {
|
|
94
|
+
const {apparitionDelay, autoplay, firstItem} = this.props;
|
|
95
|
+
|
|
96
|
+
this._mounted = true;
|
|
97
|
+
this._initPositionsAndInterpolators();
|
|
98
|
+
|
|
99
|
+
// Without 'requestAnimationFrame' or a `0` timeout, images will randomly not be rendered on Android...
|
|
100
|
+
this._initTimeout = setTimeout(() => {
|
|
101
|
+
if (!this._mounted) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const apparitionCallback = () => {
|
|
106
|
+
if (apparitionDelay) {
|
|
107
|
+
this.setState({hideCarousel: false});
|
|
108
|
+
}
|
|
109
|
+
if (autoplay) {
|
|
110
|
+
this.startAutoplay();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// FlatList will use its own built-in prop `initialScrollIndex`
|
|
115
|
+
if (this._needsScrollView()) {
|
|
116
|
+
const _firstItem = this._getFirstItem(firstItem);
|
|
117
|
+
this._snapToItem(_firstItem, false, false, true);
|
|
118
|
+
// this._hackActiveSlideAnimation(_firstItem);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (apparitionDelay) {
|
|
122
|
+
this._apparitionTimeout = setTimeout(() => {
|
|
123
|
+
apparitionCallback();
|
|
124
|
+
}, apparitionDelay);
|
|
125
|
+
} else {
|
|
126
|
+
apparitionCallback();
|
|
127
|
+
}
|
|
128
|
+
}, 1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
componentDidUpdate(prevProps) {
|
|
132
|
+
const {interpolators} = this.state;
|
|
133
|
+
const {firstItem, scrollEnabled} = this.props;
|
|
134
|
+
const itemsLength = this._getCustomDataLength(this.props);
|
|
135
|
+
|
|
136
|
+
if (!itemsLength) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const nextFirstItem = this._getFirstItem(firstItem, this.props);
|
|
141
|
+
let nextActiveItem =
|
|
142
|
+
typeof this._activeItem !== 'undefined'
|
|
143
|
+
? this._activeItem
|
|
144
|
+
: nextFirstItem;
|
|
145
|
+
|
|
146
|
+
const hasNewSize =
|
|
147
|
+
this.props.vertical !== prevProps.vertical ||
|
|
148
|
+
(this.props.vertical &&
|
|
149
|
+
prevProps.vertical &&
|
|
150
|
+
(prevProps.itemHeight !== this.props.itemHeight ||
|
|
151
|
+
prevProps.sliderHeight !== this.props.sliderHeight)) ||
|
|
152
|
+
(!this.props.vertical &&
|
|
153
|
+
!prevProps.vertical &&
|
|
154
|
+
(prevProps.itemWidth !== this.props.itemWidth ||
|
|
155
|
+
prevProps.sliderWidth !== this.props.sliderWidth));
|
|
156
|
+
|
|
157
|
+
// Prevent issues with dynamically removed items
|
|
158
|
+
if (nextActiveItem > itemsLength - 1) {
|
|
159
|
+
nextActiveItem = itemsLength - 1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Handle changing scrollEnabled independent of user -> carousel interaction
|
|
163
|
+
if (scrollEnabled !== prevProps.scrollEnabled) {
|
|
164
|
+
this._setScrollEnabled(scrollEnabled);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (interpolators.length !== itemsLength || hasNewSize) {
|
|
168
|
+
this._activeItem = nextActiveItem;
|
|
169
|
+
this._previousItemsLength = itemsLength;
|
|
170
|
+
|
|
171
|
+
this._initPositionsAndInterpolators(this.props);
|
|
172
|
+
|
|
173
|
+
// Handle scroll issue when dynamically removing items (see #133)
|
|
174
|
+
// This also fixes first item's active state on Android
|
|
175
|
+
// Because 'initialScrollIndex' apparently doesn't trigger scroll
|
|
176
|
+
if (this._previousItemsLength > itemsLength) {
|
|
177
|
+
this._hackActiveSlideAnimation(nextActiveItem);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (hasNewSize) {
|
|
181
|
+
this._snapToItem(nextActiveItem, false, false, true);
|
|
182
|
+
}
|
|
183
|
+
} else if (
|
|
184
|
+
nextFirstItem !== this._previousFirstItem &&
|
|
185
|
+
nextFirstItem !== this._activeItem
|
|
186
|
+
) {
|
|
187
|
+
this._activeItem = nextFirstItem;
|
|
188
|
+
this._previousFirstItem = nextFirstItem;
|
|
189
|
+
this._snapToItem(nextFirstItem, false, true, true);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (this.props.onScroll !== prevProps.onScroll) {
|
|
193
|
+
this._setScrollHandler(this.props);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
componentWillUnmount() {
|
|
198
|
+
this._mounted = false;
|
|
199
|
+
this.stopAutoplay();
|
|
200
|
+
// @ts-expect-error setTimeout / clearTiemout is buggy :/
|
|
201
|
+
clearTimeout(this._initTimeout);
|
|
202
|
+
// @ts-expect-error setTimeout / clearTiemout is buggy :/
|
|
203
|
+
clearTimeout(this._apparitionTimeout);
|
|
204
|
+
// @ts-expect-error setTimeout / clearTiemout is buggy :/
|
|
205
|
+
clearTimeout(this._hackSlideAnimationTimeout);
|
|
206
|
+
// @ts-expect-error setTimeout / clearTiemout is buggy :/
|
|
207
|
+
clearTimeout(this._enableAutoplayTimeout);
|
|
208
|
+
// @ts-expect-error setTimeout / clearTiemout is buggy :/
|
|
209
|
+
clearTimeout(this._autoplayTimeout);
|
|
210
|
+
// @ts-expect-error setTimeout / clearTiemout is buggy :/
|
|
211
|
+
clearTimeout(this._snapNoMomentumTimeout);
|
|
212
|
+
// @ts-expect-error setTimeout / clearTiemout is buggy :/
|
|
213
|
+
clearTimeout(this._androidRepositioningTimeout);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
_setScrollHandler(props) {
|
|
217
|
+
// Native driver for scroll events
|
|
218
|
+
const scrollEventConfig = {
|
|
219
|
+
listener: this._onScroll,
|
|
220
|
+
useNativeDriver: true,
|
|
221
|
+
};
|
|
222
|
+
this._scrollPos = new Animated.Value(0);
|
|
223
|
+
const argMapping = props.vertical
|
|
224
|
+
? [{nativeEvent: {contentOffset: {y: this._scrollPos}}}]
|
|
225
|
+
: [{nativeEvent: {contentOffset: {x: this._scrollPos}}}];
|
|
226
|
+
|
|
227
|
+
// @ts-expect-error Let's ignore for now that trick
|
|
228
|
+
if (props.onScroll && Array.isArray(props.onScroll._argMapping)) {
|
|
229
|
+
// Because of a react-native issue https://github.com/facebook/react-native/issues/13294
|
|
230
|
+
argMapping.pop();
|
|
231
|
+
// @ts-expect-error Let's ignore for now that trick
|
|
232
|
+
const [argMap] = props.onScroll._argMapping;
|
|
233
|
+
if (argMap && argMap.nativeEvent && argMap.nativeEvent.contentOffset) {
|
|
234
|
+
// Shares the same animated value passed in props
|
|
235
|
+
this._scrollPos =
|
|
236
|
+
argMap.nativeEvent.contentOffset.x ||
|
|
237
|
+
argMap.nativeEvent.contentOffset.y ||
|
|
238
|
+
this._scrollPos;
|
|
239
|
+
}
|
|
240
|
+
// @ts-expect-error Let's ignore for now that trick
|
|
241
|
+
argMapping.push(...props.onScroll._argMapping);
|
|
242
|
+
}
|
|
243
|
+
this._onScrollHandler = Animated.event(argMapping, scrollEventConfig);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// This will return a future-proof version code number compatible with semantic versioning
|
|
247
|
+
// Examples: 0.59.3 -> 5903 / 0.61.4 -> 6104 / 0.62.12 -> 6212 / 1.0.2 -> 10002
|
|
248
|
+
// _getRNVersionCode () {
|
|
249
|
+
// const version = RN_PACKAGE && RN_PACKAGE.version;
|
|
250
|
+
// if (!version) {
|
|
251
|
+
// return null;
|
|
252
|
+
// }
|
|
253
|
+
// const versionSplit = version.split('.');
|
|
254
|
+
// if (!versionSplit || !versionSplit.length) {
|
|
255
|
+
// return null;
|
|
256
|
+
// }
|
|
257
|
+
// return versionSplit[0] * 10000 +
|
|
258
|
+
// (typeof versionSplit[1] !== 'undefined' ? versionSplit[1] * 100 : 0) +
|
|
259
|
+
// (typeof versionSplit[2] !== 'undefined' ? versionSplit[2] * 1 : 0);
|
|
260
|
+
// }
|
|
261
|
+
|
|
262
|
+
_displayWarnings(props = this.props) {
|
|
263
|
+
const pluginName = 'react-native-snap-carousel';
|
|
264
|
+
const removedProps = [
|
|
265
|
+
'activeAnimationType',
|
|
266
|
+
'activeAnimationOptions',
|
|
267
|
+
'enableMomentum',
|
|
268
|
+
'lockScrollTimeoutDuration',
|
|
269
|
+
'lockScrollWhileSnapping',
|
|
270
|
+
'onBeforeSnapToItem',
|
|
271
|
+
'swipeThreshold',
|
|
272
|
+
];
|
|
273
|
+
|
|
274
|
+
// if (this._RNVersionCode && this._RNVersionCode < 5800) {
|
|
275
|
+
// console.error(
|
|
276
|
+
// `${pluginName}: Version 4+ of the plugin is based on React Native props that were introduced in version 0.58. ` +
|
|
277
|
+
// 'Please downgrade to version 3.x or update your version of React Native.'
|
|
278
|
+
// );
|
|
279
|
+
// }
|
|
280
|
+
if (!props.vertical && (!props.sliderWidth || !props.itemWidth)) {
|
|
281
|
+
console.error(
|
|
282
|
+
`${pluginName}: You need to specify both 'sliderWidth' and 'itemWidth' for horizontal carousels`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
if (props.vertical && (!props.sliderHeight || !props.itemHeight)) {
|
|
286
|
+
console.error(
|
|
287
|
+
`${pluginName}: You need to specify both 'sliderHeight' and 'itemHeight' for vertical carousels`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
removedProps.forEach(removedProp => {
|
|
292
|
+
if (removedProp in props) {
|
|
293
|
+
console.warn(
|
|
294
|
+
`${pluginName}: Prop ${removedProp} has been removed in version 4 of the plugin`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
_needsScrollView() {
|
|
301
|
+
const {useScrollView} = this.props;
|
|
302
|
+
// Android's cell renderer is buggy and has a stange overflow
|
|
303
|
+
// TODO: a workaround might be to pass the custom animated styles directly to it
|
|
304
|
+
return IS_ANDROID
|
|
305
|
+
? useScrollView ||
|
|
306
|
+
!Animated.FlatList ||
|
|
307
|
+
this._shouldUseStackLayout() ||
|
|
308
|
+
this._shouldUseTinderLayout()
|
|
309
|
+
: useScrollView || !Animated.FlatList;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
_needsRTLAdaptations() {
|
|
313
|
+
const {vertical} = this.props;
|
|
314
|
+
return IS_RTL && IS_ANDROID && !vertical;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
_enableLoop() {
|
|
318
|
+
const {data, enableSnap, loop} = this.props;
|
|
319
|
+
return enableSnap && loop && data && data.length && data.length > 1;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
_shouldAnimateSlides(props = this.props) {
|
|
323
|
+
const {
|
|
324
|
+
inactiveSlideOpacity,
|
|
325
|
+
inactiveSlideScale,
|
|
326
|
+
scrollInterpolator,
|
|
327
|
+
slideInterpolatedStyle,
|
|
328
|
+
} = props;
|
|
329
|
+
return (
|
|
330
|
+
inactiveSlideOpacity < 1 ||
|
|
331
|
+
inactiveSlideScale < 1 ||
|
|
332
|
+
!!scrollInterpolator ||
|
|
333
|
+
!!slideInterpolatedStyle ||
|
|
334
|
+
this._shouldUseShiftLayout() ||
|
|
335
|
+
this._shouldUseStackLayout() ||
|
|
336
|
+
this._shouldUseTinderLayout()
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
_shouldUseShiftLayout() {
|
|
341
|
+
const {inactiveSlideShift, layout} = this.props;
|
|
342
|
+
return layout === 'default' && inactiveSlideShift !== 0;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
_shouldUseStackLayout() {
|
|
346
|
+
return this.props.layout === 'stack';
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
_shouldUseTinderLayout() {
|
|
350
|
+
return this.props.layout === 'tinder';
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
_shouldRepositionScroll(index) {
|
|
354
|
+
const {data, enableSnap, loopClonesPerSide} = this.props;
|
|
355
|
+
const dataLength = data && data.length;
|
|
356
|
+
if (
|
|
357
|
+
!enableSnap ||
|
|
358
|
+
!dataLength ||
|
|
359
|
+
!this._enableLoop() ||
|
|
360
|
+
(index >= loopClonesPerSide && index < dataLength + loopClonesPerSide)
|
|
361
|
+
) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
_isMultiple(x, y) {
|
|
368
|
+
// This prevents Javascript precision issues: https://stackoverflow.com/a/58440614/
|
|
369
|
+
// Required because Android viewport size can return pretty complicated decimals numbers
|
|
370
|
+
return Math.round(Math.round(x / y) / (1 / y)) === Math.round(x);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
_getCustomData(props = this.props) {
|
|
374
|
+
const {data, loopClonesPerSide} = props;
|
|
375
|
+
const dataLength = data && data.length;
|
|
376
|
+
|
|
377
|
+
if (!dataLength) {
|
|
378
|
+
return [];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!this._enableLoop()) {
|
|
382
|
+
return data;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let previousItems = [];
|
|
386
|
+
let nextItems = [];
|
|
387
|
+
|
|
388
|
+
if (loopClonesPerSide > dataLength) {
|
|
389
|
+
const dataMultiplier = Math.floor(loopClonesPerSide / dataLength);
|
|
390
|
+
const remainder = loopClonesPerSide % dataLength;
|
|
391
|
+
|
|
392
|
+
for (let i = 0; i < dataMultiplier; i++) {
|
|
393
|
+
previousItems.push(...data);
|
|
394
|
+
nextItems.push(...data);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
previousItems.unshift(...data.slice(-remainder));
|
|
398
|
+
nextItems.push(...data.slice(0, remainder));
|
|
399
|
+
} else {
|
|
400
|
+
previousItems = data.slice(-loopClonesPerSide);
|
|
401
|
+
nextItems = data.slice(0, loopClonesPerSide);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return previousItems.concat(data, nextItems);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
_getCustomDataLength(props = this.props) {
|
|
408
|
+
const {data, loopClonesPerSide} = props;
|
|
409
|
+
const dataLength = data && data.length;
|
|
410
|
+
|
|
411
|
+
if (!dataLength) {
|
|
412
|
+
return 0;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return this._enableLoop() ? dataLength + 2 * loopClonesPerSide : dataLength;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
_getCustomIndex(index, props = this.props) {
|
|
419
|
+
const itemsLength = this._getCustomDataLength(props);
|
|
420
|
+
|
|
421
|
+
if (!itemsLength || typeof index === 'undefined') {
|
|
422
|
+
return 0;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return this._needsRTLAdaptations() ? itemsLength - index - 1 : index;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
_getDataIndex(index) {
|
|
429
|
+
const {data, loopClonesPerSide} = this.props;
|
|
430
|
+
const dataLength = data && data.length;
|
|
431
|
+
if (!this._enableLoop() || !dataLength) {
|
|
432
|
+
return index;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (index >= dataLength + loopClonesPerSide) {
|
|
436
|
+
return loopClonesPerSide > dataLength
|
|
437
|
+
? (index - loopClonesPerSide) % dataLength
|
|
438
|
+
: index - dataLength - loopClonesPerSide;
|
|
439
|
+
} else if (index < loopClonesPerSide) {
|
|
440
|
+
// TODO: is there a simpler way of determining the interpolated index?
|
|
441
|
+
if (loopClonesPerSide > dataLength) {
|
|
442
|
+
const baseDataIndexes = [];
|
|
443
|
+
const dataIndexes = [];
|
|
444
|
+
const dataMultiplier = Math.floor(loopClonesPerSide / dataLength);
|
|
445
|
+
const remainder = loopClonesPerSide % dataLength;
|
|
446
|
+
|
|
447
|
+
for (let i = 0; i < dataLength; i++) {
|
|
448
|
+
baseDataIndexes.push(i);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
for (let j = 0; j < dataMultiplier; j++) {
|
|
452
|
+
dataIndexes.push(...baseDataIndexes);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
dataIndexes.unshift(...baseDataIndexes.slice(-remainder));
|
|
456
|
+
return dataIndexes[index];
|
|
457
|
+
} else {
|
|
458
|
+
return index + dataLength - loopClonesPerSide;
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
return index - loopClonesPerSide;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Used with `snapToItem()` and 'PaginationDot'
|
|
466
|
+
_getPositionIndex(index) {
|
|
467
|
+
const {loop, loopClonesPerSide} = this.props;
|
|
468
|
+
return loop ? index + loopClonesPerSide : index;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
_getSnapOffsets(props = this.props) {
|
|
472
|
+
const offset = this._getItemMainDimension();
|
|
473
|
+
if (!props.enableSnap) return;
|
|
474
|
+
return [...Array(this._getCustomDataLength(props))].map((_, i) => {
|
|
475
|
+
return i * offset;
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
_getFirstItem(index, props = this.props) {
|
|
480
|
+
const {loopClonesPerSide} = props;
|
|
481
|
+
const itemsLength = this._getCustomDataLength(props);
|
|
482
|
+
|
|
483
|
+
if (!itemsLength || index > itemsLength - 1 || index < 0) {
|
|
484
|
+
return 0;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return this._enableLoop() ? index + loopClonesPerSide : index;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
_getWrappedRef() {
|
|
491
|
+
// Starting with RN 0.62, we should no longer call `getNode()` on the ref of an Animated component
|
|
492
|
+
if (
|
|
493
|
+
this._carouselRef &&
|
|
494
|
+
((this._needsScrollView() && this._carouselRef.scrollTo) ||
|
|
495
|
+
(!this._needsScrollView() && this._carouselRef.scrollToOffset))
|
|
496
|
+
) {
|
|
497
|
+
return this._carouselRef;
|
|
498
|
+
}
|
|
499
|
+
// https://github.com/facebook/react-native/issues/10635
|
|
500
|
+
// https://stackoverflow.com/a/48786374/8412141
|
|
501
|
+
return (
|
|
502
|
+
this._carouselRef &&
|
|
503
|
+
// @ts-expect-error This is for before 0.62
|
|
504
|
+
this._carouselRef.getNode &&
|
|
505
|
+
// @ts-expect-error This is for before 0.62
|
|
506
|
+
this._carouselRef.getNode()
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
_getScrollEnabled() {
|
|
511
|
+
return this._scrollEnabled;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
_setScrollEnabled(scrollEnabled = true) {
|
|
515
|
+
const wrappedRef = this._getWrappedRef();
|
|
516
|
+
|
|
517
|
+
if (!wrappedRef || !wrappedRef.setNativeProps) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// 'setNativeProps()' is used instead of 'setState()' because the latter
|
|
522
|
+
// really takes a toll on Android behavior when momentum is disabled
|
|
523
|
+
wrappedRef.setNativeProps({scrollEnabled});
|
|
524
|
+
this._scrollEnabled = scrollEnabled;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
_getItemMainDimension() {
|
|
528
|
+
return this.props.vertical ? this.props.itemHeight : this.props.itemWidth;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
_getItemScrollOffset(index) {
|
|
532
|
+
return (
|
|
533
|
+
this._positions && this._positions[index] && this._positions[index].start
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
_getItemLayout(_, index) {
|
|
538
|
+
const itemMainDimension = this._getItemMainDimension();
|
|
539
|
+
return {
|
|
540
|
+
index,
|
|
541
|
+
length: itemMainDimension,
|
|
542
|
+
offset: itemMainDimension * index, // + this._getContainerInnerMargin()
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// This will allow us to have a proper zIndex even with a FlatList
|
|
547
|
+
// https://github.com/facebook/react-native/issues/18616#issuecomment-389444165
|
|
548
|
+
_getCellRendererComponent({children, index, style, ...props}) {
|
|
549
|
+
const cellStyle = [
|
|
550
|
+
style,
|
|
551
|
+
!IS_ANDROID ? {zIndex: this._getCustomDataLength() - index} : {},
|
|
552
|
+
];
|
|
553
|
+
|
|
554
|
+
return (
|
|
555
|
+
<View style={cellStyle} key={index} {...props}>
|
|
556
|
+
{children}
|
|
557
|
+
</View>
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
_getKeyExtractor(_, index) {
|
|
562
|
+
return this._needsScrollView()
|
|
563
|
+
? `scrollview-item-${index}`
|
|
564
|
+
: `flatlist-item-${index}`;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
_getScrollOffset(event) {
|
|
568
|
+
const {vertical} = this.props;
|
|
569
|
+
return (
|
|
570
|
+
(event &&
|
|
571
|
+
event.nativeEvent &&
|
|
572
|
+
event.nativeEvent.contentOffset &&
|
|
573
|
+
event.nativeEvent.contentOffset[vertical ? 'y' : 'x']) ||
|
|
574
|
+
0
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
_getContainerInnerMargin(opposite = false) {
|
|
579
|
+
const {activeSlideAlignment} = this.props;
|
|
580
|
+
|
|
581
|
+
if (
|
|
582
|
+
(activeSlideAlignment === 'start' && !opposite) ||
|
|
583
|
+
(activeSlideAlignment === 'end' && opposite)
|
|
584
|
+
) {
|
|
585
|
+
return 0;
|
|
586
|
+
} else if (
|
|
587
|
+
(activeSlideAlignment === 'end' && !opposite) ||
|
|
588
|
+
(activeSlideAlignment === 'start' && opposite)
|
|
589
|
+
) {
|
|
590
|
+
return this.props.vertical
|
|
591
|
+
? this.props.sliderHeight - this.props.itemHeight
|
|
592
|
+
: this.props.sliderWidth - this.props.itemWidth;
|
|
593
|
+
} else {
|
|
594
|
+
return this.props.vertical
|
|
595
|
+
? (this.props.sliderHeight - this.props.itemHeight) / 2
|
|
596
|
+
: (this.props.sliderWidth - this.props.itemWidth) / 2;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
_getActiveSlideOffset() {
|
|
601
|
+
const {activeSlideOffset} = this.props;
|
|
602
|
+
const itemMainDimension = this._getItemMainDimension();
|
|
603
|
+
const minOffset = 10;
|
|
604
|
+
// Make sure activeSlideOffset never prevents the active area from being at least 10 px wide
|
|
605
|
+
return itemMainDimension / 2 - activeSlideOffset >= minOffset
|
|
606
|
+
? activeSlideOffset
|
|
607
|
+
: minOffset;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
_getActiveItem(offset) {
|
|
611
|
+
const itemMainDimension = this._getItemMainDimension();
|
|
612
|
+
const center = offset + itemMainDimension / 2;
|
|
613
|
+
const activeSlideOffset = this._getActiveSlideOffset();
|
|
614
|
+
const lastIndex = this._positions.length - 1;
|
|
615
|
+
let itemIndex;
|
|
616
|
+
|
|
617
|
+
if (offset <= 0) {
|
|
618
|
+
return 0;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (
|
|
622
|
+
this._positions[lastIndex] &&
|
|
623
|
+
offset >= this._positions[lastIndex].start
|
|
624
|
+
) {
|
|
625
|
+
return lastIndex;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
for (let i = 0; i < this._positions.length; i++) {
|
|
629
|
+
const {start, end} = this._positions[i];
|
|
630
|
+
if (
|
|
631
|
+
center + activeSlideOffset >= start &&
|
|
632
|
+
center - activeSlideOffset <= end
|
|
633
|
+
) {
|
|
634
|
+
itemIndex = i;
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return itemIndex || 0;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
_getSlideInterpolatedStyle(index, animatedValue) {
|
|
643
|
+
const {layoutCardOffset, slideInterpolatedStyle} = this.props;
|
|
644
|
+
|
|
645
|
+
if (slideInterpolatedStyle) {
|
|
646
|
+
return slideInterpolatedStyle(index, animatedValue, this.props);
|
|
647
|
+
} else if (this._shouldUseTinderLayout()) {
|
|
648
|
+
return tinderAnimatedStyles(
|
|
649
|
+
index,
|
|
650
|
+
animatedValue,
|
|
651
|
+
this.props,
|
|
652
|
+
layoutCardOffset,
|
|
653
|
+
);
|
|
654
|
+
} else if (this._shouldUseStackLayout()) {
|
|
655
|
+
return stackAnimatedStyles(
|
|
656
|
+
index,
|
|
657
|
+
animatedValue,
|
|
658
|
+
this.props,
|
|
659
|
+
layoutCardOffset,
|
|
660
|
+
);
|
|
661
|
+
} else if (this._shouldUseShiftLayout()) {
|
|
662
|
+
return shiftAnimatedStyles(index, animatedValue, this.props);
|
|
663
|
+
} else {
|
|
664
|
+
return defaultAnimatedStyles(index, animatedValue, this.props);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
_initPositionsAndInterpolators(props = this.props) {
|
|
669
|
+
const {data, scrollInterpolator} = props;
|
|
670
|
+
const itemMainDimension = this._getItemMainDimension();
|
|
671
|
+
|
|
672
|
+
if (!data || !data.length) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const interpolators = [];
|
|
677
|
+
this._positions = [];
|
|
678
|
+
|
|
679
|
+
this._getCustomData(props).forEach((_itemData, index) => {
|
|
680
|
+
const _index = this._getCustomIndex(index, props);
|
|
681
|
+
let animatedValue;
|
|
682
|
+
|
|
683
|
+
this._positions[index] = {
|
|
684
|
+
start: index * itemMainDimension,
|
|
685
|
+
end: index * itemMainDimension + itemMainDimension,
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
if (!this._shouldAnimateSlides(props) || !this._scrollPos) {
|
|
689
|
+
animatedValue = new Animated.Value(1);
|
|
690
|
+
} else {
|
|
691
|
+
let interpolator;
|
|
692
|
+
|
|
693
|
+
if (scrollInterpolator) {
|
|
694
|
+
interpolator = scrollInterpolator(_index, props);
|
|
695
|
+
} else if (this._shouldUseStackLayout()) {
|
|
696
|
+
interpolator = stackScrollInterpolator(_index, props);
|
|
697
|
+
} else if (this._shouldUseTinderLayout()) {
|
|
698
|
+
interpolator = tinderScrollInterpolator(_index, props);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (
|
|
702
|
+
!interpolator ||
|
|
703
|
+
!interpolator.inputRange ||
|
|
704
|
+
!interpolator.outputRange
|
|
705
|
+
) {
|
|
706
|
+
interpolator = defaultScrollInterpolator(_index, props);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
animatedValue = this._scrollPos.interpolate({
|
|
710
|
+
...interpolator,
|
|
711
|
+
extrapolate: 'clamp',
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
interpolators.push(animatedValue);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
this.setState({interpolators});
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
_hackActiveSlideAnimation(index, scrollValue = 1) {
|
|
722
|
+
const offset = this._getItemScrollOffset(index);
|
|
723
|
+
|
|
724
|
+
if (!this._mounted || !this._carouselRef || typeof offset === 'undefined') {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const multiplier = this._currentScrollOffset === 0 ? 1 : -1;
|
|
729
|
+
const scrollDelta = scrollValue * multiplier;
|
|
730
|
+
|
|
731
|
+
this._scrollTo({
|
|
732
|
+
offset: offset + scrollDelta,
|
|
733
|
+
animated: false,
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
// @ts-expect-error setTimeout / clearTiemout is buggy :/
|
|
737
|
+
clearTimeout(this._hackSlideAnimationTimeout);
|
|
738
|
+
this._hackSlideAnimationTimeout = setTimeout(() => {
|
|
739
|
+
this._scrollTo({
|
|
740
|
+
offset,
|
|
741
|
+
animated: false,
|
|
742
|
+
});
|
|
743
|
+
}, 1); // works randomly when set to '0'
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
_repositionScroll(index, animated = false) {
|
|
747
|
+
const {data, loopClonesPerSide} = this.props;
|
|
748
|
+
const dataLength = data && data.length;
|
|
749
|
+
|
|
750
|
+
if (typeof index === 'undefined' || !this._shouldRepositionScroll(index)) {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
let repositionTo = index;
|
|
755
|
+
|
|
756
|
+
if (index >= dataLength + loopClonesPerSide) {
|
|
757
|
+
repositionTo = index - dataLength;
|
|
758
|
+
} else if (index < loopClonesPerSide) {
|
|
759
|
+
repositionTo = index + dataLength;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
this._snapToItem(repositionTo, animated, false);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
_scrollTo({offset, index, animated = true}) {
|
|
766
|
+
const {vertical} = this.props;
|
|
767
|
+
const wrappedRef = this._getWrappedRef();
|
|
768
|
+
if (
|
|
769
|
+
!this._mounted ||
|
|
770
|
+
!wrappedRef ||
|
|
771
|
+
(typeof offset === 'undefined' && typeof index === 'undefined')
|
|
772
|
+
) {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
let scrollToOffset;
|
|
777
|
+
if (typeof index !== 'undefined') {
|
|
778
|
+
scrollToOffset = this._getItemScrollOffset(index);
|
|
779
|
+
} else {
|
|
780
|
+
scrollToOffset = offset;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (typeof scrollToOffset === 'undefined') {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const options = this._needsScrollView()
|
|
788
|
+
? {
|
|
789
|
+
x: vertical ? 0 : offset,
|
|
790
|
+
y: vertical ? offset : 0,
|
|
791
|
+
animated,
|
|
792
|
+
}
|
|
793
|
+
: {
|
|
794
|
+
offset,
|
|
795
|
+
animated,
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
if (this._needsScrollView()) {
|
|
799
|
+
wrappedRef.scrollTo(options);
|
|
800
|
+
} else {
|
|
801
|
+
wrappedRef.scrollToOffset(options);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
_onTouchStart(event) {
|
|
806
|
+
const {onTouchStart} = this.props;
|
|
807
|
+
|
|
808
|
+
// `onTouchStart` is fired even when `scrollEnabled` is set to `false`
|
|
809
|
+
if (this._getScrollEnabled() !== false && this._autoplaying) {
|
|
810
|
+
this.pauseAutoPlay();
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
onTouchStart && onTouchStart(event);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
_onTouchEnd(event) {
|
|
817
|
+
const {onTouchEnd} = this.props;
|
|
818
|
+
|
|
819
|
+
if (
|
|
820
|
+
this._getScrollEnabled() !== false &&
|
|
821
|
+
this._autoplay &&
|
|
822
|
+
!this._autoplaying
|
|
823
|
+
) {
|
|
824
|
+
// This event is buggy on Android, so a fallback is provided in _onMomentumScrollEnd()
|
|
825
|
+
this.startAutoplay();
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
onTouchEnd && onTouchEnd(event);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
_onScroll(event) {
|
|
832
|
+
const {onScroll, onScrollIndexChanged, onSnapToItem} = this.props;
|
|
833
|
+
const scrollOffset = event
|
|
834
|
+
? this._getScrollOffset(event)
|
|
835
|
+
: this._currentScrollOffset;
|
|
836
|
+
const nextActiveItem = this._getActiveItem(scrollOffset);
|
|
837
|
+
const dataLength = this._getCustomDataLength();
|
|
838
|
+
const lastItemScrollOffset = this._getItemScrollOffset(dataLength - 1);
|
|
839
|
+
|
|
840
|
+
this._currentScrollOffset = scrollOffset;
|
|
841
|
+
|
|
842
|
+
if (nextActiveItem !== this._onScrollActiveItem) {
|
|
843
|
+
this._onScrollActiveItem = nextActiveItem;
|
|
844
|
+
onScrollIndexChanged &&
|
|
845
|
+
onScrollIndexChanged(this._getDataIndex(nextActiveItem));
|
|
846
|
+
|
|
847
|
+
onSnapToItem && onSnapToItem(this._getDataIndex(nextActiveItem));
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
//last item
|
|
851
|
+
if (
|
|
852
|
+
(IS_IOS && scrollOffset > lastItemScrollOffset) ||
|
|
853
|
+
(IS_ANDROID &&
|
|
854
|
+
Math.floor(scrollOffset) > Math.floor(lastItemScrollOffset))
|
|
855
|
+
) {
|
|
856
|
+
this._activeItem = nextActiveItem;
|
|
857
|
+
this._repositionScroll(nextActiveItem);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (typeof onScroll === 'function' && event) {
|
|
861
|
+
onScroll(event);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
_onMomentumScrollEnd(event) {
|
|
866
|
+
const {autoplayDelay, onMomentumScrollEnd, onSnapToItem} = this.props;
|
|
867
|
+
const scrollOffset = event
|
|
868
|
+
? this._getScrollOffset(event)
|
|
869
|
+
: this._currentScrollOffset;
|
|
870
|
+
const nextActiveItem = this._getActiveItem(scrollOffset);
|
|
871
|
+
const hasSnapped = this._isMultiple(
|
|
872
|
+
scrollOffset,
|
|
873
|
+
this.props.vertical ? this.props.itemHeight : this.props.itemWidth,
|
|
874
|
+
);
|
|
875
|
+
|
|
876
|
+
// WARNING: everything in this condition will probably need to be called on _snapToItem as well because:
|
|
877
|
+
// 1. `onMomentumScrollEnd` won't be called if the scroll isn't animated
|
|
878
|
+
// 2. `onMomentumScrollEnd` won't be called at all on Android when scrolling programmatically
|
|
879
|
+
if (nextActiveItem !== this._activeItem) {
|
|
880
|
+
this._activeItem = nextActiveItem;
|
|
881
|
+
onSnapToItem && onSnapToItem(this._getDataIndex(nextActiveItem));
|
|
882
|
+
|
|
883
|
+
if (hasSnapped && IS_ANDROID) {
|
|
884
|
+
this._repositionScroll(nextActiveItem);
|
|
885
|
+
} else if (IS_IOS) {
|
|
886
|
+
this._repositionScroll(nextActiveItem);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
onMomentumScrollEnd && onMomentumScrollEnd(event);
|
|
891
|
+
|
|
892
|
+
// The touchEnd event is buggy on Android, so this will serve as a fallback whenever needed
|
|
893
|
+
// https://github.com/facebook/react-native/issues/9439
|
|
894
|
+
if (IS_ANDROID && this._autoplay && !this._autoplaying) {
|
|
895
|
+
// @ts-expect-error setTimeout / clearTiemout is buggy :/
|
|
896
|
+
clearTimeout(this._enableAutoplayTimeout);
|
|
897
|
+
this._enableAutoplayTimeout = setTimeout(() => {
|
|
898
|
+
this.startAutoplay();
|
|
899
|
+
}, autoplayDelay);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
_onLayout(event) {
|
|
904
|
+
const {onLayout} = this.props;
|
|
905
|
+
|
|
906
|
+
// Prevent unneeded actions during the first 'onLayout' (triggered on init)
|
|
907
|
+
if (this._onLayoutInitDone) {
|
|
908
|
+
this._initPositionsAndInterpolators();
|
|
909
|
+
this._snapToItem(this._activeItem, false, false, true);
|
|
910
|
+
} else {
|
|
911
|
+
this._onLayoutInitDone = true;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
onLayout && onLayout(event);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
_snapToItem(
|
|
918
|
+
index,
|
|
919
|
+
animated = true,
|
|
920
|
+
fireCallback = true,
|
|
921
|
+
forceScrollTo = false,
|
|
922
|
+
) {
|
|
923
|
+
const {onSnapToItem} = this.props;
|
|
924
|
+
const itemsLength = this._getCustomDataLength();
|
|
925
|
+
const wrappedRef = this._getWrappedRef();
|
|
926
|
+
if (!itemsLength || !wrappedRef) {
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (!index || index < 0) {
|
|
931
|
+
index = 0;
|
|
932
|
+
} else if (itemsLength > 0 && index >= itemsLength) {
|
|
933
|
+
index = itemsLength - 1;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if (index === this._activeItem && !forceScrollTo) {
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const offset = this._getItemScrollOffset(index);
|
|
941
|
+
|
|
942
|
+
if (offset === undefined) {
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
this._scrollTo({
|
|
947
|
+
offset,
|
|
948
|
+
animated,
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
// On both platforms, `onMomentumScrollEnd` won't be triggered if the scroll isn't animated
|
|
952
|
+
// so we need to trigger the callback manually
|
|
953
|
+
// On Android `onMomentumScrollEnd` won't be triggered when scrolling programmatically
|
|
954
|
+
// Therefore everything critical needs to be manually called here as well, even though the timing might be off
|
|
955
|
+
const requiresManualTrigger = !animated || IS_ANDROID;
|
|
956
|
+
if (requiresManualTrigger) {
|
|
957
|
+
this._activeItem = index;
|
|
958
|
+
|
|
959
|
+
if (fireCallback) {
|
|
960
|
+
onSnapToItem && onSnapToItem(this._getDataIndex(index));
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Repositioning on Android
|
|
964
|
+
if (IS_ANDROID && this._shouldRepositionScroll(index)) {
|
|
965
|
+
if (animated) {
|
|
966
|
+
this._androidRepositioningTimeout = setTimeout(() => {
|
|
967
|
+
// Without scroll animation, the behavior is completely buggy...
|
|
968
|
+
this._repositionScroll(index, false);
|
|
969
|
+
}, 400); // Approximate scroll duration on Android
|
|
970
|
+
} else {
|
|
971
|
+
this._repositionScroll(index);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
startAutoplay() {
|
|
978
|
+
const {autoplayInterval, autoplayDelay} = this.props;
|
|
979
|
+
this._autoplay = true;
|
|
980
|
+
|
|
981
|
+
if (this._autoplaying) {
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// @ts-expect-error setTimeout / clearTiemout is buggy :/
|
|
986
|
+
clearTimeout(this._autoplayTimeout);
|
|
987
|
+
this._autoplayTimeout = setTimeout(() => {
|
|
988
|
+
this._autoplaying = true;
|
|
989
|
+
this._autoplayInterval = setInterval(() => {
|
|
990
|
+
if (this._autoplaying) {
|
|
991
|
+
this.snapToNext();
|
|
992
|
+
}
|
|
993
|
+
}, autoplayInterval);
|
|
994
|
+
}, autoplayDelay);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
pauseAutoPlay() {
|
|
998
|
+
this._autoplaying = false;
|
|
999
|
+
// @ts-expect-error setTimeout / clearTiemout is buggy :/
|
|
1000
|
+
clearTimeout(this._autoplayTimeout);
|
|
1001
|
+
// @ts-expect-error setTimeout / clearTiemout is buggy :/
|
|
1002
|
+
clearTimeout(this._enableAutoplayTimeout);
|
|
1003
|
+
// @ts-expect-error setTimeout / clearTiemout is buggy :/
|
|
1004
|
+
clearInterval(this._autoplayInterval);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
stopAutoplay() {
|
|
1008
|
+
this._autoplay = false;
|
|
1009
|
+
this.pauseAutoPlay();
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
snapToNext(animated = true, fireCallback = true) {
|
|
1013
|
+
const itemsLength = this._getCustomDataLength();
|
|
1014
|
+
|
|
1015
|
+
let newIndex = this._activeItem + 1;
|
|
1016
|
+
if (newIndex > itemsLength - 1) {
|
|
1017
|
+
newIndex = 0;
|
|
1018
|
+
}
|
|
1019
|
+
this._snapToItem(newIndex, animated, fireCallback);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
snapToPrev(animated = true, fireCallback = true) {
|
|
1023
|
+
const itemsLength = this._getCustomDataLength();
|
|
1024
|
+
|
|
1025
|
+
let newIndex = this._activeItem - 1;
|
|
1026
|
+
if (newIndex < 0) {
|
|
1027
|
+
newIndex = itemsLength - 1;
|
|
1028
|
+
}
|
|
1029
|
+
this._snapToItem(newIndex, animated, fireCallback);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// https://github.com/facebook/react-native/issues/1831#issuecomment-231069668
|
|
1033
|
+
triggerRenderingHack(offset = 1) {
|
|
1034
|
+
this._hackActiveSlideAnimation(this._activeItem, offset);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
_renderItem({item, index}) {
|
|
1038
|
+
const {interpolators} = this.state;
|
|
1039
|
+
const {keyExtractor, slideStyle} = this.props;
|
|
1040
|
+
const animatedValue = interpolators && interpolators[index];
|
|
1041
|
+
|
|
1042
|
+
if (typeof animatedValue === 'undefined') {
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const animate = this._shouldAnimateSlides();
|
|
1047
|
+
const Component = animate ? Animated.View : View;
|
|
1048
|
+
const animatedStyle = animate
|
|
1049
|
+
? this._getSlideInterpolatedStyle(index, animatedValue)
|
|
1050
|
+
: {};
|
|
1051
|
+
const dataIndex = this._getDataIndex(index);
|
|
1052
|
+
|
|
1053
|
+
const mainDimension = this.props.vertical
|
|
1054
|
+
? {height: this.props.itemHeight}
|
|
1055
|
+
: {width: this.props.itemWidth};
|
|
1056
|
+
const specificProps = this._needsScrollView()
|
|
1057
|
+
? {
|
|
1058
|
+
key: keyExtractor
|
|
1059
|
+
? keyExtractor(item, index)
|
|
1060
|
+
: this._getKeyExtractor(item, index),
|
|
1061
|
+
}
|
|
1062
|
+
: {};
|
|
1063
|
+
|
|
1064
|
+
return (
|
|
1065
|
+
<Component
|
|
1066
|
+
style={[mainDimension, slideStyle, animatedStyle]}
|
|
1067
|
+
pointerEvents="box-none"
|
|
1068
|
+
{...specificProps}>
|
|
1069
|
+
{this.props.vertical
|
|
1070
|
+
? this.props.renderItem(
|
|
1071
|
+
{
|
|
1072
|
+
item,
|
|
1073
|
+
index,
|
|
1074
|
+
dataIndex,
|
|
1075
|
+
realIndex: this._getDataIndex(index),
|
|
1076
|
+
activeIndex: this._getDataIndex(this._activeItem),
|
|
1077
|
+
},
|
|
1078
|
+
{
|
|
1079
|
+
scrollPosition: this._scrollPos,
|
|
1080
|
+
carouselRef: this._carouselRef,
|
|
1081
|
+
vertical: this.props.vertical,
|
|
1082
|
+
sliderHeight: this.props.sliderHeight,
|
|
1083
|
+
itemHeight: this.props.itemHeight,
|
|
1084
|
+
},
|
|
1085
|
+
)
|
|
1086
|
+
: this.props.renderItem(
|
|
1087
|
+
{
|
|
1088
|
+
item,
|
|
1089
|
+
index,
|
|
1090
|
+
dataIndex,
|
|
1091
|
+
realIndex: this._getDataIndex(index),
|
|
1092
|
+
activeIndex: this._getDataIndex(this._activeItem),
|
|
1093
|
+
},
|
|
1094
|
+
{
|
|
1095
|
+
scrollPosition: this._scrollPos,
|
|
1096
|
+
carouselRef: this._carouselRef,
|
|
1097
|
+
vertical: !!this.props.vertical,
|
|
1098
|
+
sliderWidth: this.props.sliderWidth,
|
|
1099
|
+
itemWidth: this.props.itemWidth,
|
|
1100
|
+
},
|
|
1101
|
+
)}
|
|
1102
|
+
</Component>
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
_getComponentOverridableProps() {
|
|
1107
|
+
const {hideCarousel} = this.state;
|
|
1108
|
+
const {loopClonesPerSide} = this.props;
|
|
1109
|
+
const visibleItems =
|
|
1110
|
+
Math.ceil(
|
|
1111
|
+
this.props.vertical
|
|
1112
|
+
? this.props.sliderHeight / this.props.itemHeight
|
|
1113
|
+
: this.props.sliderWidth / this.props.itemWidth,
|
|
1114
|
+
) + 1;
|
|
1115
|
+
const initialNumPerSide = this._enableLoop() ? loopClonesPerSide : 2;
|
|
1116
|
+
const initialNumToRender =
|
|
1117
|
+
visibleItems > 2
|
|
1118
|
+
? visibleItems + initialNumPerSide * 2
|
|
1119
|
+
: initialNumPerSide * 2;
|
|
1120
|
+
const maxToRenderPerBatch = initialNumToRender;
|
|
1121
|
+
const windowSize = maxToRenderPerBatch;
|
|
1122
|
+
const specificProps = !this._needsScrollView()
|
|
1123
|
+
? {
|
|
1124
|
+
initialNumToRender,
|
|
1125
|
+
maxToRenderPerBatch,
|
|
1126
|
+
windowSize,
|
|
1127
|
+
// updateCellsBatchingPeriod
|
|
1128
|
+
}
|
|
1129
|
+
: {};
|
|
1130
|
+
|
|
1131
|
+
return {
|
|
1132
|
+
...specificProps,
|
|
1133
|
+
automaticallyAdjustContentInsets: false,
|
|
1134
|
+
decelerationRate: 'fast',
|
|
1135
|
+
directionalLockEnabled: true,
|
|
1136
|
+
disableScrollViewPanResponder: false, // If set to `true`, touch events will be triggered too easily
|
|
1137
|
+
inverted: this._needsRTLAdaptations(),
|
|
1138
|
+
overScrollMode: 'never',
|
|
1139
|
+
pinchGestureEnabled: false,
|
|
1140
|
+
pointerEvents: hideCarousel ? 'none' : 'auto',
|
|
1141
|
+
// removeClippedSubviews: !this._needsScrollView(),
|
|
1142
|
+
// renderToHardwareTextureAndroid: true,
|
|
1143
|
+
scrollsToTop: false,
|
|
1144
|
+
showsHorizontalScrollIndicator: false,
|
|
1145
|
+
showsVerticalScrollIndicator: false,
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
_getComponentStaticProps() {
|
|
1150
|
+
const {hideCarousel} = this.state;
|
|
1151
|
+
const {
|
|
1152
|
+
activeSlideAlignment,
|
|
1153
|
+
CellRendererComponent,
|
|
1154
|
+
containerCustomStyle,
|
|
1155
|
+
contentContainerCustomStyle,
|
|
1156
|
+
firstItem,
|
|
1157
|
+
getItemLayout,
|
|
1158
|
+
keyExtractor,
|
|
1159
|
+
style,
|
|
1160
|
+
useExperimentalSnap,
|
|
1161
|
+
disableIntervalMomentum,
|
|
1162
|
+
vertical,
|
|
1163
|
+
enableSnap,
|
|
1164
|
+
} = this.props;
|
|
1165
|
+
const containerStyle = [
|
|
1166
|
+
// { overflow: 'hidden' },
|
|
1167
|
+
containerCustomStyle || style || {},
|
|
1168
|
+
hideCarousel ? {opacity: 0} : {},
|
|
1169
|
+
this.props.vertical
|
|
1170
|
+
? {
|
|
1171
|
+
height: this.props.sliderHeight,
|
|
1172
|
+
flexDirection: 'column',
|
|
1173
|
+
} // LTR hack; see https://github.com/facebook/react-native/issues/11960
|
|
1174
|
+
: // and https://github.com/facebook/react-native/issues/13100#issuecomment-328986423
|
|
1175
|
+
{
|
|
1176
|
+
width: this.props.sliderWidth,
|
|
1177
|
+
flexDirection: this._needsRTLAdaptations() ? 'row-reverse' : 'row',
|
|
1178
|
+
},
|
|
1179
|
+
];
|
|
1180
|
+
|
|
1181
|
+
const innerMarginStyle = this.props.vertical
|
|
1182
|
+
? {
|
|
1183
|
+
paddingTop: this._getContainerInnerMargin(),
|
|
1184
|
+
paddingBottom: this._getContainerInnerMargin(true),
|
|
1185
|
+
}
|
|
1186
|
+
: {
|
|
1187
|
+
paddingLeft: this._getContainerInnerMargin(),
|
|
1188
|
+
paddingRight: this._getContainerInnerMargin(true),
|
|
1189
|
+
};
|
|
1190
|
+
|
|
1191
|
+
const contentContainerStyle = [
|
|
1192
|
+
vertical
|
|
1193
|
+
? {
|
|
1194
|
+
paddingTop: this._getContainerInnerMargin(),
|
|
1195
|
+
paddingBottom: this._getContainerInnerMargin(true),
|
|
1196
|
+
}
|
|
1197
|
+
: {
|
|
1198
|
+
paddingLeft: this._getContainerInnerMargin(),
|
|
1199
|
+
paddingRight: this._getContainerInnerMargin(true),
|
|
1200
|
+
},
|
|
1201
|
+
contentContainerCustomStyle || {},
|
|
1202
|
+
];
|
|
1203
|
+
|
|
1204
|
+
// WARNING: `snapToAlignment` won't work as intended because of the following:
|
|
1205
|
+
// https://github.com/facebook/react-native/blob/d0871d0a9a373e1d3ac35da46c85c0d0e793116d/React/Views/ScrollView/RCTScrollView.m#L751-L755
|
|
1206
|
+
// - Snap points will be off
|
|
1207
|
+
// - Slide animations will be off
|
|
1208
|
+
// - Last items won't be set as active (no `onSnapToItem` callback)
|
|
1209
|
+
// Recommended only with large slides and `activeSlideAlignment` set to `start` for the time being
|
|
1210
|
+
const snapProps =
|
|
1211
|
+
enableSnap && useExperimentalSnap
|
|
1212
|
+
? {
|
|
1213
|
+
disableIntervalMomentum, // Slide ± one item at a time
|
|
1214
|
+
snapToAlignment: activeSlideAlignment,
|
|
1215
|
+
snapToInterval: this._getItemMainDimension(),
|
|
1216
|
+
}
|
|
1217
|
+
: {
|
|
1218
|
+
snapToOffsets: this._getSnapOffsets(),
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
// Flatlist specifics
|
|
1222
|
+
const specificProps = !this._needsScrollView()
|
|
1223
|
+
? {
|
|
1224
|
+
CellRendererComponent:
|
|
1225
|
+
CellRendererComponent || this._getCellRendererComponent,
|
|
1226
|
+
getItemLayout: getItemLayout || this._getItemLayout,
|
|
1227
|
+
initialScrollIndex: this._getFirstItem(firstItem),
|
|
1228
|
+
keyExtractor: keyExtractor || this._getKeyExtractor,
|
|
1229
|
+
numColumns: 1,
|
|
1230
|
+
renderItem: this._renderItem,
|
|
1231
|
+
}
|
|
1232
|
+
: {};
|
|
1233
|
+
|
|
1234
|
+
return {
|
|
1235
|
+
...specificProps,
|
|
1236
|
+
...snapProps,
|
|
1237
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1238
|
+
ref: c => {
|
|
1239
|
+
this._carouselRef = c;
|
|
1240
|
+
},
|
|
1241
|
+
contentContainerStyle: contentContainerStyle,
|
|
1242
|
+
data: this._getCustomData(),
|
|
1243
|
+
horizontal: !this.props.vertical,
|
|
1244
|
+
scrollEventThrottle: 1,
|
|
1245
|
+
style: containerStyle,
|
|
1246
|
+
onLayout: this._onLayout,
|
|
1247
|
+
onMomentumScrollEnd: this._onMomentumScrollEnd,
|
|
1248
|
+
onScroll: this._onScrollHandler,
|
|
1249
|
+
onTouchStart: this._onTouchStart,
|
|
1250
|
+
onTouchEnd: this._onTouchEnd,
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
render() {
|
|
1255
|
+
const {data, renderItem, useScrollView} = this.props;
|
|
1256
|
+
|
|
1257
|
+
if (!data || !renderItem) {
|
|
1258
|
+
return null;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
const props = {
|
|
1262
|
+
...this._getComponentOverridableProps(),
|
|
1263
|
+
...this.props,
|
|
1264
|
+
...this._getComponentStaticProps(),
|
|
1265
|
+
};
|
|
1266
|
+
|
|
1267
|
+
const ScrollViewComponent =
|
|
1268
|
+
typeof useScrollView === 'function' ? useScrollView : Animated.ScrollView;
|
|
1269
|
+
return this._needsScrollView() || !Animated.FlatList ? (
|
|
1270
|
+
<ScrollViewComponent {...props}>
|
|
1271
|
+
{this._getCustomData().map((item, index) => {
|
|
1272
|
+
return this._renderItem({
|
|
1273
|
+
item,
|
|
1274
|
+
index,
|
|
1275
|
+
realIndex: this._getDataIndex(index),
|
|
1276
|
+
activeIndex: this._getDataIndex(this._activeItem),
|
|
1277
|
+
});
|
|
1278
|
+
})}
|
|
1279
|
+
</ScrollViewComponent>
|
|
1280
|
+
) : (
|
|
1281
|
+
// @ts-expect-error Seems complicated to make TS 100% happy, while sharing that many things between
|
|
1282
|
+
// flatlist && scrollview implementation. I'll prob try to rewrite parts of the logic to overcome that.
|
|
1283
|
+
<Animated.FlatList {...props} />
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
Carousel.propTypes = {
|
|
1289
|
+
data: PropTypes.array.isRequired,
|
|
1290
|
+
renderItem: PropTypes.func.isRequired,
|
|
1291
|
+
itemWidth: PropTypes.number, // required for horizontal carousel
|
|
1292
|
+
itemHeight: PropTypes.number, // required for vertical carousel
|
|
1293
|
+
sliderWidth: PropTypes.number, // required for horizontal carousel
|
|
1294
|
+
sliderHeight: PropTypes.number, // required for vertical carousel
|
|
1295
|
+
activeSlideAlignment: PropTypes.oneOf(['center', 'end', 'start']),
|
|
1296
|
+
activeSlideOffset: PropTypes.number,
|
|
1297
|
+
apparitionDelay: PropTypes.number,
|
|
1298
|
+
autoplay: PropTypes.bool,
|
|
1299
|
+
autoplayDelay: PropTypes.number,
|
|
1300
|
+
autoplayInterval: PropTypes.number,
|
|
1301
|
+
callbackOffsetMargin: PropTypes.number,
|
|
1302
|
+
containerCustomStyle: PropTypes.oneOfType([
|
|
1303
|
+
PropTypes.object,
|
|
1304
|
+
PropTypes.array,
|
|
1305
|
+
]),
|
|
1306
|
+
contentContainerCustomStyle: PropTypes.oneOfType([
|
|
1307
|
+
PropTypes.object,
|
|
1308
|
+
PropTypes.array,
|
|
1309
|
+
]),
|
|
1310
|
+
enableSnap: PropTypes.bool,
|
|
1311
|
+
firstItem: PropTypes.number,
|
|
1312
|
+
hasParallaxImages: PropTypes.bool,
|
|
1313
|
+
inactiveSlideOpacity: PropTypes.number,
|
|
1314
|
+
inactiveSlideScale: PropTypes.number,
|
|
1315
|
+
inactiveSlideShift: PropTypes.number,
|
|
1316
|
+
layout: PropTypes.oneOf(['default', 'stack', 'tinder']),
|
|
1317
|
+
layoutCardOffset: PropTypes.number,
|
|
1318
|
+
loop: PropTypes.bool,
|
|
1319
|
+
loopClonesPerSide: PropTypes.number,
|
|
1320
|
+
scrollEnabled: PropTypes.bool,
|
|
1321
|
+
scrollInterpolator: PropTypes.func,
|
|
1322
|
+
slideInterpolatedStyle: PropTypes.func,
|
|
1323
|
+
slideStyle: PropTypes.object,
|
|
1324
|
+
shouldOptimizeUpdates: PropTypes.bool,
|
|
1325
|
+
swipeThreshold: PropTypes.number,
|
|
1326
|
+
useScrollView: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
|
|
1327
|
+
vertical: PropTypes.bool,
|
|
1328
|
+
showsPagination: PropTypes.bool,
|
|
1329
|
+
isCustomScrollWidth: PropTypes.bool,
|
|
1330
|
+
disableIntervalMomentum: PropTypes.bool,
|
|
1331
|
+
useExperimentalSnap: PropTypes.bool,
|
|
1332
|
+
onBeforeSnapToItem: PropTypes.func,
|
|
1333
|
+
onSnapToItem: PropTypes.func,
|
|
1334
|
+
};
|
|
1335
|
+
|
|
1336
|
+
Carousel.defaultProps = {
|
|
1337
|
+
activeSlideAlignment: 'center',
|
|
1338
|
+
activeSlideOffset: 20,
|
|
1339
|
+
apparitionDelay: 0,
|
|
1340
|
+
autoplay: false,
|
|
1341
|
+
autoplayDelay: 1000,
|
|
1342
|
+
autoplayInterval: 3000,
|
|
1343
|
+
callbackOffsetMargin: 5,
|
|
1344
|
+
containerCustomStyle: {},
|
|
1345
|
+
contentContainerCustomStyle: {},
|
|
1346
|
+
enableSnap: true,
|
|
1347
|
+
firstItem: 0,
|
|
1348
|
+
hasParallaxImages: false,
|
|
1349
|
+
inactiveSlideOpacity: 0.7,
|
|
1350
|
+
inactiveSlideScale: 0.9,
|
|
1351
|
+
inactiveSlideShift: 0,
|
|
1352
|
+
layout: 'default',
|
|
1353
|
+
loop: false,
|
|
1354
|
+
loopClonesPerSide: 3,
|
|
1355
|
+
scrollEnabled: true,
|
|
1356
|
+
slideStyle: {},
|
|
1357
|
+
shouldOptimizeUpdates: true,
|
|
1358
|
+
useScrollView: !Animated.FlatList,
|
|
1359
|
+
vertical: false,
|
|
1360
|
+
isCustomScrollWidth: false,
|
|
1361
|
+
disableIntervalMomentum: IS_ANDROID,
|
|
1362
|
+
useExperimentalSnap: IS_ANDROID,
|
|
1363
|
+
};
|