/* eslint-disable @typescript-eslint/no-explicit-any */ import React, { memo, useCallback, useMemo } from 'react'; import isEqual from 'react-fast-compare'; import { StyleProp, View, ViewStyle } from 'react-native'; import Animated, { useAnimatedProps, useDerivedValue, } from 'react-native-reanimated'; import { useSafeAreaFrame, useSafeAreaInsets, } from 'react-native-safe-area-context'; import Svg, { Path, PathProps } from 'react-native-svg'; import { sharedEq, sharedTiming, useInterpolate, withSharedTransition, } from '../AnimatedHelper'; import { RNShadow } from '../RNShadow'; import type { TabBarViewProps, TabRoute } from '../types'; import { HEIGHT_HOLE, TAB_BAR_HEIGHT } from './constant'; import { ButtonTab } from './item/ButtonTabItem'; import { Dot } from './item/Dot'; import { styles } from './style'; const AnimatedPath = Animated.createAnimatedComponent(Path); const CurvedTabBarComponent = (props: TabBarViewProps) => { // props const { routes, selectedIndex, barWidth, duration, dotColor, tabBarColor, titleShown, isRtl, navigationIndex, dotSize: SIZE_DOT, barHeight = TAB_BAR_HEIGHT, } = props; // state const { bottom } = useSafeAreaInsets(); const { width } = useSafeAreaFrame(); const actualBarWidth = useMemo<number>( () => barWidth || width, [barWidth, width] ); const widthTab = useMemo( () => actualBarWidth / routes.length, [routes, actualBarWidth] ); const inputRange = useMemo( () => isRtl ? routes.map((_: any, index: number) => index).reverse() : routes.map((_: any, index: number) => index), [isRtl, routes] ); const outputRange = useMemo( () => routes.map( (_: any, index: number) => (index / routes.length) * actualBarWidth ), [routes, actualBarWidth] ); const actualBarHeight = useMemo<number>( () => barHeight + bottom, [barHeight, bottom] ); const indexAnimated = useDerivedValue(() => sharedTiming(selectedIndex.value, { duration }) ); // func const renderButtonTab = useCallback( ({ key, title, ...configs }: TabRoute, index: number) => { return ( <ButtonTab focused={index === selectedIndex.value} width={actualBarWidth} key={key} title={title} titleShown={titleShown} indexAnimated={indexAnimated} countTab={routes.length} selectedIndex={selectedIndex} index={index} {...configs} /> ); }, [indexAnimated, routes.length, selectedIndex, titleShown, actualBarWidth] ); // reanimated const progress = withSharedTransition(sharedEq(selectedIndex, indexAnimated)); const xPath = useInterpolate(indexAnimated, inputRange, outputRange); // path const pathProps = useAnimatedProps<PathProps>(() => { const centerHoleX = xPath.value + widthTab / 2; return { d: `M0,0 L${centerHoleX - SIZE_DOT},0 C${centerHoleX - SIZE_DOT * 0.5},0 ${ centerHoleX - SIZE_DOT * 0.75 },${HEIGHT_HOLE} ${centerHoleX},${HEIGHT_HOLE} C${centerHoleX + SIZE_DOT * 0.75},${HEIGHT_HOLE} ${ centerHoleX + SIZE_DOT * 0.5 },0 ${centerHoleX + SIZE_DOT} 0 L${actualBarWidth * 2},0 L ${ actualBarWidth * 2 },${actualBarHeight} L 0,${actualBarHeight} Z `, }; }, [actualBarWidth, widthTab, SIZE_DOT, actualBarHeight]); // style const containerStyle = useMemo<StyleProp<ViewStyle>>( () => [ { height: actualBarHeight, width: actualBarWidth, }, ], [actualBarHeight, actualBarWidth] ); const rowTab = useMemo<StyleProp<ViewStyle>>( () => [ { width: actualBarWidth, height: actualBarHeight, }, ], [actualBarHeight, actualBarWidth] ); return ( <> <RNShadow style={[styles.container, containerStyle]}> <Svg width={actualBarWidth} height={actualBarHeight} style={[styles.svg]} > <AnimatedPath animatedProps={pathProps} translateY={3} fill={tabBarColor} stroke={'transparent'} strokeWidth={0} /> </Svg> </RNShadow> <View style={[styles.rowTab, rowTab]}> <Dot navigationIndex={navigationIndex} isRtl={isRtl} dotColor={dotColor} dotSize={SIZE_DOT} barHeight={actualBarHeight} width={actualBarWidth} selectedIndex={indexAnimated} routes={routes} progress={progress} /> {routes.map(renderButtonTab)} </View> </> ); }; export const CurvedTabBar = memo(CurvedTabBarComponent, isEqual);