UNPKG

react-native-snap-carousel-v4

Version:

Original project: https://github.com/meliorence/react-native-snap-carousel I made this package because I need the version 4 package to be published, so that I can run EAS Build on my expo app, previously I was pointing directly to the v4 branch on the ori

384 lines (353 loc) 14.3 kB
import { Platform, Animated } from 'react-native'; import type { CarouselProps } from 'src/carousel/types'; const IS_ANDROID = Platform.OS === 'android'; // Get scroll interpolator's input range from an array of slide indexes // Indexes are relative to the current active slide (index 0) // For example, using [3, 2, 1, 0, -1] will return: // [ // (index - 3) * sizeRef, // active + 3 // (index - 2) * sizeRef, // active + 2 // (index - 1) * sizeRef, // active + 1 // index * sizeRef, // active // (index + 1) * sizeRef // active - 1 // ] export function getInputRangeFromIndexes<TData> ( range: number[], index: number, carouselProps: CarouselProps<TData> ) { const sizeRef = carouselProps.vertical ? carouselProps.itemHeight : carouselProps.itemWidth; const inputRange = []; for (let i = 0; i < range.length; i++) { inputRange.push((index - range[i]) * sizeRef); } return inputRange; } // Default behavior // Scale and/or opacity effect // Based on props 'inactiveSlideOpacity' and 'inactiveSlideScale' export function defaultScrollInterpolator<TData> ( index: number, carouselProps: CarouselProps<TData> ) { const range = [1, 0, -1]; const inputRange = getInputRangeFromIndexes(range, index, carouselProps); const outputRange = [0, 1, 0]; return { inputRange, outputRange }; } export function defaultAnimatedStyles<TData> ( _index: number, animatedValue: Animated.AnimatedInterpolation, carouselProps: CarouselProps<TData> ) { let animatedOpacity = {}; let animatedScale = {}; if (carouselProps.inactiveSlideOpacity < 1) { animatedOpacity = { opacity: animatedValue.interpolate({ inputRange: [0, 1], outputRange: [carouselProps.inactiveSlideOpacity, 1] }) }; } if (carouselProps.inactiveSlideScale < 1) { animatedScale = { transform: [ { scale: animatedValue.interpolate({ inputRange: [0, 1], outputRange: [carouselProps.inactiveSlideScale, 1] }) } ] }; } return { ...animatedOpacity, ...animatedScale }; } // Shift animation // Same as the default one, but the active slide is also shifted up or down // Based on prop 'inactiveSlideShift' export function shiftAnimatedStyles<TData> ( _index: number, animatedValue: Animated.AnimatedInterpolation, carouselProps: CarouselProps<TData> ) { let animatedOpacity = {}; let animatedScale = {}; let animatedTranslate = {}; if (carouselProps.inactiveSlideOpacity < 1) { animatedOpacity = { opacity: animatedValue.interpolate({ inputRange: [0, 1], outputRange: [carouselProps.inactiveSlideOpacity, 1] }) }; } if (carouselProps.inactiveSlideScale < 1) { animatedScale = { scale: animatedValue.interpolate({ inputRange: [0, 1], outputRange: [carouselProps.inactiveSlideScale, 1] }) }; } if (carouselProps.inactiveSlideShift !== 0) { const translateProp = carouselProps.vertical ? 'translateX' : 'translateY'; animatedTranslate = { [translateProp]: animatedValue.interpolate({ inputRange: [0, 1], outputRange: [carouselProps.inactiveSlideShift, 0] }) }; } return { ...animatedOpacity, transform: [{ ...animatedScale }, { ...animatedTranslate }] }; } // Stack animation // Imitate a deck/stack of cards (see #195) // WARNING: The effect had to be visually inverted on Android because this OS doesn't honor the `zIndex`property // This means that the item with the higher zIndex (and therefore the tap receiver) remains the one AFTER the currently active item // The `elevation` property compensates for that only visually, which is not good enough export function stackScrollInterpolator<TData> ( index: number, carouselProps: CarouselProps<TData> ) { const range = IS_ANDROID ? [1, 0, -1, -2, -3] : [3, 2, 1, 0, -1]; const inputRange = getInputRangeFromIndexes(range, index, carouselProps); const outputRange = range; return { inputRange, outputRange }; } export function stackAnimatedStyles<TData> ( index: number, animatedValue: Animated.AnimatedInterpolation, carouselProps: CarouselProps<TData>, cardOffset?: number ) { const sizeRef = carouselProps.vertical ? carouselProps.itemHeight : carouselProps.itemWidth; const translateProp = carouselProps.vertical ? 'translateY' : 'translateX'; const card1Scale = 0.9; const card2Scale = 0.8; const newCardOffset = cardOffset ?? 18; const getTranslateFromScale = (cardIndex: number, scale: number) => { const centerFactor = (1 / scale) * cardIndex; const centeredPosition = -Math.round(sizeRef * centerFactor); const edgeAlignment = Math.round((sizeRef - sizeRef * scale) / 2); const offset = Math.round((newCardOffset * Math.abs(cardIndex)) / scale); return IS_ANDROID ? centeredPosition - edgeAlignment - offset : centeredPosition + edgeAlignment + offset; }; const opacityOutputRange = carouselProps.inactiveSlideOpacity === 1 ? [1, 1, 1, 0] : [1, 0.75, 0.5, 0]; return IS_ANDROID ? { // elevation: carouselProps.data.length - index, // fix zIndex bug visually, but not from a logic point of view opacity: animatedValue.interpolate({ inputRange: [-3, -2, -1, 0], outputRange: opacityOutputRange.reverse(), extrapolate: 'clamp' }), transform: [ { scale: animatedValue.interpolate({ inputRange: [-2, -1, 0, 1], outputRange: [card2Scale, card1Scale, 1, card1Scale], extrapolate: 'clamp' }) }, { [translateProp]: animatedValue.interpolate({ inputRange: [-3, -2, -1, 0, 1], outputRange: [ getTranslateFromScale(-3, card2Scale), getTranslateFromScale(-2, card2Scale), getTranslateFromScale(-1, card1Scale), 0, sizeRef * 0.5 ], extrapolate: 'clamp' }) } ] } : { zIndex: carouselProps.data.length - index, opacity: animatedValue.interpolate({ inputRange: [0, 1, 2, 3], outputRange: opacityOutputRange, extrapolate: 'clamp' }), transform: [ { scale: animatedValue.interpolate({ inputRange: [-1, 0, 1, 2], outputRange: [card1Scale, 1, card1Scale, card2Scale], extrapolate: 'clamp' }) }, { [translateProp]: animatedValue.interpolate({ inputRange: [-1, 0, 1, 2, 3], outputRange: [ -sizeRef * 0.5, 0, getTranslateFromScale(1, card1Scale), getTranslateFromScale(2, card2Scale), getTranslateFromScale(3, card2Scale) ], extrapolate: 'clamp' }) } ] }; } // Tinder animation // Imitate the popular Tinder layout // WARNING: The effect had to be visually inverted on Android because this OS doesn't honor the `zIndex`property // This means that the item with the higher zIndex (and therefore the tap receiver) remains the one AFTER the currently active item // The `elevation` property compensates for that only visually, which is not good enough export function tinderScrollInterpolator<TData> ( index: number, carouselProps: CarouselProps<TData> ) { const range = IS_ANDROID ? [1, 0, -1, -2, -3] : [3, 2, 1, 0, -1]; const inputRange = getInputRangeFromIndexes(range, index, carouselProps); const outputRange = range; return { inputRange, outputRange }; } export function tinderAnimatedStyles<TData> ( index: number, animatedValue: Animated.AnimatedInterpolation, carouselProps: CarouselProps<TData>, cardOffset?: number ) { const sizeRef = carouselProps.vertical ? carouselProps.itemHeight : carouselProps.itemWidth; const mainTranslateProp = carouselProps.vertical ? 'translateY' : 'translateX'; const secondaryTranslateProp = carouselProps.vertical ? 'translateX' : 'translateY'; const card1Scale = 0.96; const card2Scale = 0.92; const card3Scale = 0.88; const peekingCardsOpacity = IS_ANDROID ? 0.92 : 1; const newCardOffset = cardOffset ?? 9; const getMainTranslateFromScale = (cardIndex: number, scale: number) => { const centerFactor = (1 / scale) * cardIndex; return -Math.round(sizeRef * centerFactor); }; const getSecondaryTranslateFromScale = (cardIndex: number, scale: number) => { return Math.round((newCardOffset * Math.abs(cardIndex)) / scale); }; return IS_ANDROID ? { // elevation: carouselProps.data.length - index, // fix zIndex bug visually, but not from a logic point of view opacity: animatedValue.interpolate({ inputRange: [-3, -2, -1, 0, 1], outputRange: [0, peekingCardsOpacity, peekingCardsOpacity, 1, 0], extrapolate: 'clamp' }), transform: [ { scale: animatedValue.interpolate({ inputRange: [-3, -2, -1, 0], outputRange: [card3Scale, card2Scale, card1Scale, 1], extrapolate: 'clamp' }) }, { rotate: animatedValue.interpolate({ inputRange: [0, 1], outputRange: ['0deg', '22deg'], extrapolate: 'clamp' }) }, { [mainTranslateProp]: animatedValue.interpolate({ inputRange: [-3, -2, -1, 0, 1], outputRange: [ getMainTranslateFromScale(-3, card3Scale), getMainTranslateFromScale(-2, card2Scale), getMainTranslateFromScale(-1, card1Scale), 0, sizeRef * 1.1 ], extrapolate: 'clamp' }) }, { [secondaryTranslateProp]: animatedValue.interpolate({ inputRange: [-3, -2, -1, 0], outputRange: [ getSecondaryTranslateFromScale(-3, card3Scale), getSecondaryTranslateFromScale(-2, card2Scale), getSecondaryTranslateFromScale(-1, card1Scale), 0 ], extrapolate: 'clamp' }) } ] } : { zIndex: carouselProps.data.length - index, opacity: animatedValue.interpolate({ inputRange: [-1, 0, 1, 2, 3], outputRange: [0, 1, peekingCardsOpacity, peekingCardsOpacity, 0], extrapolate: 'clamp' }), transform: [ { scale: animatedValue.interpolate({ inputRange: [0, 1, 2, 3], outputRange: [1, card1Scale, card2Scale, card3Scale], extrapolate: 'clamp' }) }, { rotate: animatedValue.interpolate({ inputRange: [-1, 0], outputRange: ['-22deg', '0deg'], extrapolate: 'clamp' }) }, { [mainTranslateProp]: animatedValue.interpolate({ inputRange: [-1, 0, 1, 2, 3], outputRange: [ -sizeRef * 1.1, 0, getMainTranslateFromScale(1, card1Scale), getMainTranslateFromScale(2, card2Scale), getMainTranslateFromScale(3, card3Scale) ], extrapolate: 'clamp' }) }, { [secondaryTranslateProp]: animatedValue.interpolate({ inputRange: [0, 1, 2, 3], outputRange: [ 0, getSecondaryTranslateFromScale(1, card1Scale), getSecondaryTranslateFromScale(2, card2Scale), getSecondaryTranslateFromScale(3, card3Scale) ], extrapolate: 'clamp' }) } ] }; }