UNPKG

react-native-reanimated-carousel

Version:

Simple carousel component.fully implemented using Reanimated 2.Infinitely scrolling, very smooth.

307 lines (273 loc) 7.61 kB
import { useMemo } from "react"; import type { TransformsStyle, ViewStyle } from "react-native"; import { Dimensions } from "react-native"; import { Extrapolation, interpolate } from "react-native-reanimated"; import type { CustomConfig, IComputedDirectionTypes } from "../types"; const screen = Dimensions.get("window"); export interface ILayoutConfig { showLength?: number; moveSize?: number; stackInterval?: number; scaleInterval?: number; opacityInterval?: number; rotateZDeg?: number; snapDirection?: "left" | "right"; } export type TStackModeProps = IComputedDirectionTypes<{ /** * Carousel Animated transitions. */ mode?: "horizontal-stack" | "vertical-stack"; /** * Stack animation style. * @default * snapDirection: 'right', * moveSize: window.width, * stackInterval: 30, * scaleInterval: 0.08, * rotateZDeg: 135, * opacityInterval: 0.1, */ modeConfig?: ILayoutConfig; }>; export function horizontalStackLayout(modeConfig: ILayoutConfig = {}) { return (_value: number) => { "worklet"; const { showLength, snapDirection = "left", moveSize = screen.width, stackInterval = 18, scaleInterval = 0.04, opacityInterval = 0.1, rotateZDeg = 30, } = modeConfig; const { validLength, value, inputRange } = getCommonVariables({ showLength: showLength!, value: _value, snapDirection, }); const { zIndex, opacity } = getCommonStyles({ validLength, value, opacityInterval, snapDirection, }); let translateX: number; let scale: number; let rotateZ: string; if (snapDirection === "left") { translateX = interpolate( value, inputRange, [-moveSize, 0, validLength * stackInterval], Extrapolation.CLAMP ); scale = interpolate( value, inputRange, [1, 1, 1 - validLength * scaleInterval], Extrapolation.CLAMP ); rotateZ = `${interpolate(value, inputRange, [-rotateZDeg, 0, 0], Extrapolation.CLAMP)}deg`; } else if (snapDirection === "right") { translateX = interpolate( value, inputRange, [-validLength * stackInterval, 0, moveSize], Extrapolation.CLAMP ); scale = interpolate( value, inputRange, [1 - validLength * scaleInterval, 1, 1], Extrapolation.CLAMP ); rotateZ = `${interpolate(value, inputRange, [0, 0, rotateZDeg], Extrapolation.CLAMP)}deg`; } const transform: TransformsStyle["transform"] = [ { translateX: translateX!, }, { scale: scale!, }, { rotateZ: rotateZ!, }, ]; const styles: ViewStyle = { transform, zIndex, opacity, }; return styles; }; } export function useHorizontalStackLayout( customAnimationConfig: ILayoutConfig = {}, customConfig: CustomConfig = {} ) { const config = useMemo( () => ({ type: customAnimationConfig.snapDirection === "right" ? "negative" : "positive", viewCount: customAnimationConfig.showLength, ...customConfig, }), [customAnimationConfig, customConfig] ); return { layout: horizontalStackLayout(customAnimationConfig), config, }; } export function verticalStackLayout(modeConfig: ILayoutConfig = {}) { return (_value: number) => { "worklet"; const { showLength, snapDirection = "left", moveSize = screen.width, stackInterval = 18, scaleInterval = 0.04, opacityInterval = 0.1, rotateZDeg = 30, } = modeConfig; const { validLength, value, inputRange } = getCommonVariables({ showLength: showLength!, value: _value, snapDirection, }); const { zIndex, opacity } = getCommonStyles({ validLength, value, opacityInterval, snapDirection, }); let translateX: number; let scale: number; let rotateZ: string; let translateY: number; if (snapDirection === "left") { translateX = interpolate(value, inputRange, [-moveSize, 0, 0], Extrapolation.CLAMP); scale = interpolate( value, inputRange, [1, 1, 1 - validLength * scaleInterval], Extrapolation.CLAMP ); rotateZ = `${interpolate(value, inputRange, [-rotateZDeg, 0, 0], Extrapolation.CLAMP)}deg`; translateY = interpolate( value, inputRange, [0, 0, validLength * stackInterval], Extrapolation.CLAMP ); } else if (snapDirection === "right") { translateX = interpolate(value, inputRange, [0, 0, moveSize], Extrapolation.CLAMP); scale = interpolate( value, inputRange, [1 - validLength * scaleInterval, 1, 1], Extrapolation.CLAMP ); rotateZ = `${interpolate(value, inputRange, [0, 0, rotateZDeg], Extrapolation.CLAMP)}deg`; translateY = interpolate( value, inputRange, [validLength * stackInterval, 0, 0], Extrapolation.CLAMP ); } const transform: TransformsStyle["transform"] = [ { translateX: translateX!, }, { scale: scale!, }, { rotateZ: rotateZ!, }, { translateY: translateY!, }, ]; const styles: ViewStyle = { transform, zIndex, opacity, }; return styles; }; } function getCommonVariables(opts: { value: number; showLength: number; snapDirection: "left" | "right"; }) { "worklet"; const { showLength, value: _value, snapDirection } = opts; function easeInOutCubic(v: number): number { return v < 0.5 ? 4 * v * v * v : 1 - (-2 * v + 2) ** 3 / 2; } const page = Math.floor(Math.abs(_value)); const diff = Math.abs(_value) % 1; const value = _value < 0 ? -(page + easeInOutCubic(diff)) : page + easeInOutCubic(diff); const validLength = showLength! - 1; let inputRange: [number, number, number]; if (snapDirection === "left") inputRange = [-1, 0, validLength]; else if (snapDirection === "right") inputRange = [-validLength, 0, 1]; else throw new Error("snapDirection must be set to either left or right"); return { inputRange, validLength, value, }; } function getCommonStyles(opts: { value: number; validLength: number; opacityInterval: number; snapDirection: "left" | "right"; }) { "worklet"; const { snapDirection, validLength, value, opacityInterval } = opts; let zIndex: number; let opacity: number; if (snapDirection === "left") { zIndex = Math.floor( interpolate( value, [-1.5, -1, -1 + Number.MIN_VALUE, 0, validLength], [Number.MIN_VALUE, validLength, validLength, validLength - 1, -1] ) * 10000 ) / 100; opacity = interpolate( value, [-1, 0, validLength - 1, validLength], [0.25, 1, 1 - (validLength - 1) * opacityInterval, 0.25] ); } else if (snapDirection === "right") { zIndex = Math.floor( interpolate( value, [-validLength, 0, 1 - Number.MIN_VALUE, 1, 1.5], [1, validLength - 1, validLength, validLength, Number.MIN_VALUE] ) * 10000 ) / 100; opacity = interpolate( value, [-validLength, 1 - validLength, 0, 1], [0.25, 1 - (validLength - 1) * opacityInterval, 1, 0.25] ); } else { throw new Error("snapDirection must be set to either left or right"); } return { zIndex: Math.round(zIndex), opacity, }; }