react-native-snap-carousel
Version:
Swiper/carousel component for React Native with previews, multiple layouts, parallax images, performant handling of huge numbers of items, and RTL support. Compatible with Android & iOS.
1,372 lines (1,143 loc) • 48.5 kB
JavaScript
import React, { Component } from 'react';
import { Animated, Easing, FlatList, I18nManager, Platform, ScrollView, View, ViewPropTypes } from 'react-native';
import PropTypes from 'prop-types';
import shallowCompare from 'react-addons-shallow-compare';
import {
defaultScrollInterpolator,
stackScrollInterpolator,
tinderScrollInterpolator,
defaultAnimatedStyles,
shiftAnimatedStyles,
stackAnimatedStyles,
tinderAnimatedStyles
} from '../utils/animations';
const IS_IOS = Platform.OS === 'ios';
// Native driver for scroll events
// See: https://facebook.github.io/react-native/blog/2017/02/14/using-native-driver-for-animated.html
const AnimatedFlatList = FlatList ? Animated.createAnimatedComponent(FlatList) : null;
const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView);
// React Native automatically handles RTL layouts; unfortunately, it's buggy with horizontal ScrollView
// See https://github.com/facebook/react-native/issues/11960
// NOTE: the following variable is not declared in the constructor
// otherwise it is undefined at init, which messes with custom indexes
const IS_RTL = I18nManager.isRTL;
export default class Carousel extends Component {
static propTypes = {
data: PropTypes.array.isRequired,
renderItem: PropTypes.func.isRequired,
itemWidth: PropTypes.number, // required for horizontal carousel
itemHeight: PropTypes.number, // required for vertical carousel
sliderWidth: PropTypes.number, // required for horizontal carousel
sliderHeight: PropTypes.number, // required for vertical carousel
activeAnimationType: PropTypes.string,
activeAnimationOptions: PropTypes.object,
activeSlideAlignment: PropTypes.oneOf(['center', 'end', 'start']),
activeSlideOffset: PropTypes.number,
apparitionDelay: PropTypes.number,
autoplay: PropTypes.bool,
autoplayDelay: PropTypes.number,
autoplayInterval: PropTypes.number,
callbackOffsetMargin: PropTypes.number,
containerCustomStyle: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style,
contentContainerCustomStyle: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style,
enableMomentum: PropTypes.bool,
enableSnap: PropTypes.bool,
firstItem: PropTypes.number,
hasParallaxImages: PropTypes.bool,
inactiveSlideOpacity: PropTypes.number,
inactiveSlideScale: PropTypes.number,
inactiveSlideShift: PropTypes.number,
layout: PropTypes.oneOf(['default', 'stack', 'tinder']),
layoutCardOffset: PropTypes.number,
lockScrollTimeoutDuration: PropTypes.number,
lockScrollWhileSnapping: PropTypes.bool,
loop: PropTypes.bool,
loopClonesPerSide: PropTypes.number,
scrollEnabled: PropTypes.bool,
scrollInterpolator: PropTypes.func,
slideInterpolatedStyle: PropTypes.func,
slideStyle: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style,
shouldOptimizeUpdates: PropTypes.bool,
swipeThreshold: PropTypes.number,
useScrollView: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
vertical: PropTypes.bool,
onBeforeSnapToItem: PropTypes.func,
onSnapToItem: PropTypes.func
};
static defaultProps = {
activeAnimationType: 'timing',
activeAnimationOptions: null,
activeSlideAlignment: 'center',
activeSlideOffset: 20,
apparitionDelay: 0,
autoplay: false,
autoplayDelay: 1000,
autoplayInterval: 3000,
callbackOffsetMargin: 5,
containerCustomStyle: {},
contentContainerCustomStyle: {},
enableMomentum: false,
enableSnap: true,
firstItem: 0,
hasParallaxImages: false,
inactiveSlideOpacity: 0.7,
inactiveSlideScale: 0.9,
inactiveSlideShift: 0,
layout: 'default',
lockScrollTimeoutDuration: 1000,
lockScrollWhileSnapping: false,
loop: false,
loopClonesPerSide: 3,
scrollEnabled: true,
slideStyle: {},
shouldOptimizeUpdates: true,
swipeThreshold: 20,
useScrollView: !AnimatedFlatList,
vertical: false
}
constructor (props) {
super(props);
this.state = {
hideCarousel: true,
interpolators: []
};
// The following values are not stored in the state because 'setState()' is asynchronous
// and this results in an absolutely crappy behavior on Android while swiping (see #156)
const initialActiveItem = this._getFirstItem(props.firstItem);
this._activeItem = initialActiveItem;
this._previousActiveItem = initialActiveItem;
this._previousFirstItem = initialActiveItem;
this._previousItemsLength = initialActiveItem;
this._mounted = false;
this._positions = [];
this._currentContentOffset = 0; // store ScrollView's scroll position
this._canFireBeforeCallback = false;
this._canFireCallback = false;
this._scrollOffsetRef = null;
this._onScrollTriggered = true; // used when momentum is enabled to prevent an issue with edges items
this._lastScrollDate = 0; // used to work around a FlatList bug
this._scrollEnabled = props.scrollEnabled !== false;
this._initPositionsAndInterpolators = this._initPositionsAndInterpolators.bind(this);
this._renderItem = this._renderItem.bind(this);
this._onSnap = this._onSnap.bind(this);
this._onLayout = this._onLayout.bind(this);
this._onScroll = this._onScroll.bind(this);
this._onScrollBeginDrag = props.enableSnap ? this._onScrollBeginDrag.bind(this) : undefined;
this._onScrollEnd = props.enableSnap || props.autoplay ? this._onScrollEnd.bind(this) : undefined;
this._onScrollEndDrag = !props.enableMomentum ? this._onScrollEndDrag.bind(this) : undefined;
this._onMomentumScrollEnd = props.enableMomentum ? this._onMomentumScrollEnd.bind(this) : undefined;
this._onTouchStart = this._onTouchStart.bind(this);
this._onTouchEnd = this._onTouchEnd.bind(this);
this._onTouchRelease = this._onTouchRelease.bind(this);
this._getKeyExtractor = this._getKeyExtractor.bind(this);
this._setScrollHandler(props);
// This bool aims at fixing an iOS bug due to scrollTo that triggers onMomentumScrollEnd.
// onMomentumScrollEnd fires this._snapScroll, thus creating an infinite loop.
this._ignoreNextMomentum = false;
// Warnings
if (!ViewPropTypes) {
console.warn('react-native-snap-carousel: It is recommended to use at least version 0.44 of React Native with the plugin');
}
if (!props.vertical && (!props.sliderWidth || !props.itemWidth)) {
console.error('react-native-snap-carousel: You need to specify both `sliderWidth` and `itemWidth` for horizontal carousels');
}
if (props.vertical && (!props.sliderHeight || !props.itemHeight)) {
console.error('react-native-snap-carousel: You need to specify both `sliderHeight` and `itemHeight` for vertical carousels');
}
if (props.apparitionDelay && !IS_IOS && !props.useScrollView) {
console.warn('react-native-snap-carousel: Using `apparitionDelay` on Android is not recommended since it can lead to rendering issues');
}
if (props.customAnimationType || props.customAnimationOptions) {
console.warn('react-native-snap-carousel: Props `customAnimationType` and `customAnimationOptions` have been renamed to `activeAnimationType` and `activeAnimationOptions`');
}
if (props.onScrollViewScroll) {
console.error('react-native-snap-carousel: Prop `onScrollViewScroll` has been removed. Use `onScroll` instead');
}
}
componentDidMount () {
const { apparitionDelay, autoplay, firstItem } = this.props;
const _firstItem = this._getFirstItem(firstItem);
const apparitionCallback = () => {
this.setState({ hideCarousel: false });
if (autoplay) {
this.startAutoplay();
}
};
this._mounted = true;
this._initPositionsAndInterpolators();
// Without 'requestAnimationFrame' or a `0` timeout, images will randomly not be rendered on Android...
requestAnimationFrame(() => {
if (!this._mounted) {
return;
}
this._snapToItem(_firstItem, false, false, true, false);
this._hackActiveSlideAnimation(_firstItem, 'start', true);
if (apparitionDelay) {
this._apparitionTimeout = setTimeout(() => {
apparitionCallback();
}, apparitionDelay);
} else {
apparitionCallback();
}
});
}
shouldComponentUpdate (nextProps, nextState) {
if (this.props.shouldOptimizeUpdates === false) {
return true;
} else {
return shallowCompare(this, nextProps, nextState);
}
}
componentDidUpdate (prevProps) {
const { interpolators } = this.state;
const { firstItem, itemHeight, itemWidth, scrollEnabled, sliderHeight, sliderWidth } = this.props;
const itemsLength = this._getCustomDataLength(this.props);
if (!itemsLength) {
return;
}
const nextFirstItem = this._getFirstItem(firstItem, this.props);
let nextActiveItem = this._activeItem || this._activeItem === 0 ? this._activeItem : nextFirstItem;
const hasNewSliderWidth = sliderWidth && sliderWidth !== prevProps.sliderWidth;
const hasNewSliderHeight = sliderHeight && sliderHeight !== prevProps.sliderHeight;
const hasNewItemWidth = itemWidth && itemWidth !== prevProps.itemWidth;
const hasNewItemHeight = itemHeight && itemHeight !== prevProps.itemHeight;
const hasNewScrollEnabled = scrollEnabled !== prevProps.scrollEnabled;
// Prevent issues with dynamically removed items
if (nextActiveItem > itemsLength - 1) {
nextActiveItem = itemsLength - 1;
}
// Handle changing scrollEnabled independent of user -> carousel interaction
if (hasNewScrollEnabled) {
this._setScrollEnabled(scrollEnabled);
}
if (interpolators.length !== itemsLength || hasNewSliderWidth ||
hasNewSliderHeight || hasNewItemWidth || hasNewItemHeight) {
this._activeItem = nextActiveItem;
this._previousItemsLength = itemsLength;
this._initPositionsAndInterpolators(this.props);
// Handle scroll issue when dynamically removing items (see #133)
// This also fixes first item's active state on Android
// Because 'initialScrollIndex' apparently doesn't trigger scroll
if (this._previousItemsLength > itemsLength) {
this._hackActiveSlideAnimation(nextActiveItem, null, true);
}
if (hasNewSliderWidth || hasNewSliderHeight || hasNewItemWidth || hasNewItemHeight) {
this._snapToItem(nextActiveItem, false, false, false, false);
}
} else if (nextFirstItem !== this._previousFirstItem && nextFirstItem !== this._activeItem) {
this._activeItem = nextFirstItem;
this._previousFirstItem = nextFirstItem;
this._snapToItem(nextFirstItem, false, true, false, false);
}
if (this.props.onScroll !== prevProps.onScroll) {
this._setScrollHandler(this.props);
}
}
componentWillUnmount () {
this._mounted = false;
this.stopAutoplay();
clearTimeout(this._apparitionTimeout);
clearTimeout(this._hackSlideAnimationTimeout);
clearTimeout(this._enableAutoplayTimeout);
clearTimeout(this._autoplayTimeout);
clearTimeout(this._snapNoMomentumTimeout);
clearTimeout(this._edgeItemTimeout);
clearTimeout(this._lockScrollTimeout);
}
get realIndex () {
return this._activeItem;
}
get currentIndex () {
return this._getDataIndex(this._activeItem);
}
get currentScrollPosition () {
return this._currentContentOffset;
}
_setScrollHandler(props) {
// Native driver for scroll events
const scrollEventConfig = {
listener: this._onScroll,
useNativeDriver: true,
};
this._scrollPos = new Animated.Value(0);
const argMapping = props.vertical
? [{ nativeEvent: { contentOffset: { y: this._scrollPos } } }]
: [{ nativeEvent: { contentOffset: { x: this._scrollPos } } }];
if (props.onScroll && Array.isArray(props.onScroll._argMapping)) {
// Because of a react-native issue https://github.com/facebook/react-native/issues/13294
argMapping.pop();
const [ argMap ] = props.onScroll._argMapping;
if (argMap && argMap.nativeEvent && argMap.nativeEvent.contentOffset) {
// Shares the same animated value passed in props
this._scrollPos =
argMap.nativeEvent.contentOffset.x ||
argMap.nativeEvent.contentOffset.y ||
this._scrollPos;
}
argMapping.push(...props.onScroll._argMapping);
}
this._onScrollHandler = Animated.event(
argMapping,
scrollEventConfig
);
}
_needsScrollView () {
const { useScrollView } = this.props;
return useScrollView || !AnimatedFlatList || this._shouldUseStackLayout() || this._shouldUseTinderLayout();
}
_needsRTLAdaptations () {
const { vertical } = this.props;
return IS_RTL && !IS_IOS && !vertical;
}
_canLockScroll () {
const { scrollEnabled, enableMomentum, lockScrollWhileSnapping } = this.props;
return scrollEnabled && !enableMomentum && lockScrollWhileSnapping;
}
_enableLoop () {
const { data, enableSnap, loop } = this.props;
return enableSnap && loop && data && data.length && data.length > 1;
}
_shouldAnimateSlides (props = this.props) {
const { inactiveSlideOpacity, inactiveSlideScale, scrollInterpolator, slideInterpolatedStyle } = props;
return inactiveSlideOpacity < 1 ||
inactiveSlideScale < 1 ||
!!scrollInterpolator ||
!!slideInterpolatedStyle ||
this._shouldUseShiftLayout() ||
this._shouldUseStackLayout() ||
this._shouldUseTinderLayout();
}
_shouldUseCustomAnimation () {
const { activeAnimationOptions } = this.props;
return !!activeAnimationOptions && !this._shouldUseStackLayout() && !this._shouldUseTinderLayout();
}
_shouldUseShiftLayout () {
const { inactiveSlideShift, layout } = this.props;
return layout === 'default' && inactiveSlideShift !== 0;
}
_shouldUseStackLayout () {
return this.props.layout === 'stack';
}
_shouldUseTinderLayout () {
return this.props.layout === 'tinder';
}
_getCustomData (props = this.props) {
const { data, loopClonesPerSide } = props;
const dataLength = data && data.length;
if (!dataLength) {
return [];
}
if (!this._enableLoop()) {
return data;
}
let previousItems = [];
let nextItems = [];
if (loopClonesPerSide > dataLength) {
const dataMultiplier = Math.floor(loopClonesPerSide / dataLength);
const remainder = loopClonesPerSide % dataLength;
for (let i = 0; i < dataMultiplier; i++) {
previousItems.push(...data);
nextItems.push(...data);
}
previousItems.unshift(...data.slice(-remainder));
nextItems.push(...data.slice(0, remainder));
} else {
previousItems = data.slice(-loopClonesPerSide);
nextItems = data.slice(0, loopClonesPerSide);
}
return previousItems.concat(data, nextItems);
}
_getCustomDataLength (props = this.props) {
const { data, loopClonesPerSide } = props;
const dataLength = data && data.length;
if (!dataLength) {
return 0;
}
return this._enableLoop() ? dataLength + (2 * loopClonesPerSide) : dataLength;
}
_getCustomIndex (index, props = this.props) {
const itemsLength = this._getCustomDataLength(props);
if (!itemsLength || (!index && index !== 0)) {
return 0;
}
return this._needsRTLAdaptations() ? itemsLength - index - 1 : index;
}
_getDataIndex (index) {
const { data, loopClonesPerSide } = this.props;
const dataLength = data && data.length;
if (!this._enableLoop() || !dataLength) {
return index;
}
if (index >= dataLength + loopClonesPerSide) {
return loopClonesPerSide > dataLength ?
(index - loopClonesPerSide) % dataLength :
index - dataLength - loopClonesPerSide;
} else if (index < loopClonesPerSide) {
// TODO: is there a simpler way of determining the interpolated index?
if (loopClonesPerSide > dataLength) {
const baseDataIndexes = [];
const dataIndexes = [];
const dataMultiplier = Math.floor(loopClonesPerSide / dataLength);
const remainder = loopClonesPerSide % dataLength;
for (let i = 0; i < dataLength; i++) {
baseDataIndexes.push(i);
}
for (let j = 0; j < dataMultiplier; j++) {
dataIndexes.push(...baseDataIndexes);
}
dataIndexes.unshift(...baseDataIndexes.slice(-remainder));
return dataIndexes[index];
} else {
return index + dataLength - loopClonesPerSide;
}
} else {
return index - loopClonesPerSide;
}
}
// Used with `snapToItem()` and 'PaginationDot'
_getPositionIndex (index) {
const { loop, loopClonesPerSide } = this.props;
return loop ? index + loopClonesPerSide : index;
}
_getFirstItem (index, props = this.props) {
const { loopClonesPerSide } = props;
const itemsLength = this._getCustomDataLength(props);
if (!itemsLength || index > itemsLength - 1 || index < 0) {
return 0;
}
return this._enableLoop() ? index + loopClonesPerSide : index;
}
_getWrappedRef () {
if (this._carouselRef && (
(this._needsScrollView() && this._carouselRef.scrollTo) ||
(!this._needsScrollView() && this._carouselRef.scrollToOffset)
)) {
return this._carouselRef;
}
// https://github.com/facebook/react-native/issues/10635
// https://stackoverflow.com/a/48786374/8412141
return this._carouselRef && this._carouselRef.getNode && this._carouselRef.getNode();
}
_getScrollEnabled () {
return this._scrollEnabled;
}
_setScrollEnabled (scrollEnabled = true) {
const wrappedRef = this._getWrappedRef();
if (!wrappedRef || !wrappedRef.setNativeProps) {
return;
}
// 'setNativeProps()' is used instead of 'setState()' because the latter
// really takes a toll on Android behavior when momentum is disabled
wrappedRef.setNativeProps({ scrollEnabled });
this._scrollEnabled = scrollEnabled;
}
_getKeyExtractor (item, index) {
return this._needsScrollView() ? `scrollview-item-${index}` : `flatlist-item-${index}`;
}
_getScrollOffset (event) {
const { vertical } = this.props;
return (event && event.nativeEvent && event.nativeEvent.contentOffset &&
event.nativeEvent.contentOffset[vertical ? 'y' : 'x']) || 0;
}
_getContainerInnerMargin (opposite = false) {
const { sliderWidth, sliderHeight, itemWidth, itemHeight, vertical, activeSlideAlignment } = this.props;
if ((activeSlideAlignment === 'start' && !opposite) ||
(activeSlideAlignment === 'end' && opposite)) {
return 0;
} else if ((activeSlideAlignment === 'end' && !opposite) ||
(activeSlideAlignment === 'start' && opposite)) {
return vertical ? sliderHeight - itemHeight : sliderWidth - itemWidth;
} else {
return vertical ? (sliderHeight - itemHeight) / 2 : (sliderWidth - itemWidth) / 2;
}
}
_getViewportOffset () {
const { sliderWidth, sliderHeight, itemWidth, itemHeight, vertical, activeSlideAlignment } = this.props;
if (activeSlideAlignment === 'start') {
return vertical ? itemHeight / 2 : itemWidth / 2;
} else if (activeSlideAlignment === 'end') {
return vertical ?
sliderHeight - (itemHeight / 2) :
sliderWidth - (itemWidth / 2);
} else {
return vertical ? sliderHeight / 2 : sliderWidth / 2;
}
}
_getCenter (offset) {
return offset + this._getViewportOffset() - this._getContainerInnerMargin();
}
_getActiveItem (offset) {
const { activeSlideOffset, swipeThreshold } = this.props;
const center = this._getCenter(offset);
const centerOffset = activeSlideOffset || swipeThreshold;
for (let i = 0; i < this._positions.length; i++) {
const { start, end } = this._positions[i];
if (center + centerOffset >= start && center - centerOffset <= end) {
return i;
}
}
const lastIndex = this._positions.length - 1;
if (this._positions[lastIndex] && center - centerOffset > this._positions[lastIndex].end) {
return lastIndex;
}
return 0;
}
_initPositionsAndInterpolators (props = this.props) {
const { data, itemWidth, itemHeight, scrollInterpolator, vertical } = props;
const sizeRef = vertical ? itemHeight : itemWidth;
if (!data || !data.length) {
return;
}
let interpolators = [];
this._positions = [];
this._getCustomData(props).forEach((itemData, index) => {
const _index = this._getCustomIndex(index, props);
let animatedValue;
this._positions[index] = {
start: index * sizeRef,
end: index * sizeRef + sizeRef
};
if (!this._shouldAnimateSlides(props)) {
animatedValue = new Animated.Value(1);
} else if (this._shouldUseCustomAnimation()) {
animatedValue = new Animated.Value(_index === this._activeItem ? 1 : 0);
} else {
let interpolator;
if (scrollInterpolator) {
interpolator = scrollInterpolator(_index, props);
} else if (this._shouldUseStackLayout()) {
interpolator = stackScrollInterpolator(_index, props);
} else if (this._shouldUseTinderLayout()) {
interpolator = tinderScrollInterpolator(_index, props);
}
if (!interpolator || !interpolator.inputRange || !interpolator.outputRange) {
interpolator = defaultScrollInterpolator(_index, props);
}
animatedValue = this._scrollPos.interpolate({
...interpolator,
extrapolate: 'clamp'
});
}
interpolators.push(animatedValue);
});
this.setState({ interpolators });
}
_getSlideAnimation (index, toValue) {
const { interpolators } = this.state;
const { activeAnimationType, activeAnimationOptions } = this.props;
const animatedValue = interpolators && interpolators[index];
if (!animatedValue && animatedValue !== 0) {
return null;
}
const animationCommonOptions = {
isInteraction: false,
useNativeDriver: true,
...activeAnimationOptions,
toValue: toValue
};
return Animated.parallel([
Animated['timing'](
animatedValue,
{ ...animationCommonOptions, easing: Easing.linear }
),
Animated[activeAnimationType](
animatedValue,
{ ...animationCommonOptions }
)
]);
}
_playCustomSlideAnimation (current, next) {
const { interpolators } = this.state;
const itemsLength = this._getCustomDataLength();
const _currentIndex = this._getCustomIndex(current);
const _currentDataIndex = this._getDataIndex(_currentIndex);
const _nextIndex = this._getCustomIndex(next);
const _nextDataIndex = this._getDataIndex(_nextIndex);
let animations = [];
// Keep animations in sync when looping
if (this._enableLoop()) {
for (let i = 0; i < itemsLength; i++) {
if (this._getDataIndex(i) === _currentDataIndex && interpolators[i]) {
animations.push(this._getSlideAnimation(i, 0));
} else if (this._getDataIndex(i) === _nextDataIndex && interpolators[i]) {
animations.push(this._getSlideAnimation(i, 1));
}
}
} else {
if (interpolators[current]) {
animations.push(this._getSlideAnimation(current, 0));
}
if (interpolators[next]) {
animations.push(this._getSlideAnimation(next, 1));
}
}
Animated.parallel(animations, { stopTogether: false }).start();
}
_hackActiveSlideAnimation (index, goTo, force = false) {
const { data } = this.props;
if (!this._mounted || !this._carouselRef || !this._positions[index] || (!force && this._enableLoop())) {
return;
}
const offset = this._positions[index] && this._positions[index].start;
if (!offset && offset !== 0) {
return;
}
const itemsLength = data && data.length;
const direction = goTo || itemsLength === 1 ? 'start' : 'end';
this._scrollTo(offset + (direction === 'start' ? -1 : 1), false);
clearTimeout(this._hackSlideAnimationTimeout);
this._hackSlideAnimationTimeout = setTimeout(() => {
this._scrollTo(offset, false);
}, 50); // works randomly when set to '0'
}
_lockScroll () {
const { lockScrollTimeoutDuration } = this.props;
clearTimeout(this._lockScrollTimeout);
this._lockScrollTimeout = setTimeout(() => {
this._releaseScroll();
}, lockScrollTimeoutDuration);
this._setScrollEnabled(false);
}
_releaseScroll () {
clearTimeout(this._lockScrollTimeout);
this._setScrollEnabled(true);
}
_repositionScroll (index) {
const { data, loopClonesPerSide } = this.props;
const dataLength = data && data.length;
if (!this._enableLoop() || !dataLength ||
(index >= loopClonesPerSide && index < dataLength + loopClonesPerSide)) {
return;
}
let repositionTo = index;
if (index >= dataLength + loopClonesPerSide) {
repositionTo = index - dataLength;
} else if (index < loopClonesPerSide) {
repositionTo = index + dataLength;
}
this._snapToItem(repositionTo, false, false, false, false);
}
_scrollTo (offset, animated = true) {
const { vertical } = this.props;
const wrappedRef = this._getWrappedRef();
if (!this._mounted || !wrappedRef) {
return;
}
const specificOptions = this._needsScrollView() ? {
x: vertical ? 0 : offset,
y: vertical ? offset : 0
} : {
offset
};
const options = {
...specificOptions,
animated
};
if (this._needsScrollView()) {
wrappedRef.scrollTo(options);
} else {
wrappedRef.scrollToOffset(options);
}
}
_onScroll (event) {
const { callbackOffsetMargin, enableMomentum, onScroll } = this.props;
const scrollOffset = event ? this._getScrollOffset(event) : this._currentContentOffset;
const nextActiveItem = this._getActiveItem(scrollOffset);
const itemReached = nextActiveItem === this._itemToSnapTo;
const scrollConditions =
scrollOffset >= this._scrollOffsetRef - callbackOffsetMargin &&
scrollOffset <= this._scrollOffsetRef + callbackOffsetMargin;
this._currentContentOffset = scrollOffset;
this._onScrollTriggered = true;
this._lastScrollDate = Date.now();
if (this._activeItem !== nextActiveItem && this._shouldUseCustomAnimation()) {
this._playCustomSlideAnimation(this._activeItem, nextActiveItem);
}
if (enableMomentum) {
clearTimeout(this._snapNoMomentumTimeout);
if (this._activeItem !== nextActiveItem) {
this._activeItem = nextActiveItem;
}
if (itemReached) {
if (this._canFireBeforeCallback) {
this._onBeforeSnap(this._getDataIndex(nextActiveItem));
}
if (scrollConditions && this._canFireCallback) {
this._onSnap(this._getDataIndex(nextActiveItem));
}
}
} else if (this._activeItem !== nextActiveItem && itemReached) {
if (this._canFireBeforeCallback) {
this._onBeforeSnap(this._getDataIndex(nextActiveItem));
}
if (scrollConditions) {
this._activeItem = nextActiveItem;
if (this._canLockScroll()) {
this._releaseScroll();
}
if (this._canFireCallback) {
this._onSnap(this._getDataIndex(nextActiveItem));
}
}
}
if (nextActiveItem === this._itemToSnapTo &&
scrollOffset === this._scrollOffsetRef) {
this._repositionScroll(nextActiveItem);
}
if (typeof onScroll === "function" && event) {
onScroll(event);
}
}
_onStartShouldSetResponderCapture (event) {
const { onStartShouldSetResponderCapture } = this.props;
if (onStartShouldSetResponderCapture) {
onStartShouldSetResponderCapture(event);
}
return this._getScrollEnabled();
}
_onTouchStart () {
const { onTouchStart } = this.props
// `onTouchStart` is fired even when `scrollEnabled` is set to `false`
if (this._getScrollEnabled() !== false && this._autoplaying) {
this.pauseAutoPlay();
}
if (onTouchStart) {
onTouchStart()
}
}
_onTouchEnd () {
const { onTouchEnd } = this.props
if (this._getScrollEnabled() !== false && this._autoplay && !this._autoplaying) {
// This event is buggy on Android, so a fallback is provided in _onScrollEnd()
this.startAutoplay();
}
if (onTouchEnd) {
onTouchEnd()
}
}
// Used when `enableSnap` is ENABLED
_onScrollBeginDrag (event) {
const { onScrollBeginDrag } = this.props;
if (!this._getScrollEnabled()) {
return;
}
this._scrollStartOffset = this._getScrollOffset(event);
this._scrollStartActive = this._getActiveItem(this._scrollStartOffset);
this._ignoreNextMomentum = false;
// this._canFireCallback = false;
if (onScrollBeginDrag) {
onScrollBeginDrag(event);
}
}
// Used when `enableMomentum` is DISABLED
_onScrollEndDrag (event) {
const { onScrollEndDrag } = this.props;
if (this._carouselRef) {
this._onScrollEnd && this._onScrollEnd();
}
if (onScrollEndDrag) {
onScrollEndDrag(event);
}
}
// Used when `enableMomentum` is ENABLED
_onMomentumScrollEnd (event) {
const { onMomentumScrollEnd } = this.props;
if (this._carouselRef) {
this._onScrollEnd && this._onScrollEnd();
}
if (onMomentumScrollEnd) {
onMomentumScrollEnd(event);
}
}
_onScrollEnd (event) {
const { autoplayDelay, enableSnap } = this.props;
if (this._ignoreNextMomentum) {
// iOS fix
this._ignoreNextMomentum = false;
return;
}
if (this._currentContentOffset === this._scrollEndOffset) {
return;
}
this._scrollEndOffset = this._currentContentOffset;
this._scrollEndActive = this._getActiveItem(this._scrollEndOffset);
if (enableSnap) {
this._snapScroll(this._scrollEndOffset - this._scrollStartOffset);
}
// The touchEnd event is buggy on Android, so this will serve as a fallback whenever needed
// https://github.com/facebook/react-native/issues/9439
if (this._autoplay && !this._autoplaying) {
clearTimeout(this._enableAutoplayTimeout);
this._enableAutoplayTimeout = setTimeout(() => {
this.startAutoplay();
}, autoplayDelay + 50);
}
}
// Due to a bug, this event is only fired on iOS
// https://github.com/facebook/react-native/issues/6791
// it's fine since we're only fixing an iOS bug in it, so ...
_onTouchRelease (event) {
const { enableMomentum } = this.props;
if (enableMomentum && IS_IOS) {
clearTimeout(this._snapNoMomentumTimeout);
this._snapNoMomentumTimeout = setTimeout(() => {
this._snapToItem(this._activeItem);
}, 100);
}
}
_onLayout (event) {
const { onLayout } = this.props;
// Prevent unneeded actions during the first 'onLayout' (triggered on init)
if (this._onLayoutInitDone) {
this._initPositionsAndInterpolators();
this._snapToItem(this._activeItem, false, false, false, false);
} else {
this._onLayoutInitDone = true;
}
if (onLayout) {
onLayout(event);
}
}
_snapScroll (delta) {
const { swipeThreshold } = this.props;
// When using momentum and releasing the touch with
// no velocity, scrollEndActive will be undefined (iOS)
if (!this._scrollEndActive && this._scrollEndActive !== 0 && IS_IOS) {
this._scrollEndActive = this._scrollStartActive;
}
if (this._scrollStartActive !== this._scrollEndActive) {
// Snap to the new active item
this._snapToItem(this._scrollEndActive);
} else {
// Snap depending on delta
if (delta > 0) {
if (delta > swipeThreshold) {
this._snapToItem(this._scrollStartActive + 1);
} else {
this._snapToItem(this._scrollEndActive);
}
} else if (delta < 0) {
if (delta < -swipeThreshold) {
this._snapToItem(this._scrollStartActive - 1);
} else {
this._snapToItem(this._scrollEndActive);
}
} else {
// Snap to current
this._snapToItem(this._scrollEndActive);
}
}
}
_snapToItem (index, animated = true, fireCallback = true, initial = false, lockScroll = true) {
const { enableMomentum, onSnapToItem, onBeforeSnapToItem } = this.props;
const itemsLength = this._getCustomDataLength();
const wrappedRef = this._getWrappedRef();
if (!itemsLength || !wrappedRef) {
return;
}
if (!index || index < 0) {
index = 0;
} else if (itemsLength > 0 && index >= itemsLength) {
index = itemsLength - 1;
}
if (index !== this._previousActiveItem) {
this._previousActiveItem = index;
// Placed here to allow overscrolling for edges items
if (lockScroll && this._canLockScroll()) {
this._lockScroll();
}
if (fireCallback) {
if (onBeforeSnapToItem) {
this._canFireBeforeCallback = true;
}
if (onSnapToItem) {
this._canFireCallback = true;
}
}
}
this._itemToSnapTo = index;
this._scrollOffsetRef = this._positions[index] && this._positions[index].start;
this._onScrollTriggered = false;
if (!this._scrollOffsetRef && this._scrollOffsetRef !== 0) {
return;
}
this._scrollTo(this._scrollOffsetRef, animated);
this._scrollEndOffset = this._currentContentOffset;
if (enableMomentum) {
// iOS fix, check the note in the constructor
if (!initial) {
this._ignoreNextMomentum = true;
}
// When momentum is enabled and the user is overscrolling or swiping very quickly,
// 'onScroll' is not going to be triggered for edge items. Then callback won't be
// fired and loop won't work since the scrollview is not going to be repositioned.
// As a workaround, '_onScroll()' will be called manually for these items if a given
// condition hasn't been met after a small delay.
// WARNING: this is ok only when relying on 'momentumScrollEnd', not with 'scrollEndDrag'
if (index === 0 || index === itemsLength - 1) {
clearTimeout(this._edgeItemTimeout);
this._edgeItemTimeout = setTimeout(() => {
if (!initial && index === this._activeItem && !this._onScrollTriggered) {
this._onScroll();
}
}, 250);
}
}
}
_onBeforeSnap (index) {
const { onBeforeSnapToItem } = this.props;
if (!this._carouselRef) {
return;
}
this._canFireBeforeCallback = false;
onBeforeSnapToItem && onBeforeSnapToItem(index);
}
_onSnap (index) {
const { onSnapToItem } = this.props;
if (!this._carouselRef) {
return;
}
this._canFireCallback = false;
onSnapToItem && onSnapToItem(index);
}
startAutoplay () {
const { autoplayInterval, autoplayDelay } = this.props;
this._autoplay = true;
if (this._autoplaying) {
return;
}
clearTimeout(this._autoplayTimeout);
this._autoplayTimeout = setTimeout(() => {
this._autoplaying = true;
this._autoplayInterval = setInterval(() => {
if (this._autoplaying) {
this.snapToNext();
}
}, autoplayInterval);
}, autoplayDelay);
}
pauseAutoPlay () {
this._autoplaying = false;
clearTimeout(this._autoplayTimeout);
clearTimeout(this._enableAutoplayTimeout);
clearInterval(this._autoplayInterval);
}
stopAutoplay () {
this._autoplay = false;
this.pauseAutoPlay();
}
snapToItem (index, animated = true, fireCallback = true) {
if (!index || index < 0) {
index = 0;
}
const positionIndex = this._getPositionIndex(index);
if (positionIndex === this._activeItem) {
return;
}
this._snapToItem(positionIndex, animated, fireCallback);
}
snapToNext (animated = true, fireCallback = true) {
const itemsLength = this._getCustomDataLength();
let newIndex = this._activeItem + 1;
if (newIndex > itemsLength - 1) {
if (!this._enableLoop()) {
return;
}
newIndex = 0;
}
this._snapToItem(newIndex, animated, fireCallback);
}
snapToPrev (animated = true, fireCallback = true) {
const itemsLength = this._getCustomDataLength();
let newIndex = this._activeItem - 1;
if (newIndex < 0) {
if (!this._enableLoop()) {
return;
}
newIndex = itemsLength - 1;
}
this._snapToItem(newIndex, animated, fireCallback);
}
// https://github.com/facebook/react-native/issues/1831#issuecomment-231069668
triggerRenderingHack (offset) {
// Avoid messing with user scroll
if (Date.now() - this._lastScrollDate < 500) {
return;
}
const scrollPosition = this._currentContentOffset;
if (!scrollPosition && scrollPosition !== 0) {
return;
}
const scrollOffset = offset || (scrollPosition === 0 ? 1 : -1);
this._scrollTo(scrollPosition + scrollOffset, false);
}
_getSlideInterpolatedStyle (index, animatedValue) {
const { layoutCardOffset, slideInterpolatedStyle } = this.props;
if (slideInterpolatedStyle) {
return slideInterpolatedStyle(index, animatedValue, this.props);
} else if (this._shouldUseTinderLayout()) {
return tinderAnimatedStyles(index, animatedValue, this.props, layoutCardOffset);
} else if (this._shouldUseStackLayout()) {
return stackAnimatedStyles(index, animatedValue, this.props, layoutCardOffset);
} else if (this._shouldUseShiftLayout()) {
return shiftAnimatedStyles(index, animatedValue, this.props);
} else {
return defaultAnimatedStyles(index, animatedValue, this.props);
}
}
_renderItem ({ item, index }) {
const { interpolators } = this.state;
const {
hasParallaxImages,
itemWidth,
itemHeight,
keyExtractor,
renderItem,
sliderHeight,
sliderWidth,
slideStyle,
vertical
} = this.props;
const animatedValue = interpolators && interpolators[index];
if (!animatedValue && animatedValue !== 0) {
return null;
}
const animate = this._shouldAnimateSlides();
const Component = animate ? Animated.View : View;
const animatedStyle = animate ? this._getSlideInterpolatedStyle(index, animatedValue) : {};
const parallaxProps = hasParallaxImages ? {
scrollPosition: this._scrollPos,
carouselRef: this._carouselRef,
vertical,
sliderWidth,
sliderHeight,
itemWidth,
itemHeight
} : undefined;
const mainDimension = vertical ? { height: itemHeight } : { width: itemWidth };
const specificProps = this._needsScrollView() ? {
key: keyExtractor ? keyExtractor(item, index) : this._getKeyExtractor(item, index)
} : {};
return (
<Component style={[mainDimension, slideStyle, animatedStyle]} pointerEvents={'box-none'} {...specificProps}>
{ renderItem({ item, index }, parallaxProps) }
</Component>
);
}
_getComponentOverridableProps () {
const {
enableMomentum,
itemWidth,
itemHeight,
loopClonesPerSide,
sliderWidth,
sliderHeight,
vertical
} = this.props;
const visibleItems = Math.ceil(vertical ?
sliderHeight / itemHeight :
sliderWidth / itemWidth) + 1;
const initialNumPerSide = this._enableLoop() ? loopClonesPerSide : 2;
const initialNumToRender = visibleItems + (initialNumPerSide * 2);
const maxToRenderPerBatch = 1 + (initialNumToRender * 2);
const windowSize = maxToRenderPerBatch;
const specificProps = !this._needsScrollView() ? {
initialNumToRender: initialNumToRender,
maxToRenderPerBatch: maxToRenderPerBatch,
windowSize: windowSize
// updateCellsBatchingPeriod
} : {};
return {
decelerationRate: enableMomentum ? 0.9 : 'fast',
showsHorizontalScrollIndicator: false,
showsVerticalScrollIndicator: false,
overScrollMode: 'never',
automaticallyAdjustContentInsets: false,
directionalLockEnabled: true,
pinchGestureEnabled: false,
scrollsToTop: false,
removeClippedSubviews: !this._needsScrollView(),
inverted: this._needsRTLAdaptations(),
// renderToHardwareTextureAndroid: true,
...specificProps
};
}
_getComponentStaticProps () {
const { hideCarousel } = this.state;
const {
containerCustomStyle,
contentContainerCustomStyle,
keyExtractor,
sliderWidth,
sliderHeight,
style,
vertical
} = this.props;
const containerStyle = [
containerCustomStyle || style || {},
hideCarousel ? { opacity: 0 } : {},
vertical ?
{ height: sliderHeight, flexDirection: 'column' } :
// LTR hack; see https://github.com/facebook/react-native/issues/11960
// and https://github.com/facebook/react-native/issues/13100#issuecomment-328986423
{ width: sliderWidth, flexDirection: this._needsRTLAdaptations() ? 'row-reverse' : 'row' }
];
const contentContainerStyle = [
vertical ? {
paddingTop: this._getContainerInnerMargin(),
paddingBottom: this._getContainerInnerMargin(true)
} : {
paddingLeft: this._getContainerInnerMargin(),
paddingRight: this._getContainerInnerMargin(true)
},
contentContainerCustomStyle || {}
];
const specificProps = !this._needsScrollView() ? {
// extraData: this.state,
renderItem: this._renderItem,
numColumns: 1,
keyExtractor: keyExtractor || this._getKeyExtractor
} : {};
return {
ref: c => this._carouselRef = c,
data: this._getCustomData(),
style: containerStyle,
contentContainerStyle: contentContainerStyle,
horizontal: !vertical,
scrollEventThrottle: 1,
onScroll: this._onScrollHandler,
onScrollBeginDrag: this._onScrollBeginDrag,
onScrollEndDrag: this._onScrollEndDrag,
onMomentumScrollEnd: this._onMomentumScrollEnd,
onResponderRelease: this._onTouchRelease,
onStartShouldSetResponderCapture: this._onStartShouldSetResponderCapture,
onTouchStart: this._onTouchStart,
onTouchEnd: this._onScrollEnd,
onLayout: this._onLayout,
...specificProps
};
}
render () {
const { data, renderItem, useScrollView } = this.props;
if (!data || !renderItem) {
return null;
}
const props = {
...this._getComponentOverridableProps(),
...this.props,
...this._getComponentStaticProps()
};
const ScrollViewComponent = typeof useScrollView === 'function' ? useScrollView : AnimatedScrollView
return this._needsScrollView() ? (
<ScrollViewComponent {...props}>
{
this._getCustomData().map((item, index) => {
return this._renderItem({ item, index });
})
}
</ScrollViewComponent>
) : (
<AnimatedFlatList {...props} />
);
}
}