diff --git a/example/src/App.tsx b/example/src/App.tsx index d6eb650d..bfaf631a 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -26,7 +26,6 @@ export default function App() { autoPlay - timingConfig={{ duration: 500 }} autoPlayInterval={2000} ref={r} width={width} @@ -48,7 +47,6 @@ export default function App() { autoPlay - timingConfig={{ duration: 500 }} autoPlayInterval={2000} ref={r} mode="parallax" diff --git a/src/Carousel.tsx b/src/Carousel.tsx index 4f1a098d..c3b038a1 100644 --- a/src/Carousel.tsx +++ b/src/Carousel.tsx @@ -6,11 +6,12 @@ import { PanGestureHandlerGestureEvent, } from 'react-native-gesture-handler'; import Animated, { + cancelAnimation, runOnJS, useAnimatedGestureHandler, useDerivedValue, useSharedValue, - withTiming, + withSpring, } from 'react-native-reanimated'; import { CarouselItem } from './CarouselItem'; import type { TMode } from './layouts'; @@ -19,12 +20,10 @@ import { useCarouselController } from './useCarouselController'; import { useComputedAnim } from './useComputedAnim'; import { useAutoPlay } from './useAutoPlay'; import { useIndexController } from './useIndexController'; -import { useLockController } from './useLock'; -const defaultTimingConfig: Animated.WithTimingConfig = { - duration: 250, +const defaultSpringConfig: Animated.WithSpringConfig = { + damping: 40, }; - export interface ICarouselProps { ref?: React.Ref; /** @@ -87,9 +86,9 @@ export interface ICarouselProps { */ onSnapToItem?: (index: number) => void; /** - * Timing config of translation animated + * Sping config of translation animated */ - timingConfig?: Animated.WithTimingConfig; + springConfig?: Animated.WithSpringConfig; /** * On scroll begin */ @@ -143,21 +142,14 @@ function Carousel( parallaxScrollingScale, onSnapToItem, style, - timingConfig = defaultTimingConfig, panGestureHandlerProps = {}, } = props; - if ( - typeof timingConfig.duration === 'number' && - timingConfig.duration > autoPlayInterval - ) { - throw Error( - 'The during time of animation must less than autoplay interval.' - ); - } - + const timingConfig = { + ...defaultSpringConfig, + ...props.springConfig, + }; const width = Math.round(props.width); - const lockController = useLockController(); const handlerOffsetX = useSharedValue(0); const data = React.useMemo(() => { if (!loop) return _data; @@ -189,8 +181,6 @@ function Carousel( width, handlerOffsetX, indexController, - lockController, - timingConfig, disable: !data.length, onScrollBegin: () => runOnJS(onScrollBegin)(), onScrollEnd: () => runOnJS(onScrollEnd)(), @@ -245,93 +235,75 @@ function Carousel( useAnimatedGestureHandler( { onStart: (_, ctx: any) => { - if (lockController.isLock()) return; runOnJS(pause)(); runOnJS(onScrollBegin)(); - ctx.startContentOffsetX = handlerOffsetX.value; + cancelAnimation(handlerOffsetX); ctx.currentContentOffsetX = handlerOffsetX.value; ctx.start = true; }, onActive: (e, ctx: any) => { - if (lockController.isLock() || !ctx.start) return; - /** - * `onActive` and `onEnd` return different values of translationX!So that creates a bias!TAT - * */ - ctx.translationX = e.translationX; - if (loop) { + const { translationX } = e; + if ( + !loop && + (handlerOffsetX.value >= 0 || + handlerOffsetX.value <= -(data.length - 1) * width) + ) { handlerOffsetX.value = - ctx.currentContentOffsetX + e.translationX; + ctx.currentContentOffsetX + translationX / 2; return; } - handlerOffsetX.value = Math.max( - Math.min(ctx.currentContentOffsetX + e.translationX, 0), - -(data.length - 1) * width - ); + handlerOffsetX.value = + ctx.currentContentOffsetX + translationX; }, - onEnd: (e, ctx: any) => { - if (lockController.isLock() || !ctx.start) return; - const translationX = ctx.translationX; - function _withTimingCallback(num: number) { - return withTiming(num, timingConfig, (isFinished) => { - if (isFinished) { - ctx.start = false; - lockController.unLock(); - runOnJS(onScrollEnd)(); + onEnd: (e) => { + function _withAnimationCallback(num: number) { + return withSpring( + num, + { + ...timingConfig, + velocity: e.velocityX, + }, + (isFinished) => { + if (isFinished) { + runOnJS(onScrollEnd)(); + } } - }); + ); } - if (translationX > 0) { - /** - * If not loop no , longer scroll when sliding to the start. - * */ - if (!loop && handlerOffsetX.value >= 0) { - return; - } - lockController.lock(); - if ( - Math.abs(translationX) + Math.abs(e.velocityX) > - width / 2 - ) { - handlerOffsetX.value = _withTimingCallback( - handlerOffsetX.value + width - translationX - ); - } else { - handlerOffsetX.value = _withTimingCallback( - handlerOffsetX.value - translationX - ); - } + const page = Math.round(handlerOffsetX.value / width); + const velocityPage = Math.round( + (handlerOffsetX.value + e.velocityX) / width + ); + const pageWithVelocity = Math.min( + page + 1, + Math.max(page - 1, velocityPage) + ); + + if (loop) { + handlerOffsetX.value = _withAnimationCallback( + pageWithVelocity * width + ); + return; + } + if (handlerOffsetX.value >= 0) { + handlerOffsetX.value = _withAnimationCallback(0); return; } - if (translationX < 0) { - /** - * If not loop , no longer scroll when sliding to the end. - * */ - if ( - !loop && - handlerOffsetX.value <= -(data.length - 1) * width - ) { - return; - } - lockController.lock(); - if ( - Math.abs(translationX) + Math.abs(e.velocityX) > - width / 2 - ) { - handlerOffsetX.value = _withTimingCallback( - handlerOffsetX.value - width - translationX - ); - } else { - handlerOffsetX.value = _withTimingCallback( - handlerOffsetX.value - translationX - ); - } + if (handlerOffsetX.value <= -(data.length - 1) * width) { + handlerOffsetX.value = _withAnimationCallback( + -(data.length - 1) * width + ); return; } + + handlerOffsetX.value = _withAnimationCallback( + pageWithVelocity * width + ); }, }, - [loop, data, lockController, onScrollBegin, onScrollEnd] + [loop, data, onScrollBegin, onScrollEnd] ); React.useImperativeHandle(ref, () => { diff --git a/src/useCarouselController.tsx b/src/useCarouselController.tsx index 9fa45dd9..2541ae72 100644 --- a/src/useCarouselController.tsx +++ b/src/useCarouselController.tsx @@ -2,15 +2,16 @@ import React from 'react'; import type Animated from 'react-native-reanimated'; import { runOnJS, withTiming } from 'react-native-reanimated'; import type { IIndexController } from './useIndexController'; -import type { ILockController } from './useLock'; + +const defaultTimingConfig: Animated.WithTimingConfig = { + duration: 250, +}; interface IOpts { loop: boolean; width: number; handlerOffsetX: Animated.SharedValue; - lockController: ILockController; indexController: IIndexController; - timingConfig: Animated.WithTimingConfig; disable?: boolean; onScrollBegin?: () => void; onScrollEnd?: () => void; @@ -27,25 +28,21 @@ export function useCarouselController(opts: IOpts): ICarouselController { width, loop, handlerOffsetX, - timingConfig, - lockController, indexController, disable = false, } = opts; const canSliding = React.useCallback(() => { - return !disable && !lockController.isLock(); - }, [lockController, disable]); + return !disable; + }, [disable]); const onScrollEnd = React.useCallback(() => { - lockController.unLock(); opts.onScrollEnd?.(); - }, [lockController, opts]); + }, [opts]); const onScrollBegin = React.useCallback(() => { opts.onScrollBegin?.(); - lockController.lock(); - }, [lockController, opts]); + }, [opts]); const next = React.useCallback(() => { if ( @@ -59,7 +56,7 @@ export function useCarouselController(opts: IOpts): ICarouselController { handlerOffsetX.value = withTiming( handlerOffsetX.value - width, - timingConfig, + defaultTimingConfig, (isFinished: boolean) => { if (isFinished) { runOnJS(onScrollEnd)(); @@ -71,7 +68,6 @@ export function useCarouselController(opts: IOpts): ICarouselController { canSliding, onScrollBegin, width, - timingConfig, handlerOffsetX, indexController, loop, @@ -85,7 +81,7 @@ export function useCarouselController(opts: IOpts): ICarouselController { handlerOffsetX.value = withTiming( handlerOffsetX.value + width, - timingConfig, + defaultTimingConfig, (isFinished: boolean) => { if (isFinished) { runOnJS(onScrollEnd)(); @@ -97,7 +93,6 @@ export function useCarouselController(opts: IOpts): ICarouselController { canSliding, onScrollBegin, width, - timingConfig, handlerOffsetX, indexController, loop, @@ -117,7 +112,7 @@ export function useCarouselController(opts: IOpts): ICarouselController { if (animated) { handlerOffsetX.value = withTiming( offset, - timingConfig, + defaultTimingConfig, (isFinished: boolean) => { indexController.index.value = index; if (isFinished) { @@ -136,7 +131,6 @@ export function useCarouselController(opts: IOpts): ICarouselController { onScrollBegin, onScrollEnd, width, - timingConfig, indexController, handlerOffsetX, ] diff --git a/src/useLock.ts b/src/useLock.ts deleted file mode 100644 index 4c80dc15..00000000 --- a/src/useLock.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useSharedValue } from 'react-native-reanimated'; -export interface ILockController { - lock(): void; - unLock(): void; - isLock(): boolean; -} - -/** - * Cannot operate while animation is locking - */ -export function useLockController(): ILockController { - // This value is true if the animation is executing - const _lock = useSharedValue(false); - function lock() { - 'worklet'; - _lock.value = true; - } - function unLock() { - 'worklet'; - _lock.value = false; - } - function isLock() { - 'worklet'; - return _lock.value; - } - return { - lock, - unLock, - isLock, - }; -}