From 6ae05fcaa382c4b406d135c66bebdaa614c07b67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=9D=B1=E6=BE=94?= Date: Wed, 8 Sep 2021 15:47:22 +0800 Subject: [PATCH] feat: add onSnapToItem props,should get current item info --- README.md | 8 +++ example/src/App.tsx | 13 +++-- src/Carousel.tsx | 91 +++++++++++++++++++++++++---------- src/useCarouselController.tsx | 51 +++++++++++++------- src/useComputedIndex.ts | 22 +++++++++ src/useLoop.ts | 32 ++++++++++++ 6 files changed, 172 insertions(+), 45 deletions(-) create mode 100644 src/useComputedIndex.ts create mode 100644 src/useLoop.ts diff --git a/README.md b/README.md index da0cb6c6..fb5c14ae 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,14 @@ import Carousel from "react-native-reanimated-carousel"; | style | false | {} | ViewStyle | Carousel container style | | height | false | '100%' | undefined \| string \| number | Specified carousel container height | +## Ref + +| name | types | description | +| --------------- | ---------- | ---------------------- | +| prev | ()=>void | Play the last one | +| loop | ()=>void | Play the next one | +| getCurrentIndex | ()=>number | Get current item index | + ## Contributing See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. diff --git a/example/src/App.tsx b/example/src/App.tsx index 6a43522d..75bd9ef2 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,6 +1,6 @@ /* eslint-disable react-native/no-inline-styles */ import * as React from 'react'; -import { Button, Dimensions, View } from 'react-native'; +import { Button, Dimensions, Text, View } from 'react-native'; import { TouchableWithoutFeedback } from 'react-native-gesture-handler'; import { ICarouselInstance } from '../../src/Carousel'; @@ -31,7 +31,10 @@ export default function App() { { color: 'yellow' }, ]} parallaxScrollingScale={0.8} - renderItem={({ color }) => { + onSnapToItem={(index) => { + console.log('current index:', index); + }} + renderItem={({ color }, index) => { return ( + > + {index} + ); diff --git a/src/Carousel.tsx b/src/Carousel.tsx index 20bc8cb4..19ef6683 100644 --- a/src/Carousel.tsx +++ b/src/Carousel.tsx @@ -7,6 +7,7 @@ import { import Animated, { runOnJS, useAnimatedGestureHandler, + useAnimatedReaction, useDerivedValue, useSharedValue, withTiming, @@ -17,6 +18,8 @@ import type { TMode } from './layouts'; import { ParallaxLayout } from './layouts/index'; import { useCarouselController } from './useCarouselController'; import { useComputedAnim } from './useComputedAnim'; +import { useLoop } from './useLoop'; +import { useComputedIndex } from './useComputedIndex'; export const _withTiming = ( num: number, @@ -91,11 +94,25 @@ export interface ICarouselProps { * @default 0.8 */ parallaxScrollingScale?: number; + /** + * Callback fired when navigating to an item + */ + onSnapToItem?: (index: number) => void; } export interface ICarouselInstance { + /** + * Play the last one + */ prev: () => void; + /** + * Play the next one + */ next: () => void; + /** + * Get current item index + */ + getCurrentIndex: () => number; } function Carousel( @@ -109,15 +126,15 @@ function Carousel( loop = true, mode = 'default', renderItem, - autoPlay = false, - autoPlayReverse = false, - autoPlayInterval = 1000, + autoPlay, + autoPlayReverse, + autoPlayInterval, parallaxScrollingOffset, parallaxScrollingScale, + onSnapToItem, style, } = props; const handlerOffsetX = useSharedValue(0); - const timer = React.useRef(); const data = React.useMemo(() => { if (_data.length === 1) { return [_data[0], _data[0], _data[0]]; @@ -129,14 +146,47 @@ function Carousel( }, [_data]); const computedAnimResult = useComputedAnim(width, data.length); - - const { next, prev } = useCarouselController({ width, handlerOffsetX }); + const carouselController = useCarouselController({ width, handlerOffsetX }); + useLoop({ + autoPlay, + autoPlayInterval, + autoPlayReverse, + carouselController, + }); + const { index, computedIndex } = useComputedIndex({ + length: data.length, + handlerOffsetX, + width, + }); const offsetX = useDerivedValue(() => { const x = handlerOffsetX.value % computedAnimResult.WL; return isNaN(x) ? 0 : x; }, [computedAnimResult]); + useAnimatedReaction( + () => index.value, + (i) => onSnapToItem && runOnJS(onSnapToItem)(i), + [onSnapToItem] + ); + + const callComputedIndex = React.useCallback( + (isFinished: boolean) => isFinished && computedIndex?.(), + [computedIndex] + ); + + const next = React.useCallback(() => { + return carouselController.next(callComputedIndex); + }, [carouselController, callComputedIndex]); + + const prev = React.useCallback(() => { + return carouselController.prev(callComputedIndex); + }, [carouselController, callComputedIndex]); + + const getCurrentIndex = React.useCallback(() => { + return index.value; + }, [index]); + const animatedListScrollHandler = useAnimatedGestureHandler( { @@ -153,7 +203,7 @@ function Carousel( handlerOffsetX.value = Math.max( Math.min( ctx.startContentOffsetX + - Math.round(e.translationX), + Math.round(e.translationX), 0 ), -(data.length - 1) * width @@ -163,20 +213,24 @@ function Carousel( const intTranslationX = Math.round(e.translationX); const sub = Math.abs(intTranslationX); + function _withTimingCallback(num: number) { + return _withTiming(num, callComputedIndex); + } + if (intTranslationX > 0) { if (!loop && handlerOffsetX.value >= 0) { return; } if (sub > width / 2) { - handlerOffsetX.value = _withTiming( + handlerOffsetX.value = _withTimingCallback( fillNum( width, handlerOffsetX.value + (width - sub) ) ); } else { - handlerOffsetX.value = _withTiming( + handlerOffsetX.value = _withTimingCallback( fillNum(width, handlerOffsetX.value - sub) ); } @@ -192,14 +246,14 @@ function Carousel( } if (sub > width / 2) { - handlerOffsetX.value = _withTiming( + handlerOffsetX.value = _withTimingCallback( fillNum( width, handlerOffsetX.value - (width - sub) ) ); } else { - handlerOffsetX.value = _withTiming( + handlerOffsetX.value = _withTimingCallback( fillNum(width, handlerOffsetX.value + sub) ); } @@ -214,23 +268,10 @@ function Carousel( return { next, prev, + getCurrentIndex, }; }); - React.useEffect(() => { - if (timer.current) { - clearInterval(timer.current); - } - if (autoPlay) { - timer.current = setInterval(() => { - autoPlayReverse ? prev() : next(); - }, autoPlayInterval); - } - return () => { - !!timer.current && clearInterval(timer.current); - }; - }, [autoPlay, autoPlayReverse, autoPlayInterval, prev, next]); - const Layouts = React.useMemo>(() => { switch (mode) { case 'parallax': diff --git a/src/useCarouselController.tsx b/src/useCarouselController.tsx index b69e4b59..a5fe1e4a 100644 --- a/src/useCarouselController.tsx +++ b/src/useCarouselController.tsx @@ -8,7 +8,12 @@ interface IOpts { handlerOffsetX: Animated.SharedValue; } -export function useCarouselController(opts: IOpts) { +export interface ICarouselController { + prev: (callback?: (isFinished: boolean) => void) => void; + next: (callback?: (isFinished: boolean) => void) => void; +} + +export function useCarouselController(opts: IOpts): ICarouselController { const lock = useSharedValue(false); const { width, handlerOffsetX } = opts; @@ -24,23 +29,35 @@ export function useCarouselController(opts: IOpts) { lock.value = true; }, [lock]); - const next = React.useCallback(() => { - if (lock.value) return; - openLock(); - handlerOffsetX.value = _withTiming( - handlerOffsetX.value - width, - closeLock - ); - }, [width, openLock, closeLock, lock, handlerOffsetX]); + const next = React.useCallback( + (callback?: (isFinished: boolean) => void) => { + if (lock.value) return; + openLock(); + handlerOffsetX.value = _withTiming( + handlerOffsetX.value - width, + (isFinished: boolean) => { + callback?.(isFinished); + closeLock(isFinished); + } + ); + }, + [width, openLock, closeLock, lock, handlerOffsetX] + ); - const prev = React.useCallback(() => { - if (lock.value) return; - openLock(); - handlerOffsetX.value = _withTiming( - handlerOffsetX.value + width, - closeLock - ); - }, [width, openLock, closeLock, lock, handlerOffsetX]); + const prev = React.useCallback( + (callback?: (isFinished: boolean) => void) => { + if (lock.value) return; + openLock(); + handlerOffsetX.value = _withTiming( + handlerOffsetX.value + width, + (isFinished: boolean) => { + callback?.(isFinished); + closeLock(isFinished); + } + ); + }, + [width, openLock, closeLock, lock, handlerOffsetX] + ); return { next, diff --git a/src/useComputedIndex.ts b/src/useComputedIndex.ts new file mode 100644 index 00000000..32e94542 --- /dev/null +++ b/src/useComputedIndex.ts @@ -0,0 +1,22 @@ +import * as React from 'react'; +import Animated, { useSharedValue } from 'react-native-reanimated'; + +export function useComputedIndex(opts: { + handlerOffsetX: Animated.SharedValue; + length: number; + width: number; +}) { + const { length, width, handlerOffsetX } = opts; + const index = useSharedValue(0); + + const computedIndex = React.useCallback(() => { + const toInt = (handlerOffsetX.value / width) % length; + const i = + handlerOffsetX.value <= 0 + ? Math.abs(toInt) + : Math.abs(toInt > 0 ? length - toInt : 0); + index.value = i; + }, [length, handlerOffsetX, index, width]); + + return { index, computedIndex }; +} diff --git a/src/useLoop.ts b/src/useLoop.ts new file mode 100644 index 00000000..cebdab13 --- /dev/null +++ b/src/useLoop.ts @@ -0,0 +1,32 @@ +import * as React from 'react'; +import type { ICarouselController } from './useCarouselController'; + +export function useLoop(opts: { + autoPlay?: boolean; + autoPlayInterval?: number; + autoPlayReverse?: boolean; + carouselController: ICarouselController; +}) { + const { + autoPlay = false, + autoPlayReverse = false, + autoPlayInterval = 1000, + carouselController, + } = opts; + const timer = React.useRef(); + React.useEffect(() => { + if (timer.current) { + clearInterval(timer.current); + } + if (autoPlay) { + timer.current = setInterval(() => { + autoPlayReverse + ? carouselController.prev() + : carouselController.next(); + }, autoPlayInterval); + } + return () => { + !!timer.current && clearInterval(timer.current); + }; + }, [autoPlay, autoPlayReverse, autoPlayInterval, carouselController]); +}