From 2a3934647ca6558fdd1e213d79f5e64baaff3984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=9D=B1=E6=BE=94?= Date: Wed, 19 Oct 2022 13:34:44 +0800 Subject: [PATCH 1/2] feat: add new props to control or get information for carousel animation re #285 --- docs/about.md | 6 ++-- docs/about.zh-CN.md | 6 ++-- docs/props.md | 3 +- docs/props.zh-CN.md | 3 +- src/Carousel.tsx | 14 ++++---- src/hooks/useCarouselController.tsx | 51 ++++++++++++++--------------- src/hooks/useCommonVariables.ts | 22 +++++++++---- src/hooks/useOffsetX.ts | 8 ++--- src/layouts/BaseLayout.tsx | 8 ++--- src/layouts/ParallaxLayout.tsx | 6 ++-- src/types.ts | 7 +++- 11 files changed, 73 insertions(+), 61 deletions(-) diff --git a/docs/about.md b/docs/about.md index 3f03c03e..fdade290 100644 --- a/docs/about.md +++ b/docs/about.md @@ -21,7 +21,7 @@ This is how it works in code. 1. First we need a unit `size` to help us calculate the scroll distance. When horizontal, `size` is equal to the `width` prop setting, when vertical, `size` is equal to the `height` prop setting. -2. Then we need a value `handlerOffsetX`, which is the current scroll distance, and it's a total value, if we scroll two, `handlerOffsetX` is equal to `size` * 2, if we scroll ten, `handlerOffsetX` is equal to `size` * 10. +2. Then we need a value `handlerOffset`, which is the current scroll distance, and it's a total value, if we scroll two, `handlerOffset` is equal to `size` * 2, if we scroll ten, `handlerOffset` is equal to `size` * 10. 3. Followed by dealing with how to get at the end of the element at the right time to move to the front, this part of logic in `./src/hooks/useOffsetX.ts`. First we need to know the current window size (the total number of elements rendered on one side). The window size defaults to half the total number of elements, i.e. full render. ![steps-6](./assets/steps-6.jpg) @@ -56,7 +56,7 @@ The above logic is translated into code as follows: startPos, ]; return interpolate( - handlerOffsetX.value, + handlerOffset.value, inputRange, outputRange, Extrapolate.CLAMP @@ -69,7 +69,7 @@ const inputRange = [-1, 0 ,1] const outputRange = [-size, 0 ,size] return { transform: [ - { translateX: interpolate(handlerOffsetX.value, inputRange, outputRange) }, + { translateX: interpolate(handlerOffset.value, inputRange, outputRange) }, ], } ``` diff --git a/docs/about.zh-CN.md b/docs/about.zh-CN.md index 142c9332..73301e38 100644 --- a/docs/about.zh-CN.md +++ b/docs/about.zh-CN.md @@ -21,7 +21,7 @@ 1. 首先我们需要一个单位`size`,它用来帮我们计算滚动距离,当水平时`size`等于`width` prop的设置,当垂直时`size`等于`height` prop的设置。 -2. 其次我们需要一个值`handlerOffsetX`,用来记录当前的滚动距离,这是一个总值,当我们滚动两张,那`handlerOffsetX`等于size * 2,如果滚动十张那`handlerOffsetX`等于size * 10。 +2. 其次我们需要一个值`handlerOffset`,用来记录当前的滚动距离,这是一个总值,当我们滚动两张,那`handlerOffset`等于size * 2,如果滚动十张那`handlerOffset`等于size * 10。 3. 紧接着是处理如何让末尾的元素在合适的时候挪动到最前面,这部分逻辑在`./src/hooks/useOffsetX.ts`中。首先我们需要知道目前的视窗大小(一侧元素渲染的总数量),视窗大小默认为元素总数量的一半,即全量渲染。 ![steps-6](./assets/steps-6.jpg) @@ -56,7 +56,7 @@ startPos, ]; return interpolate( - handlerOffsetX.value, + handlerOffset.value, inputRange, outputRange, Extrapolate.CLAMP @@ -69,7 +69,7 @@ const inputRange = [-1, 0 ,1] const outputRange = [-size, 0 ,size] return { transform: [ - { translateX: interpolate(handlerOffsetX.value, inputRange, outputRange) }, + { translateX: interpolate(handlerOffset.value, inputRange, outputRange) }, ], } ``` diff --git a/docs/props.md b/docs/props.md index bd22c90f..0ce49338 100644 --- a/docs/props.md +++ b/docs/props.md @@ -3,7 +3,8 @@ | name | required | default | types | description | | ----------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | | data | ✅ | | T[] | Carousel items data set | -| renderItem | ✅ | | (info: { data: T, index: number, animationValue: SharedValue\ }) => React.ReactElement | Render carousel item | +| renderItem | ✅ | | (info: { item: T, index: number, animationValue: SharedValue\ }) => React.ReactElement | Render carousel item | +| defaultScrollOffsetValue| ❌ | useSharedValue(0) | boolean | The default animated value of the carousel. | | autoFillData | ❌ | true | boolean | Auto fill data array to allow loop playback when the loop props is true.([1] => [1, 1, 1];[1, 2] => [1, 2, 1, 2]) | | vertical | ❌ | false | boolean | Layout items vertically instead of horizontally | | width | vertical ❌ horizontal ✅ | '100%' | number \| undefined | Specified carousel item width | diff --git a/docs/props.zh-CN.md b/docs/props.zh-CN.md index 2a8e3de1..5e3e2ac3 100644 --- a/docs/props.zh-CN.md +++ b/docs/props.zh-CN.md @@ -3,7 +3,8 @@ | name | required | default | types | description | | ----------------------- | ------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | | data | ✅ | | T[] | 即将渲染的数据集合 | -| renderItem | ✅ | | (info: { data: T, index: number, animationValue: SharedValue\ }) => React.ReactElement | 渲染元素的方法 | +| renderItem | ✅ | | (info: { item: T, index: number, animationValue: SharedValue\ }) => React.ReactElement | 渲染元素的方法 | +| defaultScrollOffsetValue| ❌ | useSharedValue(0) | boolean | 轮播图的默认动画值 | | autoFillData | ❌ | true | boolean | 将会在`loop`属性设置为 true 时,自动填充 data 元素以满足 loop 循环效果([1] => [1, 1, 1];[1, 2] => [1, 2, 1, 2]) | | vertical | ❌ | false | boolean | 将元素垂直布局而不是水平 | | width | 垂直时 ❌ 水平时 ✅ | '100%' | number \| undefined | 指定每一项的宽度 | diff --git a/src/Carousel.tsx b/src/Carousel.tsx index fee411e5..f9dc2374 100644 --- a/src/Carousel.tsx +++ b/src/Carousel.tsx @@ -48,15 +48,15 @@ const Carousel = React.forwardRef>( } = props; const commonVariables = useCommonVariables(props); - const { size, handlerOffsetX } = commonVariables; + const { size, handlerOffset } = commonVariables; const dataLength = data.length; const offsetX = useDerivedValue(() => { const totalSize = size * dataLength; - const x = handlerOffsetX.value % totalSize; + const x = handlerOffset.value % totalSize; if (!loop) { - return handlerOffsetX.value; + return handlerOffset.value; } return isNaN(x) ? 0 : x; }, [loop, size, dataLength]); @@ -76,7 +76,7 @@ const Carousel = React.forwardRef>( size, data, autoFillData, - handlerOffsetX, + handlerOffset, withAnimation, defaultIndex, onScrollEnd: () => runOnJS(_onScrollEnd)(), @@ -151,7 +151,7 @@ const Carousel = React.forwardRef>( const visibleRanges = useVisibleRanges({ total: data.length, viewSize: size, - translation: handlerOffsetX, + translation: handlerOffset, windowSize, }); @@ -170,7 +170,7 @@ const Carousel = React.forwardRef>( @@ -201,7 +201,7 @@ const Carousel = React.forwardRef>( ; + handlerOffset: Animated.SharedValue; withAnimation?: TCarouselProps['withAnimation']; duration?: number; defaultIndex?: number; @@ -41,7 +41,7 @@ export function useCarouselController(options: IOpts): ICarouselController { size, data, loop, - handlerOffsetX, + handlerOffset, withAnimation, defaultIndex = 0, duration, @@ -64,16 +64,16 @@ export function useCarouselController(options: IOpts): ICarouselController { const currentFixedPage = React.useCallback(() => { if (loop) { - return -Math.round(handlerOffsetX.value / size); + return -Math.round(handlerOffset.value / size); } - const fixed = (handlerOffsetX.value / size) % dataInfo.length; + const fixed = (handlerOffset.value / size) % dataInfo.length; return Math.round( - handlerOffsetX.value <= 0 + handlerOffset.value <= 0 ? Math.abs(fixed) : Math.abs(fixed > 0 ? dataInfo.length - fixed : 0) ); - }, [handlerOffsetX, dataInfo, size, loop]); + }, [handlerOffset, dataInfo, size, loop]); function setSharedIndex(newSharedIndex: number) { sharedIndex.current = newSharedIndex; @@ -81,9 +81,9 @@ export function useCarouselController(options: IOpts): ICarouselController { useAnimatedReaction( () => { - const handlerOffsetXValue = handlerOffsetX.value; - const toInt = round(handlerOffsetXValue / size) % dataInfo.length; - const isPositive = handlerOffsetXValue <= 0; + const handlerOffsetValue = handlerOffset.value; + const toInt = round(handlerOffsetValue / size) % dataInfo.length; + const isPositive = handlerOffsetValue <= 0; const i = isPositive ? Math.abs(toInt) : Math.abs(toInt > 0 ? dataInfo.length - toInt : 0); @@ -112,7 +112,7 @@ export function useCarouselController(options: IOpts): ICarouselController { index, loop, autoFillData, - handlerOffsetX, + handlerOffset, ] ); @@ -169,12 +169,12 @@ export function useCarouselController(options: IOpts): ICarouselController { index.value = nextPage; if (animated) { - handlerOffsetX.value = scrollWithTiming( + handlerOffset.value = scrollWithTiming( -nextPage * size, onFinished ) as any; } else { - handlerOffsetX.value = -nextPage * size; + handlerOffset.value = -nextPage * size; onFinished?.(); } }, @@ -184,7 +184,7 @@ export function useCarouselController(options: IOpts): ICarouselController { index, dataInfo, onScrollBegin, - handlerOffsetX, + handlerOffset, size, scrollWithTiming, currentFixedPage, @@ -202,12 +202,12 @@ export function useCarouselController(options: IOpts): ICarouselController { index.value = prevPage; if (animated) { - handlerOffsetX.value = scrollWithTiming( + handlerOffset.value = scrollWithTiming( -prevPage * size, onFinished ); } else { - handlerOffsetX.value = -prevPage * size; + handlerOffset.value = -prevPage * size; onFinished?.(); } }, @@ -216,7 +216,7 @@ export function useCarouselController(options: IOpts): ICarouselController { loop, index, onScrollBegin, - handlerOffsetX, + handlerOffset, size, scrollWithTiming, currentFixedPage, @@ -231,13 +231,13 @@ export function useCarouselController(options: IOpts): ICarouselController { onScrollBegin?.(); // direction -> 1 | -1 - const isPositiveZero = Object.is(handlerOffsetX.value, +0); - const isNegativeZero = Object.is(handlerOffsetX.value, -0); + const isPositiveZero = Object.is(handlerOffset.value, +0); + const isNegativeZero = Object.is(handlerOffset.value, -0); const direction = isPositiveZero ? 1 : isNegativeZero ? -1 - : Math.sign(handlerOffsetX.value); + : Math.sign(handlerOffset.value); // target offset const offset = i * size * direction; @@ -248,12 +248,12 @@ export function useCarouselController(options: IOpts): ICarouselController { if (loop) { isCloseToNextLoop = - Math.abs(handlerOffsetX.value % totalSize) / totalSize >= + Math.abs(handlerOffset.value % totalSize) / totalSize >= 0.5; } const finalOffset = - (Math.floor(Math.abs(handlerOffsetX.value / totalSize)) + + (Math.floor(Math.abs(handlerOffset.value / totalSize)) + (isCloseToNextLoop ? 1 : 0)) * totalSize * direction + @@ -261,12 +261,9 @@ export function useCarouselController(options: IOpts): ICarouselController { if (animated) { index.value = i; - handlerOffsetX.value = scrollWithTiming( - finalOffset, - onFinished - ); + handlerOffset.value = scrollWithTiming(finalOffset, onFinished); } else { - handlerOffsetX.value = finalOffset; + handlerOffset.value = finalOffset; index.value = i; onFinished?.(); } @@ -275,7 +272,7 @@ export function useCarouselController(options: IOpts): ICarouselController { index, canSliding, onScrollBegin, - handlerOffsetX, + handlerOffset, size, dataInfo.length, loop, diff --git a/src/hooks/useCommonVariables.ts b/src/hooks/useCommonVariables.ts index 2de1a20a..bae7c27e 100644 --- a/src/hooks/useCommonVariables.ts +++ b/src/hooks/useCommonVariables.ts @@ -5,25 +5,33 @@ import type { TInitializeCarouselProps } from './useInitProps'; interface ICommonVariables { size: number; validLength: number; - handlerOffsetX: Animated.SharedValue; + handlerOffset: Animated.SharedValue; } export function useCommonVariables( props: TInitializeCarouselProps ): ICommonVariables { - const { vertical, height, width, data, defaultIndex } = props; + const { + vertical, + height, + width, + data, + defaultIndex, + defaultScrollOffsetValue, + } = props; const size = vertical ? height : width; const validLength = data.length - 1; - const defaultHandlerOffsetX = -Math.abs(defaultIndex * size); - const handlerOffsetX = useSharedValue(defaultHandlerOffsetX); + const defaultHandlerOffsetValue = -Math.abs(defaultIndex * size); + const _handlerOffset = useSharedValue(defaultHandlerOffsetValue); + const handlerOffset = defaultScrollOffsetValue ?? _handlerOffset; React.useEffect(() => { - handlerOffsetX.value = defaultHandlerOffsetX; - }, [vertical, handlerOffsetX, defaultHandlerOffsetX]); + handlerOffset.value = defaultHandlerOffsetValue; + }, [vertical, handlerOffset, defaultHandlerOffsetValue]); return { size, validLength, - handlerOffsetX, + handlerOffset, }; } diff --git a/src/hooks/useOffsetX.ts b/src/hooks/useOffsetX.ts index f8d75f77..4702eaf2 100644 --- a/src/hooks/useOffsetX.ts +++ b/src/hooks/useOffsetX.ts @@ -8,7 +8,7 @@ import type { IVisibleRanges } from './useVisibleRanges'; export interface IOpts { index: number; size: number; - handlerOffsetX: Animated.SharedValue; + handlerOffset: Animated.SharedValue; data: unknown[]; type?: 'positive' | 'negative'; viewCount?: number; @@ -17,7 +17,7 @@ export interface IOpts { export const useOffsetX = (opts: IOpts, visibleRanges: IVisibleRanges) => { const { - handlerOffsetX, + handlerOffset, index, size, loop, @@ -73,14 +73,14 @@ export const useOffsetX = (opts: IOpts, visibleRanges: IVisibleRanges) => { ]; return interpolate( - handlerOffsetX.value, + handlerOffset.value, inputRange, outputRange, Extrapolate.CLAMP ); } - return handlerOffsetX.value + size * index; + return handlerOffset.value + size * index; }, [loop, data, viewCount, type, size, visibleRanges]); return x; diff --git a/src/layouts/BaseLayout.tsx b/src/layouts/BaseLayout.tsx index bb317225..eddaee19 100644 --- a/src/layouts/BaseLayout.tsx +++ b/src/layouts/BaseLayout.tsx @@ -18,7 +18,7 @@ export type TAnimationStyle = (value: number) => AnimatedStyleProp; export const BaseLayout: React.FC<{ index: number; - handlerOffsetX: Animated.SharedValue; + handlerOffset: Animated.SharedValue; visibleRanges: IVisibleRanges; animationStyle: TAnimationStyle; children: (ctx: { @@ -26,7 +26,7 @@ export const BaseLayout: React.FC<{ }) => React.ReactElement; }> = (props) => { const mounted = useCheckMounted(); - const { handlerOffsetX, index, children, visibleRanges, animationStyle } = + const { handlerOffset, index, children, visibleRanges, animationStyle } = props; const context = React.useContext(CTX); @@ -45,7 +45,7 @@ export const BaseLayout: React.FC<{ const size = vertical ? height : width; const [shouldUpdate, setShouldUpdate] = React.useState(false); let offsetXConfig: IOpts = { - handlerOffsetX, + handlerOffset: handlerOffset, index, size, data, @@ -57,7 +57,7 @@ export const BaseLayout: React.FC<{ const { snapDirection, showLength } = modeConfig as ILayoutConfig; offsetXConfig = { - handlerOffsetX, + handlerOffset: handlerOffset, index, size, data, diff --git a/src/layouts/ParallaxLayout.tsx b/src/layouts/ParallaxLayout.tsx index 8a41aafc..bf693527 100644 --- a/src/layouts/ParallaxLayout.tsx +++ b/src/layouts/ParallaxLayout.tsx @@ -16,7 +16,7 @@ export const ParallaxLayout: React.FC< IComputedDirectionTypes< { loop?: boolean; - handlerOffsetX: Animated.SharedValue; + handlerOffset: Animated.SharedValue; index: number; data: unknown[]; visibleRanges: IVisibleRanges; @@ -24,7 +24,7 @@ export const ParallaxLayout: React.FC< > > = (props) => { const { - handlerOffsetX, + handlerOffset, parallaxScrollingOffset = 100, parallaxScrollingScale = 0.8, parallaxAdjacentItemScale = Math.pow(parallaxScrollingScale, 2), @@ -44,7 +44,7 @@ export const ParallaxLayout: React.FC< const x = useOffsetX( { - handlerOffsetX, + handlerOffset: handlerOffset, index, size, data, diff --git a/src/types.ts b/src/types.ts index 68277271..34f53d48 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,6 +2,7 @@ import type { StyleProp, ViewStyle } from 'react-native'; import type { PanGestureHandlerProps } from 'react-native-gesture-handler'; import type { AnimatedStyleProp, + SharedValue, WithSpringConfig, WithTimingConfig, } from 'react-native-reanimated'; @@ -60,6 +61,10 @@ export type WithAnimation = WithSpringAnimation | WithTimingAnimation; export type TCarouselProps = { ref?: React.Ref; + /** + * The default animated value of the carousel. + */ + defaultScrollOffsetValue?: SharedValue; /** * Carousel loop playback. * @default true @@ -162,7 +167,7 @@ export type TCarouselProps = { */ renderItem: CarouselRenderItem; /** - * Callback fired when navigating to an item + * Callback fired when navigating to an item. */ onSnapToItem?: (index: number) => void; /** From ce4b2946934019e4063bdd43fa5bd8fec6b3ad85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=9D=B1=E6=BE=94?= Date: Wed, 19 Oct 2022 13:53:39 +0800 Subject: [PATCH 2/2] chore: release 3.1.0 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfd5559b..c77dd252 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [3.1.0](https://github.com/dohooo/react-native-reanimated-carousel/compare/v3.0.6...v3.1.0) (2022-10-19) + + +### Features + +* add new props to control or get information for carousel animation ([2a39346](https://github.com/dohooo/react-native-reanimated-carousel/commit/2a3934647ca6558fdd1e213d79f5e64baaff3984)), closes [#285](https://github.com/dohooo/react-native-reanimated-carousel/issues/285) + ## [3.0.6](https://github.com/dohooo/react-native-reanimated-carousel/compare/v3.0.5...v3.0.6) (2022-09-27) diff --git a/package.json b/package.json index fcf88b40..92e265b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-reanimated-carousel", - "version": "3.0.6", + "version": "3.1.0", "description": "Simple carousel component.fully implemented using Reanimated 2.Infinitely scrolling, very smooth.", "main": "lib/commonjs/index", "react-native": "src/index.tsx",