diff --git a/example/src/anim-tab-bar/index.tsx b/example/src/anim-tab-bar/index.tsx index 7c09b7dd..4a057735 100644 --- a/example/src/anim-tab-bar/index.tsx +++ b/example/src/anim-tab-bar/index.tsx @@ -1,80 +1,153 @@ import * as React from 'react'; -import { Dimensions } from 'react-native'; -import { Extrapolate, interpolate } from 'react-native-reanimated'; -import Carousel from 'react-native-reanimated-carousel'; -import { View, Text } from 'react-native-ui-lib'; -import type { TAnimationStyle } from '../../../src/layouts/BaseLayout'; +import { Dimensions, Pressable } from 'react-native'; +import Animated, { + Extrapolate, + interpolate, + interpolateColor, + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated'; +import Carousel, { ICarouselInstance } from 'react-native-reanimated-carousel'; +import { Colors, View } from 'react-native-ui-lib'; +import SButton from '../components/SButton'; import { ElementsText } from '../constants'; import { useToggleButton } from '../hooks/useToggleButton'; const window = Dimensions.get('window'); -const PAGE_WIDTH = 40; +const PAGE_WIDTH = 60; const PAGE_HEIGHT = 40; +const DATA = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']; function Index() { + const r = React.useRef(null); const AutoPLay = useToggleButton({ defaultValue: false, buttonTitle: ElementsText.AUTOPLAY, }); - const animationStyle: TAnimationStyle = React.useCallback( - (value: number) => { - 'worklet'; - - const translateX = interpolate( - value, - [-1, 0, 1], - [-PAGE_WIDTH, 0, PAGE_WIDTH] - ); - - const opacity = interpolate( - value, - [-1, 0, 1], - [0.5, 1, 0.5], - Extrapolate.CLAMP - ); - - const scale = interpolate( - value, - [-1, 0, 1], - [0.8, 1.4, 0.8], - Extrapolate.CLAMP - ); - - return { - transform: [{ translateX }, { scale }], - opacity, - }; - }, - [] - ); - return ( - + { + return ( + + r.current?.scrollTo(animationValue.value) + } + /> + ); + }} + autoPlay={AutoPLay.status} + /> + + {AutoPLay.button} + { - return ( - - {item} - - ); + marginTop: 24, + flexDirection: 'row', + justifyContent: 'space-evenly', }} - autoPlay={AutoPLay.status} - customAnimation={animationStyle} - /> - {AutoPLay.button} + > + r.current?.prev()}>{'Prev'} + r.current?.next()}>{'Next'} + ); } export default Index; + +interface Props { + animationValue: Animated.SharedValue; + label: string; + onPress?: () => void; +} + +const Item: React.FC = (props) => { + const { animationValue, label, onPress } = props; + + const translateY = useSharedValue(0); + + const containerStyle = useAnimatedStyle(() => { + const opacity = interpolate( + animationValue.value, + [-1, 0, 1], + [0.5, 1, 0.5], + Extrapolate.CLAMP + ); + + return { + opacity, + }; + }, [animationValue]); + + const labelStyle = useAnimatedStyle(() => { + const scale = interpolate( + animationValue.value, + [-1, 0, 1], + [1, 1.25, 1], + Extrapolate.CLAMP + ); + + const color = interpolateColor( + animationValue.value, + [-1, 0, 1], + [Colors.grey30, Colors.blue30, Colors.grey30] + ); + + return { + transform: [{ scale }, { translateY: translateY.value }], + color, + }; + }, [animationValue, translateY]); + + const onPressIn = React.useCallback(() => { + translateY.value = withTiming(-8, { duration: 250 }); + }, [translateY]); + + const onPressOut = React.useCallback(() => { + translateY.value = withTiming(0, { duration: 250 }); + }, [translateY]); + + return ( + + + + {label} + + + + ); +}; diff --git a/src/Carousel.tsx b/src/Carousel.tsx index b7453593..6c91a686 100644 --- a/src/Carousel.tsx +++ b/src/Carousel.tsx @@ -115,8 +115,9 @@ function Carousel( prev, getCurrentIndex, goToIndex, + scrollTo: carouselController.scrollTo, }), - [getCurrentIndex, goToIndex, next, prev] + [getCurrentIndex, goToIndex, next, prev, carouselController.scrollTo] ); const visibleRanges = useVisibleRanges({ diff --git a/src/hooks/useCarouselController.tsx b/src/hooks/useCarouselController.tsx index 38bc0278..0a171db8 100644 --- a/src/hooks/useCarouselController.tsx +++ b/src/hooks/useCarouselController.tsx @@ -27,6 +27,7 @@ export interface ICarouselController { computedIndex: () => void; getCurrentIndex: () => number; to: (index: number, animated?: boolean) => void; + scrollTo: (animationValue: number, animated?: boolean) => void; } export function useCarouselController(opts: IOpts): ICarouselController { @@ -46,6 +47,19 @@ export function useCarouselController(opts: IOpts): ICarouselController { const sharedIndex = React.useRef(0); const sharedPreIndex = React.useRef(0); + const currentFixedPage = React.useCallback(() => { + if (loop) { + return -Math.round(handlerOffsetX.value / size); + } + + const fixed = (handlerOffsetX.value / size) % length; + return Math.round( + handlerOffsetX.value <= 0 + ? Math.abs(fixed) + : Math.abs(fixed > 0 ? length - fixed : 0) + ); + }, [handlerOffsetX, length, size, loop]); + const convertToSharedIndex = React.useCallback( (i: number) => { if (loop) { @@ -100,12 +114,11 @@ export function useCarouselController(opts: IOpts): ICarouselController { }, [opts]); const scrollWithTiming = React.useCallback( - (toValue: number, callback?: () => void) => { + (toValue: number) => { return withTiming( toValue, { duration, easing: Easing.easeOutQuart }, (isFinished: boolean) => { - callback?.(); if (isFinished) { runOnJS(onScrollEnd)(); } @@ -115,42 +128,54 @@ export function useCarouselController(opts: IOpts): ICarouselController { [onScrollEnd, duration] ); - const next = React.useCallback(() => { - if (!canSliding() || (!loop && index.value === length - 1)) return; + const next = React.useCallback( + (n = 1, animated = true) => { + if (!canSliding() || (!loop && index.value >= length - 1)) return; - onScrollBegin?.(); - - const currentPage = Math.round(handlerOffsetX.value / size); - - handlerOffsetX.value = scrollWithTiming((currentPage - 1) * size); - }, [ - canSliding, - loop, - index.value, - length, - onScrollBegin, - handlerOffsetX, - size, - scrollWithTiming, - ]); + onScrollBegin?.(); - const prev = React.useCallback(() => { - if (!canSliding() || (!loop && index.value === 0)) return; + const nextPage = currentFixedPage() + n; + index.value = nextPage; + handlerOffsetX.value = animated + ? scrollWithTiming(-nextPage * size) + : -nextPage * size; + }, + [ + canSliding, + loop, + index, + length, + onScrollBegin, + handlerOffsetX, + size, + scrollWithTiming, + currentFixedPage, + ] + ); - onScrollBegin?.(); + const prev = React.useCallback( + (n = 1, animated = true) => { + if (!canSliding() || (!loop && index.value <= 0)) return; - const currentPage = Math.round(handlerOffsetX.value / size); + onScrollBegin?.(); - handlerOffsetX.value = scrollWithTiming((currentPage + 1) * size); - }, [ - canSliding, - loop, - index.value, - onScrollBegin, - handlerOffsetX, - size, - scrollWithTiming, - ]); + const prevPage = currentFixedPage() - n; + index.value = prevPage; + handlerOffsetX.value = animated + ? scrollWithTiming(-prevPage * size) + : -prevPage * size; + }, + [ + canSliding, + loop, + index, + onScrollBegin, + handlerOffsetX, + size, + scrollWithTiming, + currentFixedPage, + ] + ); const to = React.useCallback( (idx: number, animated: boolean = false) => { @@ -162,9 +187,8 @@ export function useCarouselController(opts: IOpts): ICarouselController { const offset = handlerOffsetX.value + (index.value - idx) * size; if (animated) { - handlerOffsetX.value = scrollWithTiming(offset, () => { - index.value = idx; - }); + index.value = idx; + handlerOffsetX.value = scrollWithTiming(offset); } else { handlerOffsetX.value = offset; index.value = idx; @@ -182,10 +206,23 @@ export function useCarouselController(opts: IOpts): ICarouselController { ] ); + const scrollTo = React.useCallback( + (value: number, animated = true) => { + const n = Math.round(value); + if (n < 0) { + prev(Math.abs(n), animated); + } else { + next(n, animated); + } + }, + [prev, next] + ); + return { next, prev, to, + scrollTo, index, length, sharedIndex, diff --git a/src/types.ts b/src/types.ts index 79915a5f..c1ec5077 100644 --- a/src/types.ts +++ b/src/types.ts @@ -140,11 +140,11 @@ export interface ICarouselInstance { /** * Play the last one */ - prev: () => void; + prev: (n?: number) => void; /** * Play the next one */ - next: () => void; + next: (n?: number) => void; /** * Get current item index */ @@ -153,6 +153,10 @@ export interface ICarouselInstance { * Go to index */ goToIndex: (index: number, animated?: boolean) => void; + /** + * Go to index by animationValue + */ + scrollTo: (value: number, animated?: boolean) => void; } export interface CarouselRenderItemInfo {