diff --git a/package.json b/package.json index 7304e37ce..66b573afc 100644 --- a/package.json +++ b/package.json @@ -1,63 +1,64 @@ { - "name": "react-native-snap-carousel", - "version": "3.9.1", - "description": "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.", - "main": "src/index.js", - "repository": { - "type": "git", - "url": "github.com/meliorence/react-native-snap-carousel" - }, - "keywords": [ - "react", - "native", - "carousel", - "slider", - "swiper", - "flatlist", - "scrollview", - "parallax", - "images", - "infinite", - "scroll", - "scrolling", - "items", - "edge", - "snap", - "card", - "cards", - "stack", - "deck", - "tinder", - "android", - "ios", - "snapping", - "component", - "rtl" - ], - "author": "Meliorence (github.com/meliorence)", - "license": "BSD-3-Clause", - "dependencies": { - "prop-types": "^15.6.1", - "react-addons-shallow-compare": "15.6.2" - }, - "peerDependencies": { - "react": ">=15.0.0", - "react-native": "*" - }, - "devDependencies": { - "babel-eslint": "^8.2.2", - "eslint": "^4.19.1", - "eslint-config-standard": "^10.2.1", - "eslint-config-standard-react": "^5.0.0", - "eslint-plugin-import": "^2.11.0", - "eslint-plugin-node": "^5.2.1", - "eslint-plugin-promise": "^3.7.0", - "eslint-plugin-react": "^7.7.0", - "eslint-plugin-standard": "^3.0.1" - }, - "homepage": "https://github.com/meliorence/react-native-snap-carousel", - "bugs": { - "url": "https://github.com/meliorence/react-native-snap-carousel/issues" - }, - "readmeFilename": "README.md" + "name": "react-native-snap-carousel", + "version": "3.9.1", + "description": "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.", + "main": "src/index.js", + "repository": { + "type": "git", + "url": "github.com/meliorence/react-native-snap-carousel" + }, + "keywords": [ + "react", + "native", + "carousel", + "slider", + "swiper", + "flatlist", + "scrollview", + "parallax", + "images", + "infinite", + "scroll", + "scrolling", + "items", + "edge", + "snap", + "card", + "cards", + "stack", + "deck", + "tinder", + "android", + "ios", + "snapping", + "component", + "rtl" + ], + "author": "Meliorence (github.com/meliorence)", + "license": "BSD-3-Clause", + "dependencies": { + "deprecated-react-native-prop-types": "^2.3.0", + "prop-types": "^15.6.1", + "react-addons-shallow-compare": "15.6.2" + }, + "peerDependencies": { + "react": ">=15.0.0", + "react-native": "*" + }, + "devDependencies": { + "babel-eslint": "^8.2.2", + "eslint": "^4.19.1", + "eslint-config-standard": "^10.2.1", + "eslint-config-standard-react": "^5.0.0", + "eslint-plugin-import": "^2.11.0", + "eslint-plugin-node": "^5.2.1", + "eslint-plugin-promise": "^3.7.0", + "eslint-plugin-react": "^7.7.0", + "eslint-plugin-standard": "^3.0.1" + }, + "homepage": "https://github.com/meliorence/react-native-snap-carousel", + "bugs": { + "url": "https://github.com/meliorence/react-native-snap-carousel/issues" + }, + "readmeFilename": "README.md" } diff --git a/src/carousel/Carousel.js b/src/carousel/Carousel.js index dae71a3da..95276e190 100644 --- a/src/carousel/Carousel.js +++ b/src/carousel/Carousel.js @@ -1,22 +1,33 @@ -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 React, { Component } from "react"; import { - defaultScrollInterpolator, - stackScrollInterpolator, - tinderScrollInterpolator, - defaultAnimatedStyles, - shiftAnimatedStyles, - stackAnimatedStyles, - tinderAnimatedStyles -} from '../utils/animations'; + Animated, + Easing, + FlatList, + I18nManager, + Platform, + ScrollView, + View, +} from "react-native"; +import { ViewPropTypes } from "deprecated-react-native-prop-types"; +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'; +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 AnimatedFlatList = FlatList + ? Animated.createAnimatedComponent(FlatList) + : null; const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView); // React Native automatically handles RTL layouts; unfortunately, it's buggy with horizontal ScrollView @@ -26,1346 +37,1516 @@ const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView); 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 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: [], }; - 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'); - } + // 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" + ); } - - 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(); - } - }); + if (!props.vertical && (!props.sliderWidth || !props.itemWidth)) { + console.error( + "react-native-snap-carousel: You need to specify both `sliderWidth` and `itemWidth` for horizontal carousels" + ); } - - shouldComponentUpdate (nextProps, nextState) { - if (this.props.shouldOptimizeUpdates === false) { - return true; - } else { - return shallowCompare(this, nextProps, nextState); - } + 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" + ); + } + } - componentDidUpdate (prevProps) { - const { interpolators } = this.state; - const { firstItem, itemHeight, itemWidth, scrollEnabled, sliderHeight, sliderWidth } = this.props; - const itemsLength = this._getCustomDataLength(this.props); - - if (!itemsLength) { - return; - } + componentDidMount() { + const { apparitionDelay, autoplay, firstItem } = this.props; + const _firstItem = this._getFirstItem(firstItem); + const apparitionCallback = () => { + this.setState({ hideCarousel: false }); + if (autoplay) { + this.startAutoplay(); + } + }; - const nextFirstItem = this._getFirstItem(firstItem, this.props); - let nextActiveItem = this._activeItem || this._activeItem === 0 ? this._activeItem : nextFirstItem; + this._mounted = true; + this._initPositionsAndInterpolators(); - 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; + // Without 'requestAnimationFrame' or a `0` timeout, images will randomly not be rendered on Android... + requestAnimationFrame(() => { + if (!this._mounted) { + return; + } - // Prevent issues with dynamically removed items - if (nextActiveItem > itemsLength - 1) { - nextActiveItem = itemsLength - 1; - } + this._snapToItem(_firstItem, false, false, true, false); + this._hackActiveSlideAnimation(_firstItem, "start", true); - // Handle changing scrollEnabled independent of user -> carousel interaction - if (hasNewScrollEnabled) { - this._setScrollEnabled(scrollEnabled); - } + 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 (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 ( + 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); + } - if (this.props.onScroll !== prevProps.onScroll) { - this._setScrollHandler(this.props); - } + previousItems.unshift(...data.slice(-remainder)); + nextItems.push(...data.slice(0, remainder)); + } else { + previousItems = data.slice(-loopClonesPerSide); + nextItems = data.slice(0, loopClonesPerSide); } - 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); - } + return previousItems.concat(data, nextItems); + } - get realIndex () { - return this._activeItem; - } + _getCustomDataLength(props = this.props) { + const { data, loopClonesPerSide } = props; + const dataLength = data && data.length; - get currentIndex () { - return this._getDataIndex(this._activeItem); + if (!dataLength) { + return 0; } - get currentScrollPosition () { - return this._currentContentOffset; - } + return this._enableLoop() ? dataLength + 2 * loopClonesPerSide : dataLength; + } - _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 - ); - } + _getCustomIndex(index, props = this.props) { + const itemsLength = this._getCustomDataLength(props); - _needsScrollView () { - const { useScrollView } = this.props; - return useScrollView || !AnimatedFlatList || this._shouldUseStackLayout() || this._shouldUseTinderLayout(); + if (!itemsLength || (!index && index !== 0)) { + return 0; } - _needsRTLAdaptations () { - const { vertical } = this.props; - return IS_RTL && !IS_IOS && !vertical; - } + return this._needsRTLAdaptations() ? itemsLength - index - 1 : index; + } - _canLockScroll () { - const { scrollEnabled, enableMomentum, lockScrollWhileSnapping } = this.props; - return scrollEnabled && !enableMomentum && lockScrollWhileSnapping; - } + _getDataIndex(index) { + const { data, loopClonesPerSide } = this.props; + const dataLength = data && data.length; - _enableLoop () { - const { data, enableSnap, loop } = this.props; - return enableSnap && loop && data && data.length && data.length > 1; + if (!this._enableLoop() || !dataLength) { + return index; } - _shouldAnimateSlides (props = this.props) { - const { inactiveSlideOpacity, inactiveSlideScale, scrollInterpolator, slideInterpolatedStyle } = props; - return inactiveSlideOpacity < 1 || - inactiveSlideScale < 1 || - !!scrollInterpolator || - !!slideInterpolatedStyle || - this._shouldUseShiftLayout() || - this._shouldUseStackLayout() || - this._shouldUseTinderLayout(); - } + 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; - _shouldUseCustomAnimation () { - const { activeAnimationOptions } = this.props; - return !!activeAnimationOptions && !this._shouldUseStackLayout() && !this._shouldUseTinderLayout(); - } + for (let i = 0; i < dataLength; i++) { + baseDataIndexes.push(i); + } - _shouldUseShiftLayout () { - const { inactiveSlideShift, layout } = this.props; - return layout === 'default' && inactiveSlideShift !== 0; - } + for (let j = 0; j < dataMultiplier; j++) { + dataIndexes.push(...baseDataIndexes); + } - _shouldUseStackLayout () { - return this.props.layout === 'stack'; + 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; + } } - _shouldUseTinderLayout () { - return this.props.layout === 'tinder'; + const lastIndex = this._positions.length - 1; + if ( + this._positions[lastIndex] && + center - centerOffset > this._positions[lastIndex].end + ) { + return lastIndex; } - _getCustomData (props = this.props) { - const { data, loopClonesPerSide } = props; - const dataLength = data && data.length; + return 0; + } - if (!dataLength) { - return []; - } + _initPositionsAndInterpolators(props = this.props) { + const { data, itemWidth, itemHeight, scrollInterpolator, vertical } = props; + const sizeRef = vertical ? itemHeight : itemWidth; - if (!this._enableLoop()) { - return data; - } + if (!data || !data.length) { + return; + } - let previousItems = []; - let nextItems = []; + let interpolators = []; + this._positions = []; - if (loopClonesPerSide > dataLength) { - const dataMultiplier = Math.floor(loopClonesPerSide / dataLength); - const remainder = loopClonesPerSide % dataLength; + this._getCustomData(props).forEach((itemData, index) => { + const _index = this._getCustomIndex(index, props); + let animatedValue; - for (let i = 0; i < dataMultiplier; i++) { - previousItems.push(...data); - nextItems.push(...data); - } + this._positions[index] = { + start: index * sizeRef, + end: index * sizeRef + sizeRef, + }; - previousItems.unshift(...data.slice(-remainder)); - nextItems.push(...data.slice(0, remainder)); - } else { - previousItems = data.slice(-loopClonesPerSide); - nextItems = data.slice(0, loopClonesPerSide); + 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); } - return previousItems.concat(data, nextItems); - } + if ( + !interpolator || + !interpolator.inputRange || + !interpolator.outputRange + ) { + interpolator = defaultScrollInterpolator(_index, props); + } - _getCustomDataLength (props = this.props) { - const { data, loopClonesPerSide } = props; - const dataLength = data && data.length; + animatedValue = this._scrollPos.interpolate({ + ...interpolator, + extrapolate: "clamp", + }); + } - if (!dataLength) { - return 0; - } + interpolators.push(animatedValue); + }); - return this._enableLoop() ? dataLength + (2 * loopClonesPerSide) : dataLength; - } + this.setState({ interpolators }); + } - _getCustomIndex (index, props = this.props) { - const itemsLength = this._getCustomDataLength(props); + _getSlideAnimation(index, toValue) { + const { interpolators } = this.state; + const { activeAnimationType, activeAnimationOptions } = this.props; - if (!itemsLength || (!index && index !== 0)) { - return 0; - } + const animatedValue = interpolators && interpolators[index]; - return this._needsRTLAdaptations() ? itemsLength - index - 1 : index; + if (!animatedValue && animatedValue !== 0) { + return null; } - _getDataIndex (index) { - const { data, loopClonesPerSide } = this.props; - const dataLength = data && data.length; - - if (!this._enableLoop() || !dataLength) { - return index; - } + const animationCommonOptions = { + isInteraction: false, + useNativeDriver: true, + ...activeAnimationOptions, + toValue: toValue, + }; - 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; + 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)); + } } - // 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); + Animated.parallel(animations, { stopTogether: false }).start(); + } - if (!itemsLength || index > itemsLength - 1 || index < 0) { - return 0; - } + _hackActiveSlideAnimation(index, goTo, force = false) { + const { data } = this.props; - return this._enableLoop() ? index + loopClonesPerSide : index; + if ( + !this._mounted || + !this._carouselRef || + !this._positions[index] || + (!force && this._enableLoop()) + ) { + return; } - _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(); - } + const offset = this._positions[index] && this._positions[index].start; - _getScrollEnabled () { - return this._scrollEnabled; + if (!offset && offset !== 0) { + return; } - _setScrollEnabled (scrollEnabled = true) { - const wrappedRef = this._getWrappedRef(); + const itemsLength = data && data.length; + const direction = goTo || itemsLength === 1 ? "start" : "end"; - if (!wrappedRef || !wrappedRef.setNativeProps) { - return; - } + this._scrollTo(offset + (direction === "start" ? -1 : 1), false); - // '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; - } + clearTimeout(this._hackSlideAnimationTimeout); + this._hackSlideAnimationTimeout = setTimeout(() => { + this._scrollTo(offset, false); + }, 50); // works randomly when set to '0' + } - _getKeyExtractor (item, index) { - return this._needsScrollView() ? `scrollview-item-${index}` : `flatlist-item-${index}`; - } + _lockScroll() { + const { lockScrollTimeoutDuration } = this.props; + clearTimeout(this._lockScrollTimeout); + this._lockScrollTimeout = setTimeout(() => { + this._releaseScroll(); + }, lockScrollTimeoutDuration); + this._setScrollEnabled(false); + } - _getScrollOffset (event) { - const { vertical } = this.props; - return (event && event.nativeEvent && event.nativeEvent.contentOffset && - event.nativeEvent.contentOffset[vertical ? 'y' : 'x']) || 0; - } + _releaseScroll() { + clearTimeout(this._lockScrollTimeout); + this._setScrollEnabled(true); + } - _getContainerInnerMargin (opposite = false) { - const { sliderWidth, sliderHeight, itemWidth, itemHeight, vertical, activeSlideAlignment } = this.props; + _repositionScroll(index) { + const { data, loopClonesPerSide } = this.props; + const dataLength = data && data.length; - 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; - } + if ( + !this._enableLoop() || + !dataLength || + (index >= loopClonesPerSide && index < dataLength + loopClonesPerSide) + ) { + return; } - _getViewportOffset () { - const { sliderWidth, sliderHeight, itemWidth, itemHeight, vertical, activeSlideAlignment } = this.props; + let repositionTo = index; - 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; - } + if (index >= dataLength + loopClonesPerSide) { + repositionTo = index - dataLength; + } else if (index < loopClonesPerSide) { + repositionTo = index + dataLength; } - _getCenter (offset) { - return offset + this._getViewportOffset() - this._getContainerInnerMargin(); - } + this._snapToItem(repositionTo, false, false, false, false); + } - _getActiveItem (offset) { - const { activeSlideOffset, swipeThreshold } = this.props; - const center = this._getCenter(offset); - const centerOffset = activeSlideOffset || swipeThreshold; + _scrollTo(offset, animated = true) { + const { vertical } = this.props; + const wrappedRef = this._getWrappedRef(); - for (let i = 0; i < this._positions.length; i++) { - const { start, end } = this._positions[i]; - if (center + centerOffset >= start && center - centerOffset <= end) { - return i; - } - } + if (!this._mounted || !wrappedRef) { + return; + } - const lastIndex = this._positions.length - 1; - if (this._positions[lastIndex] && center - centerOffset > this._positions[lastIndex].end) { - return lastIndex; + const specificOptions = this._needsScrollView() + ? { + x: vertical ? 0 : offset, + y: vertical ? offset : 0, } + : { + offset, + }; + const options = { + ...specificOptions, + animated, + }; - return 0; + if (this._needsScrollView()) { + wrappedRef.scrollTo(options); + } else { + wrappedRef.scrollToOffset(options); } + } - _initPositionsAndInterpolators (props = this.props) { - const { data, itemWidth, itemHeight, scrollInterpolator, vertical } = props; - const sizeRef = vertical ? itemHeight : itemWidth; + _onScroll(event) { + const { callbackOffsetMargin, enableMomentum, onScroll } = this.props; - if (!data || !data.length) { - return; - } + 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; - 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._currentContentOffset = scrollOffset; + this._onScrollTriggered = true; + this._lastScrollDate = Date.now(); - this.setState({ interpolators }); + if ( + this._activeItem !== nextActiveItem && + this._shouldUseCustomAnimation() + ) { + this._playCustomSlideAnimation(this._activeItem, nextActiveItem); } - _getSlideAnimation (index, toValue) { - const { interpolators } = this.state; - const { activeAnimationType, activeAnimationOptions } = this.props; + if (enableMomentum) { + clearTimeout(this._snapNoMomentumTimeout); - const animatedValue = interpolators && interpolators[index]; + if (this._activeItem !== nextActiveItem) { + this._activeItem = nextActiveItem; + } - if (!animatedValue && animatedValue !== 0) { - return null; + if (itemReached) { + if (this._canFireBeforeCallback) { + this._onBeforeSnap(this._getDataIndex(nextActiveItem)); } - 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)); - } + if (scrollConditions && this._canFireCallback) { + this._onSnap(this._getDataIndex(nextActiveItem)); } + } + } else if (this._activeItem !== nextActiveItem && itemReached) { + if (this._canFireBeforeCallback) { + this._onBeforeSnap(this._getDataIndex(nextActiveItem)); + } - Animated.parallel(animations, { stopTogether: false }).start(); - } - - _hackActiveSlideAnimation (index, goTo, force = false) { - const { data } = this.props; + if (scrollConditions) { + this._activeItem = nextActiveItem; - if (!this._mounted || !this._carouselRef || !this._positions[index] || (!force && this._enableLoop())) { - return; + if (this._canLockScroll()) { + this._releaseScroll(); } - const offset = this._positions[index] && this._positions[index].start; - - if (!offset && offset !== 0) { - return; + if (this._canFireCallback) { + this._onSnap(this._getDataIndex(nextActiveItem)); } - - 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); + if ( + nextActiveItem === this._itemToSnapTo && + scrollOffset === this._scrollOffsetRef + ) { + this._repositionScroll(nextActiveItem); } - _releaseScroll () { - clearTimeout(this._lockScrollTimeout); - this._setScrollEnabled(true); + if (typeof onScroll === "function" && event) { + onScroll(event); } + } - _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; - } + _onStartShouldSetResponderCapture(event) { + const { onStartShouldSetResponderCapture } = this.props; - this._snapToItem(repositionTo, false, false, false, false); + if (onStartShouldSetResponderCapture) { + onStartShouldSetResponderCapture(event); } - _scrollTo (offset, animated = true) { - const { vertical } = this.props; - const wrappedRef = this._getWrappedRef(); + return this._getScrollEnabled(); + } - if (!this._mounted || !wrappedRef) { - return; - } - - const specificOptions = this._needsScrollView() ? { - x: vertical ? 0 : offset, - y: vertical ? offset : 0 - } : { - offset - }; - const options = { - ...specificOptions, - animated - }; + _onTouchStart() { + const { onTouchStart } = this.props; - if (this._needsScrollView()) { - wrappedRef.scrollTo(options); - } else { - wrappedRef.scrollToOffset(options); - } + // `onTouchStart` is fired even when `scrollEnabled` is set to `false` + if (this._getScrollEnabled() !== false && this._autoplaying) { + this.pauseAutoPlay(); } - _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 (onTouchStart) { + onTouchStart(); + } + } - if (nextActiveItem === this._itemToSnapTo && - scrollOffset === this._scrollOffsetRef) { - this._repositionScroll(nextActiveItem); - } + _onTouchEnd() { + const { onTouchEnd } = this.props; - if (typeof onScroll === "function" && event) { - onScroll(event); - } + if ( + this._getScrollEnabled() !== false && + this._autoplay && + !this._autoplaying + ) { + // This event is buggy on Android, so a fallback is provided in _onScrollEnd() + this.startAutoplay(); } - _onStartShouldSetResponderCapture (event) { - const { onStartShouldSetResponderCapture } = this.props; + if (onTouchEnd) { + onTouchEnd(); + } + } - if (onStartShouldSetResponderCapture) { - onStartShouldSetResponderCapture(event); - } + // Used when `enableSnap` is ENABLED + _onScrollBeginDrag(event) { + const { onScrollBeginDrag } = this.props; - return this._getScrollEnabled(); + if (!this._getScrollEnabled()) { + return; } - _onTouchStart () { - const { onTouchStart } = this.props + this._scrollStartOffset = this._getScrollOffset(event); + this._scrollStartActive = this._getActiveItem(this._scrollStartOffset); + this._ignoreNextMomentum = false; + // this._canFireCallback = false; - // `onTouchStart` is fired even when `scrollEnabled` is set to `false` - if (this._getScrollEnabled() !== false && this._autoplaying) { - this.pauseAutoPlay(); - } - - if (onTouchStart) { - onTouchStart() - } + if (onScrollBeginDrag) { + onScrollBeginDrag(event); } + } - _onTouchEnd () { - const { onTouchEnd } = this.props + // Used when `enableMomentum` is DISABLED + _onScrollEndDrag(event) { + const { onScrollEndDrag } = 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() - } + if (this._carouselRef) { + this._onScrollEnd && this._onScrollEnd(); } - // Used when `enableSnap` is ENABLED - _onScrollBeginDrag (event) { - const { onScrollBeginDrag } = this.props; + if (onScrollEndDrag) { + onScrollEndDrag(event); + } + } - if (!this._getScrollEnabled()) { - return; - } + // Used when `enableMomentum` is ENABLED + _onMomentumScrollEnd(event) { + const { onMomentumScrollEnd } = this.props; - this._scrollStartOffset = this._getScrollOffset(event); - this._scrollStartActive = this._getActiveItem(this._scrollStartOffset); - this._ignoreNextMomentum = false; - // this._canFireCallback = false; + if (this._carouselRef) { + this._onScrollEnd && this._onScrollEnd(); + } - if (onScrollBeginDrag) { - onScrollBeginDrag(event); - } + if (onMomentumScrollEnd) { + onMomentumScrollEnd(event); } + } - // Used when `enableMomentum` is DISABLED - _onScrollEndDrag (event) { - const { onScrollEndDrag } = this.props; + _onScrollEnd(event) { + const { autoplayDelay, enableSnap } = this.props; - if (this._carouselRef) { - this._onScrollEnd && this._onScrollEnd(); - } + if (this._ignoreNextMomentum) { + // iOS fix + this._ignoreNextMomentum = false; + return; + } - if (onScrollEndDrag) { - onScrollEndDrag(event); - } + if (this._currentContentOffset === this._scrollEndOffset) { + return; } - // Used when `enableMomentum` is ENABLED - _onMomentumScrollEnd (event) { - const { onMomentumScrollEnd } = this.props; + this._scrollEndOffset = this._currentContentOffset; + this._scrollEndActive = this._getActiveItem(this._scrollEndOffset); - if (this._carouselRef) { - this._onScrollEnd && this._onScrollEnd(); - } - - if (onMomentumScrollEnd) { - onMomentumScrollEnd(event); - } + if (enableSnap) { + this._snapScroll(this._scrollEndOffset - this._scrollStartOffset); } - _onScrollEnd (event) { - const { autoplayDelay, enableSnap } = this.props; + // 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); + } + } - if (this._ignoreNextMomentum) { - // iOS fix - this._ignoreNextMomentum = false; - return; - } + // 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 (this._currentContentOffset === this._scrollEndOffset) { - return; - } + if (enableMomentum && IS_IOS) { + clearTimeout(this._snapNoMomentumTimeout); + this._snapNoMomentumTimeout = setTimeout(() => { + this._snapToItem(this._activeItem); + }, 100); + } + } - this._scrollEndOffset = this._currentContentOffset; - this._scrollEndActive = this._getActiveItem(this._scrollEndOffset); + _onLayout(event) { + const { onLayout } = this.props; - if (enableSnap) { - this._snapScroll(this._scrollEndOffset - this._scrollStartOffset); - } + // 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; + } - // 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); - } + if (onLayout) { + onLayout(event); } + } - // 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; + _snapScroll(delta) { + const { swipeThreshold } = this.props; - if (enableMomentum && IS_IOS) { - clearTimeout(this._snapNoMomentumTimeout); - this._snapNoMomentumTimeout = setTimeout(() => { - this._snapToItem(this._activeItem); - }, 100); - } + // 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; } - _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); + 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._onLayoutInitDone = true; + this._snapToItem(this._scrollEndActive); } - - if (onLayout) { - onLayout(event); + } 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); + } } + } - _snapScroll (delta) { - const { swipeThreshold } = this.props; + _snapToItem( + index, + animated = true, + fireCallback = true, + initial = false, + lockScroll = true + ) { + const { enableMomentum, onSnapToItem, onBeforeSnapToItem } = this.props; + const itemsLength = this._getCustomDataLength(); + const wrappedRef = this._getWrappedRef(); - // 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 (!itemsLength || !wrappedRef) { + return; + } - 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); - } - } + if (!index || index < 0) { + index = 0; + } else if (itemsLength > 0 && index >= itemsLength) { + index = itemsLength - 1; } - _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 (index !== this._previousActiveItem) { + this._previousActiveItem = index; - if (!itemsLength || !wrappedRef) { - return; - } + // Placed here to allow overscrolling for edges items + if (lockScroll && this._canLockScroll()) { + this._lockScroll(); + } - if (!index || index < 0) { - index = 0; - } else if (itemsLength > 0 && index >= itemsLength) { - index = itemsLength - 1; + if (fireCallback) { + if (onBeforeSnapToItem) { + this._canFireBeforeCallback = true; } - 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; - } - } + 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._itemToSnapTo = index; + this._scrollOffsetRef = + this._positions[index] && this._positions[index].start; + this._onScrollTriggered = false; - 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); - } - } + if (!this._scrollOffsetRef && this._scrollOffsetRef !== 0) { + return; } - _onBeforeSnap (index) { - const { onBeforeSnapToItem } = this.props; + this._scrollTo(this._scrollOffsetRef, animated); - if (!this._carouselRef) { - return; - } + this._scrollEndOffset = this._currentContentOffset; + + if (enableMomentum) { + // iOS fix, check the note in the constructor + if (!initial) { + this._ignoreNextMomentum = true; + } - this._canFireBeforeCallback = false; - onBeforeSnapToItem && onBeforeSnapToItem(index); + // 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); + } } + } - _onSnap (index) { - const { onSnapToItem } = this.props; + _onBeforeSnap(index) { + const { onBeforeSnapToItem } = this.props; - if (!this._carouselRef) { - return; - } - - this._canFireCallback = false; - onSnapToItem && onSnapToItem(index); + if (!this._carouselRef) { + return; } - startAutoplay () { - const { autoplayInterval, autoplayDelay } = this.props; - this._autoplay = true; - - if (this._autoplaying) { - return; - } + this._canFireBeforeCallback = false; + onBeforeSnapToItem && onBeforeSnapToItem(index); + } - clearTimeout(this._autoplayTimeout); - this._autoplayTimeout = setTimeout(() => { - this._autoplaying = true; - this._autoplayInterval = setInterval(() => { - if (this._autoplaying) { - this.snapToNext(); - } - }, autoplayInterval); - }, autoplayDelay); - } + _onSnap(index) { + const { onSnapToItem } = this.props; - pauseAutoPlay () { - this._autoplaying = false; - clearTimeout(this._autoplayTimeout); - clearTimeout(this._enableAutoplayTimeout); - clearInterval(this._autoplayInterval); + if (!this._carouselRef) { + return; } - stopAutoplay () { - this._autoplay = false; - this.pauseAutoPlay(); - } + this._canFireCallback = false; + onSnapToItem && onSnapToItem(index); + } - snapToItem (index, animated = true, fireCallback = true) { - if (!index || index < 0) { - index = 0; - } + startAutoplay() { + const { autoplayInterval, autoplayDelay } = this.props; + this._autoplay = true; - const positionIndex = this._getPositionIndex(index); + if (this._autoplaying) { + return; + } - if (positionIndex === this._activeItem) { - return; + clearTimeout(this._autoplayTimeout); + this._autoplayTimeout = setTimeout(() => { + this._autoplaying = true; + this._autoplayInterval = setInterval(() => { + if (this._autoplaying) { + this.snapToNext(); } + }, autoplayInterval); + }, autoplayDelay); + } - this._snapToItem(positionIndex, animated, fireCallback); - } + pauseAutoPlay() { + this._autoplaying = false; + clearTimeout(this._autoplayTimeout); + clearTimeout(this._enableAutoplayTimeout); + clearInterval(this._autoplayInterval); + } - snapToNext (animated = true, fireCallback = true) { - const itemsLength = this._getCustomDataLength(); + stopAutoplay() { + this._autoplay = false; + this.pauseAutoPlay(); + } - let newIndex = this._activeItem + 1; - if (newIndex > itemsLength - 1) { - if (!this._enableLoop()) { - return; - } - newIndex = 0; - } - this._snapToItem(newIndex, animated, fireCallback); + snapToItem(index, animated = true, fireCallback = true) { + if (!index || index < 0) { + index = 0; } - snapToPrev (animated = true, fireCallback = true) { - const itemsLength = this._getCustomDataLength(); + const positionIndex = this._getPositionIndex(index); - let newIndex = this._activeItem - 1; - if (newIndex < 0) { - if (!this._enableLoop()) { - return; - } - newIndex = itemsLength - 1; - } - this._snapToItem(newIndex, animated, fireCallback); + if (positionIndex === this._activeItem) { + return; } - // https://github.com/facebook/react-native/issues/1831#issuecomment-231069668 - triggerRenderingHack (offset) { - // Avoid messing with user scroll - if (Date.now() - this._lastScrollDate < 500) { - return; - } + this._snapToItem(positionIndex, animated, fireCallback); + } - const scrollPosition = this._currentContentOffset; - if (!scrollPosition && scrollPosition !== 0) { - return; - } + snapToNext(animated = true, fireCallback = true) { + const itemsLength = this._getCustomDataLength(); - const scrollOffset = offset || (scrollPosition === 0 ? 1 : -1); - this._scrollTo(scrollPosition + scrollOffset, false); + let newIndex = this._activeItem + 1; + if (newIndex > itemsLength - 1) { + if (!this._enableLoop()) { + return; + } + newIndex = 0; } + this._snapToItem(newIndex, animated, fireCallback); + } - _getSlideInterpolatedStyle (index, animatedValue) { - const { layoutCardOffset, slideInterpolatedStyle } = this.props; + snapToPrev(animated = true, fireCallback = true) { + const itemsLength = this._getCustomDataLength(); - 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); - } + let newIndex = this._activeItem - 1; + if (newIndex < 0) { + if (!this._enableLoop()) { + return; + } + newIndex = itemsLength - 1; } + this._snapToItem(newIndex, animated, fireCallback); + } - _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 ( - - { renderItem({ item, index }, parallaxProps) } - - ); - } - - _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 - }; + // https://github.com/facebook/react-native/issues/1831#issuecomment-231069668 + triggerRenderingHack(offset) { + // Avoid messing with user scroll + if (Date.now() - this._lastScrollDate < 500) { + return; } - _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 - }; + const scrollPosition = this._currentContentOffset; + if (!scrollPosition && scrollPosition !== 0) { + return; } - render () { - const { data, renderItem, useScrollView } = this.props; + const scrollOffset = offset || (scrollPosition === 0 ? 1 : -1); + this._scrollTo(scrollPosition + scrollOffset, false); + } - if (!data || !renderItem) { - return null; - } + _getSlideInterpolatedStyle(index, animatedValue) { + const { layoutCardOffset, slideInterpolatedStyle } = this.props; - const props = { - ...this._getComponentOverridableProps(), - ...this.props, - ...this._getComponentStaticProps() - }; + 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 ( + + {renderItem({ item, index }, parallaxProps)} + + ); + } + + _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, + }; + } - const ScrollViewComponent = typeof useScrollView === 'function' ? useScrollView : AnimatedScrollView + render() { + const { data, renderItem, useScrollView } = this.props; - return this._needsScrollView() ? ( - - { - this._getCustomData().map((item, index) => { - return this._renderItem({ item, index }); - }) - } - - ) : ( - - ); + if (!data || !renderItem) { + return null; } + + const props = { + ...this._getComponentOverridableProps(), + ...this.props, + ...this._getComponentStaticProps(), + }; + + const ScrollViewComponent = + typeof useScrollView === "function" ? useScrollView : AnimatedScrollView; + + return this._needsScrollView() ? ( + + {this._getCustomData().map((item, index) => { + return this._renderItem({ item, index }); + })} + + ) : ( + + ); + } } diff --git a/src/pagination/Pagination.js b/src/pagination/Pagination.js index 5c021cf36..783ec871d 100644 --- a/src/pagination/Pagination.js +++ b/src/pagination/Pagination.js @@ -1,167 +1,185 @@ -import React, { PureComponent } from 'react'; -import { I18nManager, Platform, View, ViewPropTypes } from 'react-native'; -import PropTypes from 'prop-types'; -import PaginationDot from './PaginationDot'; -import styles from './Pagination.style'; - -const IS_IOS = Platform.OS === 'ios'; +import React, { PureComponent } from "react"; +import { I18nManager, Platform, View } from "react-native"; +import { ViewPropTypes } from "deprecated-react-native-prop-types"; +import PropTypes from "prop-types"; +import PaginationDot from "./PaginationDot"; +import styles from "./Pagination.style"; + +const IS_IOS = Platform.OS === "ios"; const IS_RTL = I18nManager.isRTL; export default class Pagination extends PureComponent { - - static propTypes = { - activeDotIndex: PropTypes.number.isRequired, - dotsLength: PropTypes.number.isRequired, - activeOpacity: PropTypes.number, - carouselRef: PropTypes.object, - containerStyle: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style, - dotColor: PropTypes.string, - dotContainerStyle: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style, - dotElement: PropTypes.element, - dotStyle: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style, - inactiveDotColor: PropTypes.string, - inactiveDotElement: PropTypes.element, - inactiveDotOpacity: PropTypes.number, - inactiveDotScale: PropTypes.number, - inactiveDotStyle: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style, - renderDots: PropTypes.func, - tappableDots: PropTypes.bool, - vertical: PropTypes.bool, - accessibilityLabel: PropTypes.string, - animatedDuration: PropTypes.number, - animatedFriction: PropTypes.number, - animatedTension: PropTypes.number, - delayPressInDot: PropTypes.number, - }; - - static defaultProps = { - inactiveDotOpacity: 0.5, - inactiveDotScale: 0.5, - tappableDots: false, - vertical: false, - animatedDuration: 250, - animatedFriction: 4, - animatedTension: 50, - delayPressInDot: 0, + static propTypes = { + activeDotIndex: PropTypes.number.isRequired, + dotsLength: PropTypes.number.isRequired, + activeOpacity: PropTypes.number, + carouselRef: PropTypes.object, + containerStyle: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style, + dotColor: PropTypes.string, + dotContainerStyle: ViewPropTypes + ? ViewPropTypes.style + : View.propTypes.style, + dotElement: PropTypes.element, + dotStyle: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style, + inactiveDotColor: PropTypes.string, + inactiveDotElement: PropTypes.element, + inactiveDotOpacity: PropTypes.number, + inactiveDotScale: PropTypes.number, + inactiveDotStyle: ViewPropTypes + ? ViewPropTypes.style + : View.propTypes.style, + renderDots: PropTypes.func, + tappableDots: PropTypes.bool, + vertical: PropTypes.bool, + accessibilityLabel: PropTypes.string, + animatedDuration: PropTypes.number, + animatedFriction: PropTypes.number, + animatedTension: PropTypes.number, + delayPressInDot: PropTypes.number, + }; + + static defaultProps = { + inactiveDotOpacity: 0.5, + inactiveDotScale: 0.5, + tappableDots: false, + vertical: false, + animatedDuration: 250, + animatedFriction: 4, + animatedTension: 50, + delayPressInDot: 0, + }; + + constructor(props) { + super(props); + + // Warnings + if ( + (props.dotColor && !props.inactiveDotColor) || + (!props.dotColor && props.inactiveDotColor) + ) { + console.warn( + "react-native-snap-carousel | Pagination: " + + "You need to specify both `dotColor` and `inactiveDotColor`" + ); } - - constructor (props) { - super(props); - - // Warnings - if ((props.dotColor && !props.inactiveDotColor) || (!props.dotColor && props.inactiveDotColor)) { - console.warn( - 'react-native-snap-carousel | Pagination: ' + - 'You need to specify both `dotColor` and `inactiveDotColor`' - ); - } - if ((props.dotElement && !props.inactiveDotElement) || (!props.dotElement && props.inactiveDotElement)) { - console.warn( - 'react-native-snap-carousel | Pagination: ' + - 'You need to specify both `dotElement` and `inactiveDotElement`' - ); - } - if (props.tappableDots && props.carouselRef === undefined) { - console.warn( - 'react-native-snap-carousel | Pagination: ' + - 'You must specify prop `carouselRef` when setting `tappableDots` to `true`' - ); - } + if ( + (props.dotElement && !props.inactiveDotElement) || + (!props.dotElement && props.inactiveDotElement) + ) { + console.warn( + "react-native-snap-carousel | Pagination: " + + "You need to specify both `dotElement` and `inactiveDotElement`" + ); } - - _needsRTLAdaptations () { - const { vertical } = this.props; - return IS_RTL && !IS_IOS && !vertical; + if (props.tappableDots && props.carouselRef === undefined) { + console.warn( + "react-native-snap-carousel | Pagination: " + + "You must specify prop `carouselRef` when setting `tappableDots` to `true`" + ); } - - get _activeDotIndex () { - const { activeDotIndex, dotsLength } = this.props; - return this._needsRTLAdaptations() ? dotsLength - activeDotIndex - 1 : activeDotIndex; + } + + _needsRTLAdaptations() { + const { vertical } = this.props; + return IS_RTL && !IS_IOS && !vertical; + } + + get _activeDotIndex() { + const { activeDotIndex, dotsLength } = this.props; + return this._needsRTLAdaptations() + ? dotsLength - activeDotIndex - 1 + : activeDotIndex; + } + + get dots() { + const { + activeOpacity, + carouselRef, + dotsLength, + dotColor, + dotContainerStyle, + dotElement, + dotStyle, + inactiveDotColor, + inactiveDotElement, + inactiveDotOpacity, + inactiveDotScale, + inactiveDotStyle, + renderDots, + tappableDots, + animatedDuration, + animatedFriction, + animatedTension, + delayPressInDot, + } = this.props; + + if (renderDots) { + return renderDots(this._activeDotIndex, dotsLength, this); } - get dots () { - const { - activeOpacity, - carouselRef, - dotsLength, - dotColor, - dotContainerStyle, - dotElement, - dotStyle, - inactiveDotColor, - inactiveDotElement, - inactiveDotOpacity, - inactiveDotScale, - inactiveDotStyle, - renderDots, - tappableDots, - animatedDuration, - animatedFriction, - animatedTension, - delayPressInDot, - } = this.props; - - if (renderDots) { - return renderDots(this._activeDotIndex, dotsLength, this); + const DefaultDot = ( + + ); + + const dots = [...Array(dotsLength).keys()].map((i) => { + const isActive = i === this._activeDotIndex; + return React.cloneElement( + (isActive ? dotElement : inactiveDotElement) || DefaultDot, + { + key: `pagination-dot-${i}`, + active: isActive, + index: i, } + ); + }); - const DefaultDot = ; - - const dots = [...Array(dotsLength).keys()].map(i => { - const isActive = i === this._activeDotIndex; - return React.cloneElement( - (isActive ? dotElement : inactiveDotElement) || DefaultDot, - { - key: `pagination-dot-${i}`, - active: isActive, - index: i - } - ); - }); - - return dots; - } - - render () { - const { dotsLength, containerStyle, vertical, accessibilityLabel } = this.props; + return dots; + } - if (!dotsLength || dotsLength < 2) { - return false; - } + render() { + const { dotsLength, containerStyle, vertical, accessibilityLabel } = + this.props; - const style = [ - styles.sliderPagination, - { flexDirection: vertical ? - 'column' : - (this._needsRTLAdaptations() ? 'row-reverse' : 'row') - }, - containerStyle || {} - ]; - - return ( - - { this.dots } - - ); + if (!dotsLength || dotsLength < 2) { + return false; } + + const style = [ + styles.sliderPagination, + { + flexDirection: vertical + ? "column" + : this._needsRTLAdaptations() + ? "row-reverse" + : "row", + }, + containerStyle || {}, + ]; + + return ( + + {this.dots} + + ); + } } diff --git a/src/pagination/PaginationDot.js b/src/pagination/PaginationDot.js index e59d1969f..aa319e115 100644 --- a/src/pagination/PaginationDot.js +++ b/src/pagination/PaginationDot.js @@ -1,156 +1,165 @@ -import React, { PureComponent } from 'react'; -import { View, Animated, Easing, TouchableOpacity, ViewPropTypes } from 'react-native'; -import PropTypes from 'prop-types'; -import styles from './Pagination.style'; +import React, { PureComponent } from "react"; +import { View, Animated, Easing, TouchableOpacity } from "react-native"; +import { ViewPropTypes } from "deprecated-react-native-prop-types"; +import PropTypes from "prop-types"; +import styles from "./Pagination.style"; export default class PaginationDot extends PureComponent { - - static propTypes = { - inactiveOpacity: PropTypes.number.isRequired, - inactiveScale: PropTypes.number.isRequired, - active: PropTypes.bool, - activeOpacity: PropTypes.number, - carouselRef: PropTypes.object, - color: PropTypes.string, - containerStyle: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style, - inactiveColor: PropTypes.string, - inactiveStyle: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style, - index: PropTypes.number, - style: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style, - tappable: PropTypes.bool + static propTypes = { + inactiveOpacity: PropTypes.number.isRequired, + inactiveScale: PropTypes.number.isRequired, + active: PropTypes.bool, + activeOpacity: PropTypes.number, + carouselRef: PropTypes.object, + color: PropTypes.string, + containerStyle: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style, + inactiveColor: PropTypes.string, + inactiveStyle: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style, + index: PropTypes.number, + style: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style, + tappable: PropTypes.bool, + }; + + constructor(props) { + super(props); + this.state = { + animColor: new Animated.Value(0), + animOpacity: new Animated.Value(0), + animTransform: new Animated.Value(0), }; + } - constructor (props) { - super(props); - this.state = { - animColor: new Animated.Value(0), - animOpacity: new Animated.Value(0), - animTransform: new Animated.Value(0) - }; - } - - componentDidMount () { - if (this.props.active) { - this._animate(1); - } + componentDidMount() { + if (this.props.active) { + this._animate(1); } + } - componentDidUpdate (prevProps) { - if (prevProps.active !== this.props.active) { - this._animate(this.props.active ? 1 : 0); - } + componentDidUpdate(prevProps) { + if (prevProps.active !== this.props.active) { + this._animate(this.props.active ? 1 : 0); } + } - _animate (toValue = 0) { - const { animColor, animOpacity, animTransform } = this.state; - const { animatedDuration, animatedFriction, animatedTension } = this.props - - const commonProperties = { - toValue, - duration: animatedDuration, - isInteraction: false, - useNativeDriver: !this._shouldAnimateColor - }; - - let animations = [ - Animated.timing(animOpacity, { - easing: Easing.linear, - ...commonProperties - }), - Animated.spring(animTransform, { - friction: animatedFriction, - tension: animatedTension, - ...commonProperties - }) - ]; - - if (this._shouldAnimateColor) { - animations.push(Animated.timing(animColor, { - easing: Easing.linear, - ...commonProperties - })); - } + _animate(toValue = 0) { + const { animColor, animOpacity, animTransform } = this.state; + const { animatedDuration, animatedFriction, animatedTension } = this.props; - Animated.parallel(animations).start(); - } + const commonProperties = { + toValue, + duration: animatedDuration, + isInteraction: false, + useNativeDriver: !this._shouldAnimateColor, + }; - get _shouldAnimateColor () { - const { color, inactiveColor } = this.props; - return color && inactiveColor; + let animations = [ + Animated.timing(animOpacity, { + easing: Easing.linear, + ...commonProperties, + }), + Animated.spring(animTransform, { + friction: animatedFriction, + tension: animatedTension, + ...commonProperties, + }), + ]; + + if (this._shouldAnimateColor) { + animations.push( + Animated.timing(animColor, { + easing: Easing.linear, + ...commonProperties, + }) + ); } - render () { - const { animColor, animOpacity, animTransform } = this.state; - const { - active, - activeOpacity, - carouselRef, - color, - containerStyle, - inactiveColor, - inactiveStyle, - inactiveOpacity, - inactiveScale, - index, - style, - tappable, - delayPressInDot - } = this.props; - - const animatedStyle = { - opacity: animOpacity.interpolate({ - inputRange: [0, 1], - outputRange: [inactiveOpacity, 1] - }), - transform: [{ - scale: animTransform.interpolate({ - inputRange: [0, 1], - outputRange: [inactiveScale, 1] - }) - }] - }; - const animatedColor = this._shouldAnimateColor ? { - backgroundColor: animColor.interpolate({ - inputRange: [0, 1], - outputRange: [inactiveColor, color] - }) - } : {}; - - const dotContainerStyle = [ - styles.sliderPaginationDotContainer, - containerStyle || {} - ]; - - const dotStyle = [ - styles.sliderPaginationDot, - style || {}, - (!active && inactiveStyle) || {}, - animatedStyle, - animatedColor - ]; - - const onPress = tappable ? () => { - try { - const currentRef = carouselRef.current || carouselRef; - currentRef._snapToItem(currentRef._getPositionIndex(index)); - } catch (error) { - console.warn( - 'react-native-snap-carousel | Pagination: ' + - '`carouselRef` has to be a Carousel ref.\n' + error - ); - } - } : undefined; - - return ( - - - - ); - } + Animated.parallel(animations).start(); + } + + get _shouldAnimateColor() { + const { color, inactiveColor } = this.props; + return color && inactiveColor; + } + + render() { + const { animColor, animOpacity, animTransform } = this.state; + const { + active, + activeOpacity, + carouselRef, + color, + containerStyle, + inactiveColor, + inactiveStyle, + inactiveOpacity, + inactiveScale, + index, + style, + tappable, + delayPressInDot, + } = this.props; + + const animatedStyle = { + opacity: animOpacity.interpolate({ + inputRange: [0, 1], + outputRange: [inactiveOpacity, 1], + }), + transform: [ + { + scale: animTransform.interpolate({ + inputRange: [0, 1], + outputRange: [inactiveScale, 1], + }), + }, + ], + }; + const animatedColor = this._shouldAnimateColor + ? { + backgroundColor: animColor.interpolate({ + inputRange: [0, 1], + outputRange: [inactiveColor, color], + }), + } + : {}; + + const dotContainerStyle = [ + styles.sliderPaginationDotContainer, + containerStyle || {}, + ]; + + const dotStyle = [ + styles.sliderPaginationDot, + style || {}, + (!active && inactiveStyle) || {}, + animatedStyle, + animatedColor, + ]; + + const onPress = tappable + ? () => { + try { + const currentRef = carouselRef.current || carouselRef; + currentRef._snapToItem(currentRef._getPositionIndex(index)); + } catch (error) { + console.warn( + "react-native-snap-carousel | Pagination: " + + "`carouselRef` has to be a Carousel ref.\n" + + error + ); + } + } + : undefined; + + return ( + + + + ); + } } diff --git a/src/parallaximage/ParallaxImage.js b/src/parallaximage/ParallaxImage.js index 8bc774a10..0e203a32f 100644 --- a/src/parallaximage/ParallaxImage.js +++ b/src/parallaximage/ParallaxImage.js @@ -1,222 +1,241 @@ // Parallax effect inspired by https://github.com/oblador/react-native-parallax/ -import React, { Component } from 'react'; -import { View, ViewPropTypes, Image, Animated, Easing, ActivityIndicator, findNodeHandle } from 'react-native'; -import PropTypes from 'prop-types'; -import styles from './ParallaxImage.style'; +import React, { Component } from "react"; +import { + View, + Image, + Animated, + Easing, + ActivityIndicator, + findNodeHandle, +} from "react-native"; +import { ViewPropTypes } from "deprecated-react-native-prop-types"; +import PropTypes from "prop-types"; +import styles from "./ParallaxImage.style"; export default class ParallaxImage extends Component { - - static propTypes = { - ...Image.propTypes, - carouselRef: PropTypes.object, // passed from - itemHeight: PropTypes.number, // passed from - itemWidth: PropTypes.number, // passed from - scrollPosition: PropTypes.object, // passed from - sliderHeight: PropTypes.number, // passed from - sliderWidth: PropTypes.number, // passed from - vertical: PropTypes.bool, // passed from - containerStyle: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style, - dimensions: PropTypes.shape({ - width: PropTypes.number, - height: PropTypes.number - }), - fadeDuration: PropTypes.number, - parallaxFactor: PropTypes.number, - showSpinner: PropTypes.bool, - spinnerColor: PropTypes.string, - AnimatedImageComponent: PropTypes.oneOfType([ - PropTypes.func, - PropTypes.object - ]) + static propTypes = { + ...Image.propTypes, + carouselRef: PropTypes.object, // passed from + itemHeight: PropTypes.number, // passed from + itemWidth: PropTypes.number, // passed from + scrollPosition: PropTypes.object, // passed from + sliderHeight: PropTypes.number, // passed from + sliderWidth: PropTypes.number, // passed from + vertical: PropTypes.bool, // passed from + containerStyle: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style, + dimensions: PropTypes.shape({ + width: PropTypes.number, + height: PropTypes.number, + }), + fadeDuration: PropTypes.number, + parallaxFactor: PropTypes.number, + showSpinner: PropTypes.bool, + spinnerColor: PropTypes.string, + AnimatedImageComponent: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.object, + ]), + }; + + static defaultProps = { + containerStyle: {}, + fadeDuration: 500, + parallaxFactor: 0.3, + showSpinner: true, + spinnerColor: "rgba(0, 0, 0, 0.4)", + AnimatedImageComponent: Animated.Image, + }; + + constructor(props) { + super(props); + this.state = { + offset: 0, + width: 0, + height: 0, + status: 1, // 1 -> loading; 2 -> loaded // 3 -> transition finished; 4 -> error + animOpacity: new Animated.Value(0), }; - - static defaultProps = { - containerStyle: {}, - fadeDuration: 500, - parallaxFactor: 0.3, - showSpinner: true, - spinnerColor: 'rgba(0, 0, 0, 0.4)', - AnimatedImageComponent: Animated.Image - } - - constructor (props) { - super(props); - this.state = { - offset: 0, - width: 0, - height: 0, - status: 1, // 1 -> loading; 2 -> loaded // 3 -> transition finished; 4 -> error - animOpacity: new Animated.Value(0) - }; - this._onLoad = this._onLoad.bind(this); - this._onError = this._onError.bind(this); - this._measureLayout = this._measureLayout.bind(this); - } - - setNativeProps (nativeProps) { - this._container.setNativeProps(nativeProps); - } - - componentDidMount () { - this._mounted = true; - - setTimeout(() => { - this._measureLayout(); - }, 0); - } - - componentWillUnmount () { - this._mounted = false; + this._onLoad = this._onLoad.bind(this); + this._onError = this._onError.bind(this); + this._measureLayout = this._measureLayout.bind(this); + } + + setNativeProps(nativeProps) { + this._container.setNativeProps(nativeProps); + } + + componentDidMount() { + this._mounted = true; + + setTimeout(() => { + this._measureLayout(); + }, 0); + } + + componentWillUnmount() { + this._mounted = false; + } + + _measureLayout() { + if (this._container) { + const { + dimensions, + vertical, + carouselRef, + sliderWidth, + sliderHeight, + itemWidth, + itemHeight, + } = this.props; + + if (carouselRef) { + this._container.measureLayout( + findNodeHandle(carouselRef), + (x, y, width, height, pageX, pageY) => { + const offset = vertical + ? y - (sliderHeight - itemHeight) / 2 + : x - (sliderWidth - itemWidth) / 2; + + this.setState({ + offset: offset, + width: + dimensions && dimensions.width + ? dimensions.width + : Math.ceil(width), + height: + dimensions && dimensions.height + ? dimensions.height + : Math.ceil(height), + }); + } + ); + } } + } - _measureLayout () { - if (this._container) { - const { - dimensions, - vertical, - carouselRef, - sliderWidth, - sliderHeight, - itemWidth, - itemHeight - } = this.props; - - if (carouselRef) { - this._container.measureLayout( - findNodeHandle(carouselRef), - (x, y, width, height, pageX, pageY) => { - const offset = vertical ? - y - ((sliderHeight - itemHeight) / 2) : - x - ((sliderWidth - itemWidth) / 2); - - this.setState({ - offset: offset, - width: dimensions && dimensions.width ? - dimensions.width : - Math.ceil(width), - height: dimensions && dimensions.height ? - dimensions.height : - Math.ceil(height) - }); - } - ); - } - } - } + _onLoad(event) { + const { animOpacity } = this.state; + const { fadeDuration, onLoad } = this.props; - _onLoad (event) { - const { animOpacity } = this.state; - const { fadeDuration, onLoad } = this.props; - - if (!this._mounted) { - return; - } - - this.setState({ status: 2 }); - - if (onLoad) { - onLoad(event); - } - - Animated.timing(animOpacity, { - toValue: 1, - duration: fadeDuration, - easing: Easing.out(Easing.quad), - isInteraction: false, - useNativeDriver: true - }).start(() => { - this.setState({ status: 3 }); - }); + if (!this._mounted) { + return; } - // If arg is missing from method signature, it just won't be called - _onError (event) { - const { onError } = this.props; - - this.setState({ status: 4 }); - - if (onError) { - onError(event); - } - } + this.setState({ status: 2 }); - get image () { - const { status, animOpacity, offset, width, height } = this.state; - const { - scrollPosition, - dimensions, - vertical, - sliderWidth, - sliderHeight, - parallaxFactor, - style, - AnimatedImageComponent, - ...other - } = this.props; - - const parallaxPadding = (vertical ? height : width) * parallaxFactor; - const requiredStyles = { position: 'relative' }; - const dynamicStyles = { - width: vertical ? width : width + parallaxPadding * 2, - height: vertical ? height + parallaxPadding * 2 : height, - opacity: animOpacity, - transform: scrollPosition ? [ - { - translateX: !vertical ? scrollPosition.interpolate({ - inputRange: [offset - sliderWidth, offset + sliderWidth], - outputRange: [-parallaxPadding, parallaxPadding], - extrapolate: 'clamp' - }) : 0 - }, - { - translateY: vertical ? scrollPosition.interpolate({ - inputRange: [offset - sliderHeight, offset + sliderHeight], - outputRange: [-parallaxPadding, parallaxPadding], - extrapolate: 'clamp' - }) : 0 - } - ] : [] - }; - - return ( - - ); + if (onLoad) { + onLoad(event); } - get spinner () { - const { status } = this.state; - const { showSpinner, spinnerColor } = this.props; - - return status === 1 && showSpinner ? ( - - - - ) : false; + Animated.timing(animOpacity, { + toValue: 1, + duration: fadeDuration, + easing: Easing.out(Easing.quad), + isInteraction: false, + useNativeDriver: true, + }).start(() => { + this.setState({ status: 3 }); + }); + } + + // If arg is missing from method signature, it just won't be called + _onError(event) { + const { onError } = this.props; + + this.setState({ status: 4 }); + + if (onError) { + onError(event); } + } + + get image() { + const { status, animOpacity, offset, width, height } = this.state; + const { + scrollPosition, + dimensions, + vertical, + sliderWidth, + sliderHeight, + parallaxFactor, + style, + AnimatedImageComponent, + ...other + } = this.props; + + const parallaxPadding = (vertical ? height : width) * parallaxFactor; + const requiredStyles = { position: "relative" }; + const dynamicStyles = { + width: vertical ? width : width + parallaxPadding * 2, + height: vertical ? height + parallaxPadding * 2 : height, + opacity: animOpacity, + transform: scrollPosition + ? [ + { + translateX: !vertical + ? scrollPosition.interpolate({ + inputRange: [offset - sliderWidth, offset + sliderWidth], + outputRange: [-parallaxPadding, parallaxPadding], + extrapolate: "clamp", + }) + : 0, + }, + { + translateY: vertical + ? scrollPosition.interpolate({ + inputRange: [offset - sliderHeight, offset + sliderHeight], + outputRange: [-parallaxPadding, parallaxPadding], + extrapolate: "clamp", + }) + : 0, + }, + ] + : [], + }; - render () { - const { containerStyle } = this.props; - - return ( - { this._container = c; }} - pointerEvents={'none'} - style={[containerStyle, styles.container]} - onLayout={this._measureLayout} - > - { this.image } - { this.spinner } - - ); - } + return ( + + ); + } + + get spinner() { + const { status } = this.state; + const { showSpinner, spinnerColor } = this.props; + + return status === 1 && showSpinner ? ( + + + + ) : ( + false + ); + } + + render() { + const { containerStyle } = this.props; + + return ( + { + this._container = c; + }} + pointerEvents={"none"} + style={[containerStyle, styles.container]} + onLayout={this._measureLayout} + > + {this.image} + {this.spinner} + + ); + } }