From 9b03a4d7f53bf22c0ee78dc91ec74f42b865357b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=9D=B1=E6=BE=94?= Date: Wed, 4 May 2022 19:32:43 +0800 Subject: [PATCH] fix: getCurrentIndex always return last snap index even after multiple data changes fix #178 --- example/src/normal/index.tsx | 26 +++++- src/Carousel.tsx | 70 +++++++++------- src/ScrollViewGesture.tsx | 16 ++-- src/hooks/useAutoPlay.ts | 7 +- src/hooks/useCarouselController.tsx | 115 ++++++++++++-------------- src/utils/computedWithAutoFillData.ts | 18 ++++ 6 files changed, 147 insertions(+), 105 deletions(-) diff --git a/example/src/normal/index.tsx b/example/src/normal/index.tsx index b87a275e..85ac99ed 100644 --- a/example/src/normal/index.tsx +++ b/example/src/normal/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { View } from 'react-native-ui-lib'; -import Carousel from 'react-native-reanimated-carousel'; +import Carousel, { ICarouselInstance } from 'react-native-reanimated-carousel'; import { SBItem } from '../components/SBItem'; import SButton from '../components/SButton'; import { ElementsText, window } from '../constants'; @@ -8,9 +8,11 @@ import { ElementsText, window } from '../constants'; const PAGE_WIDTH = window.width; function Index() { + const [data, setData] = React.useState([...new Array(6).keys()]); const [isVertical, setIsVertical] = React.useState(false); const [isFast, setIsFast] = React.useState(false); const [isAutoPlay, setIsAutoPlay] = React.useState(false); + const ref = React.useRef(null); const baseOptions = isVertical ? ({ @@ -29,9 +31,11 @@ function Index() { console.log('current index:', index)} renderItem={({ index }) => } /> {ElementsText.AUTOPLAY}:{`${isAutoPlay}`} + { + console.log(ref.current?.getCurrentIndex()); + }} + > + Log current index + + { + setData( + data.length === 6 + ? [...new Array(8).keys()] + : [...new Array(6).keys()] + ); + }} + > + Change data length to:{data.length === 6 ? 8 : 6} + ); } diff --git a/src/Carousel.tsx b/src/Carousel.tsx index 4c98acfb..d92397f4 100644 --- a/src/Carousel.tsx +++ b/src/Carousel.tsx @@ -1,5 +1,9 @@ import React from 'react'; -import Animated, { runOnJS, useDerivedValue } from 'react-native-reanimated'; +import Animated, { + runOnJS, + runOnUI, + useDerivedValue, +} from 'react-native-reanimated'; import { useCarouselController } from './hooks/useCarouselController'; import { useAutoPlay } from './hooks/useAutoPlay'; @@ -74,28 +78,27 @@ const Carousel = React.forwardRef>( const carouselController = useCarouselController({ loop, size, + data, + autoFillData, handlerOffsetX, - length: data.length, - disable: !data.length, withAnimation, - originalLength: data.length, defaultIndex, onScrollEnd: () => runOnJS(_onScrollEnd)(), onScrollBegin: () => !!onScrollBegin && runOnJS(onScrollBegin)(), - onChange: (i) => !!onSnapToItem && runOnJS(onSnapToItem)(i), duration: scrollAnimationDuration, }); const { + sharedIndex, + sharedPreIndex, + to, next, prev, - sharedPreIndex, - sharedIndex, - computedIndex, + scrollTo, getCurrentIndex, } = carouselController; - const { start, pause } = useAutoPlay({ + const { start: startAutoPlay, pause: pauseAutoPlay } = useAutoPlay({ autoPlay, autoPlayInterval, autoPlayReverse, @@ -103,29 +106,44 @@ const Carousel = React.forwardRef>( }); const _onScrollEnd = React.useCallback(() => { - computedIndex(); - onScrollEnd?.(sharedPreIndex.current, sharedIndex.current); - }, [sharedPreIndex, sharedIndex, computedIndex, onScrollEnd]); + 'worklet'; + const _sharedIndex = Math.round(sharedIndex.value); + const _sharedPreIndex = Math.round(sharedPreIndex.value); + + if (onSnapToItem) { + runOnJS(onSnapToItem)(_sharedIndex); + } + if (onScrollEnd) { + runOnJS(onScrollEnd)(_sharedPreIndex, _sharedIndex); + } + }, [onSnapToItem, onScrollEnd, sharedIndex, sharedPreIndex]); const scrollViewGestureOnScrollBegin = React.useCallback(() => { - pause(); + pauseAutoPlay(); onScrollBegin?.(); - }, [onScrollBegin, pause]); + }, [onScrollBegin, pauseAutoPlay]); const scrollViewGestureOnScrollEnd = React.useCallback(() => { - start(); - _onScrollEnd(); - }, [_onScrollEnd, start]); + startAutoPlay(); + /** + * TODO magic + */ + runOnUI(_onScrollEnd)(); + }, [_onScrollEnd, startAutoPlay]); - const scrollViewGestureOnTouchBegin = React.useCallback(pause, [pause]); + const scrollViewGestureOnTouchBegin = React.useCallback(pauseAutoPlay, [ + pauseAutoPlay, + ]); - const scrollViewGestureOnTouchEnd = React.useCallback(start, [start]); + const scrollViewGestureOnTouchEnd = React.useCallback(startAutoPlay, [ + startAutoPlay, + ]); const goToIndex = React.useCallback( (i: number, animated?: boolean) => { - carouselController.to(i, animated); + to(i, animated); }, - [carouselController] + [to] ); React.useImperativeHandle( @@ -135,15 +153,9 @@ const Carousel = React.forwardRef>( prev, getCurrentIndex, goToIndex, - scrollTo: carouselController.scrollTo, + scrollTo, }), - [ - getCurrentIndex, - goToIndex, - next, - prev, - carouselController.scrollTo, - ] + [getCurrentIndex, goToIndex, next, prev, scrollTo] ); const visibleRanges = useVisibleRanges({ diff --git a/src/ScrollViewGesture.tsx b/src/ScrollViewGesture.tsx index e84c4d86..f4124ce1 100644 --- a/src/ScrollViewGesture.tsx +++ b/src/ScrollViewGesture.tsx @@ -68,24 +68,22 @@ const IScrollViewGesture: React.FC = (props) => { const _withSpring = React.useCallback( (toValue: number, onFinished?: () => void) => { 'worklet'; - const callback = (isFinished: boolean) => { - 'worklet'; - if (isFinished) { - onFinished && runOnJS(onFinished)(); - } - }; - const defaultWithAnimation: WithTimingAnimation = { type: 'timing', config: { - duration: scrollAnimationDuration, + duration: scrollAnimationDuration + 100, easing: Easing.easeOutQuart, }, }; return dealWithAnimation(withAnimation ?? defaultWithAnimation)( toValue, - callback + (isFinished: boolean) => { + 'worklet'; + if (isFinished) { + onFinished && runOnJS(onFinished)(); + } + } ); }, [scrollAnimationDuration, withAnimation] diff --git a/src/hooks/useAutoPlay.ts b/src/hooks/useAutoPlay.ts index 153f65d9..fa0159da 100644 --- a/src/hooks/useAutoPlay.ts +++ b/src/hooks/useAutoPlay.ts @@ -14,6 +14,7 @@ export function useAutoPlay(opts: { carouselController, } = opts; + const { prev, next } = carouselController; const timer = React.useRef(); const stopped = React.useRef(!autoPlay); @@ -25,10 +26,10 @@ export function useAutoPlay(opts: { timer.current && clearTimeout(timer.current); timer.current = setTimeout(() => { autoPlayReverse - ? carouselController.prev({ onFinished: play }) - : carouselController.next({ onFinished: play }); + ? prev({ onFinished: play }) + : next({ onFinished: play }); }, autoPlayInterval); - }, [autoPlayReverse, autoPlayInterval, carouselController]); + }, [autoPlayReverse, autoPlayInterval, prev, next]); const pause = React.useCallback(() => { if (!autoPlay) { diff --git a/src/hooks/useCarouselController.tsx b/src/hooks/useCarouselController.tsx index 163ceafd..95aa4bbb 100644 --- a/src/hooks/useCarouselController.tsx +++ b/src/hooks/useCarouselController.tsx @@ -1,38 +1,37 @@ import React from 'react'; import type Animated from 'react-native-reanimated'; import { Easing } from '../constants'; -import { runOnJS, useSharedValue } from 'react-native-reanimated'; +import { + runOnJS, + useAnimatedReaction, + useSharedValue, +} from 'react-native-reanimated'; import type { TCarouselActionOptions, TCarouselProps, WithTimingAnimation, } from '../types'; import { dealWithAnimation } from '@/utils/dealWithAnimation'; +import { convertToSharedIndex } from '@/utils/computedWithAutoFillData'; interface IOpts { loop: boolean; size: number; + data: TCarouselProps['data']; + autoFillData: TCarouselProps['autoFillData']; handlerOffsetX: Animated.SharedValue; withAnimation?: TCarouselProps['withAnimation']; - disable?: boolean; duration?: number; - originalLength: number; - length: number; defaultIndex?: number; onScrollBegin?: () => void; onScrollEnd?: () => void; - // the length before fill data - onChange: (index: number) => void; } export interface ICarouselController { - length: number; - index: Animated.SharedValue; - sharedIndex: React.MutableRefObject; - sharedPreIndex: React.MutableRefObject; + sharedIndex: Animated.SharedValue; + sharedPreIndex: Animated.SharedValue; prev: (opts?: TCarouselActionOptions) => void; next: (opts?: TCarouselActionOptions) => void; - computedIndex: () => void; getCurrentIndex: () => number; to: (index: number, animated?: boolean) => void; scrollTo: (opts?: TCarouselActionOptions) => void; @@ -41,70 +40,64 @@ export interface ICarouselController { export function useCarouselController(options: IOpts): ICarouselController { const { size, + data, loop, handlerOffsetX, withAnimation, - disable = false, - originalLength, - length, - onChange, - duration, defaultIndex = 0, + duration, + autoFillData, } = options; + const dataInfo = React.useMemo( + () => ({ + length: data.length, + disable: !data.length, + originalLength: data.length, + }), + [data] + ); + const index = useSharedValue(defaultIndex); // The Index displayed to the user - const sharedIndex = React.useRef(defaultIndex); - const sharedPreIndex = React.useRef(defaultIndex); + const sharedIndex = useSharedValue(defaultIndex); + const sharedPreIndex = useSharedValue(defaultIndex); const currentFixedPage = React.useCallback(() => { if (loop) { return -Math.round(handlerOffsetX.value / size); } - const fixed = (handlerOffsetX.value / size) % length; + const fixed = (handlerOffsetX.value / size) % dataInfo.length; return Math.round( handlerOffsetX.value <= 0 ? Math.abs(fixed) - : Math.abs(fixed > 0 ? length - fixed : 0) + : Math.abs(fixed > 0 ? dataInfo.length - fixed : 0) ); - }, [handlerOffsetX, length, size, loop]); + }, [handlerOffsetX, dataInfo, size, loop]); - const convertToSharedIndex = React.useCallback( - (i: number) => { - if (loop) { - switch (originalLength) { - case 1: - return 0; - case 2: - return i % 2; - } - } - return i; + const computedIndex = React.useCallback( + (handlerOffsetXValue: number) => { + 'worklet'; + sharedPreIndex.value = sharedIndex.value; + const toInt = (handlerOffsetXValue / size) % dataInfo.length; + const isPositive = handlerOffsetXValue <= 0; + const i = isPositive + ? Math.abs(toInt) + : Math.abs(toInt > 0 ? dataInfo.length - toInt : 0); + index.value = i; + sharedIndex.value = convertToSharedIndex({ + loop, + rawDataLength: dataInfo.originalLength, + autoFillData: autoFillData!, + index: i, + }); }, - [originalLength, loop] + [sharedPreIndex, sharedIndex, size, dataInfo, index, loop, autoFillData] ); - const computedIndex = React.useCallback(() => { - sharedPreIndex.current = sharedIndex.current; - const toInt = (handlerOffsetX.value / size) % length; - const i = - handlerOffsetX.value <= 0 - ? Math.abs(toInt) - : Math.abs(toInt > 0 ? length - toInt : 0); - index.value = i; - const _sharedIndex = convertToSharedIndex(i); - sharedIndex.current = _sharedIndex; - onChange(_sharedIndex); - }, [ - length, + useAnimatedReaction(() => handlerOffsetX.value, computedIndex, [ handlerOffsetX, - sharedPreIndex, - index, - size, - sharedIndex, - convertToSharedIndex, - onChange, ]); const getCurrentIndex = React.useCallback(() => { @@ -112,8 +105,8 @@ export function useCarouselController(options: IOpts): ICarouselController { }, [index]); const canSliding = React.useCallback(() => { - return !disable; - }, [disable]); + return !dataInfo.disable; + }, [dataInfo]); const onScrollEnd = React.useCallback(() => { options.onScrollEnd?.(); @@ -151,7 +144,8 @@ export function useCarouselController(options: IOpts): ICarouselController { (opts: TCarouselActionOptions = {}) => { 'worklet'; const { count = 1, animated = true, onFinished } = opts; - if (!canSliding() || (!loop && index.value >= length - 1)) return; + if (!canSliding() || (!loop && index.value >= dataInfo.length - 1)) + return; onScrollBegin?.(); @@ -172,7 +166,7 @@ export function useCarouselController(options: IOpts): ICarouselController { canSliding, loop, index, - length, + dataInfo, onScrollBegin, handlerOffsetX, size, @@ -259,15 +253,12 @@ export function useCarouselController(options: IOpts): ICarouselController { ); return { + sharedIndex, + sharedPreIndex, + to, next, prev, - to, scrollTo, - index, - length, - sharedIndex, - sharedPreIndex, - computedIndex, getCurrentIndex, }; } diff --git a/src/utils/computedWithAutoFillData.ts b/src/utils/computedWithAutoFillData.ts index 49c9f15f..1f756b4e 100644 --- a/src/utils/computedWithAutoFillData.ts +++ b/src/utils/computedWithAutoFillData.ts @@ -12,6 +12,24 @@ type BaseParams = { loop: boolean; } & T; +export function convertToSharedIndex( + params: BaseParams<{ index: number; rawDataLength: number }> +) { + 'worklet'; + const { loop, rawDataLength, index, autoFillData } = params; + + if (isAutoFillData({ loop, autoFillData })) { + switch (rawDataLength) { + case SINGLE_ITEM: + return 0; + case DOUBLE_ITEM: + return index % 2; + } + } + + return index; +} + export function computedOffsetXValueWithAutoFillData( params: BaseParams<{ rawDataLength: number;