React Navigation+Reanimated 2自定义底部导航缩放动画问题排查
问题:自定义底部导航失去焦点时无缩放动画
使用@react-navigation/bottom-tabs和react-native-reanimated搭建React Native自定义底部导航组件,期望标签在获取/失去焦点时都触发缩放动画,但目前仅获取焦点时有动画效果,失去焦点的标签会直接恢复原尺寸,无动画过渡。
相关代码如下:
import React, { useEffect, useState } from 'react'; import { BottomTabBarButtonProps, createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { useIntl } from 'react-intl'; import { Text, TouchableOpacity, View, ViewStyle } from 'react-native'; import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; import Gap from 'components/Gap'; //File Import import HomeScreen from 'screens/home/HomeScreen'; import CategoryHomeScreen from 'screens/category/CategoryHomeScreen'; import VolunteerHomeScreen from 'screens/volunteer/VolunteerHomeScreen'; import ProfileHomeScreen from 'screens/profile/ProfileHomeScreen'; //Utils Import import { CATEGORY_HOME, HOME, PROFILE_HOME, VOLUNTEER_HOME } from 'utils/routesConstant'; import useTheme from 'utils/hooks/useTheme'; import { IcCategory, IcHome, IcProfile, IcVolunteer } from 'assets/icons'; import { RootStackParams } from 'utils/routes'; const Tab = createBottomTabNavigator(); interface CustomTabBar extends BottomTabBarButtonProps{ name: string; title: string; component: ({ route, navigation }: NativeStackScreenProps<RootStackParams, any>) => React.JSX.Element; icon: (focused?: boolean) => React.JSX.Element; } const MainBottomNavigation = () => { const {Colors,Fonts} = useTheme(); const intl = useIntl(); const tabBarStyle:ViewStyle = { position: 'absolute', bottom:24, left:16, right:16, borderRadius:10, alignItems:'center', justifyContent:'center', height: 64, elevation:1, shadowOffset: { width: 0, height: 0, }, shadowOpacity: 0.1, shadowRadius: 4, shadowColor: Colors.dropShadow, backgroundColor:Colors.white } const TabArray = [ { id:1, name:HOME, title:intl.formatMessage({defaultMessage:"Home"}), component:HomeScreen, icon:(focused?:boolean) => ( <IcHome stroke={focused ? Colors.primary : Colors.textPrimary} width={24} height={24}/> ) }, .... ] const CustomTabBar = (props:CustomTabBar) => { const scale = useSharedValue(1.0); const focused = props.accessibilityState?.selected const {onPress} = props const targetScale = focused ? 1.2 : 1.0; //This only animates when tab is focoused in, when I navigate out from the tab it instantly goes to original size without animating. scale.value = withSpring(targetScale, { damping: 10, stiffness: 800, }); const tabMenuStyleAnimated = useAnimatedStyle(() => { return { transform: [{ scale: scale.value }], }; }); return ( <TouchableOpacity activeOpacity={0.7} style={[{top:10,flex:1,alignItems:'center'}]} onPress={onPress}> <Animated.View style={tabMenuStyleAnimated}> {props.icon(focused)} </Animated.View> <Gap height={4} /> <Text style={[Fonts.fontMedium, Fonts.sizeXS, focused ? Fonts.textMainColor : Fonts.textPrimary,Fonts.textCenter]}>{props.title}</Text> </TouchableOpacity> ) } return ( <Tab.Navigator initialRouteName={HOME} screenOptions={{ headerShown: false , tabBarShowLabel:false, tabBarStyle }}> {TabArray.map((item,_)=>( <Tab.Screen key={item.id} name={item.title as any} component={item.component} options={{ tabBarButton:(props)=><CustomTabBar {...props} title={item.title} component={item.component} icon={item.icon} name={item.name}/> }} /> ))} </Tab.Navigator> ); } export default MainBottomNavigation;
解决方案
问题根源
直接在组件渲染流程中给scale.value赋值withSpring,会导致每次focused状态变化时,组件重新渲染并创建新的scale共享值实例。当标签失去焦点时,旧的组件实例被销毁,动画无法完整执行,表现为直接跳转到原尺寸。
修正后的代码
修改CustomTabBar组件,通过useEffect监听focused状态变化来触发动画:
const CustomTabBar = (props:CustomTabBar) => { const scale = useSharedValue(1.0); const focused = props.accessibilityState?.selected; const {onPress} = props; // 监听focused状态变化,触发动画 useEffect(() => { const targetScale = focused ? 1.2 : 1.0; scale.value = withSpring(targetScale, { damping: 10, stiffness: 800, }); }, [focused]); const tabMenuStyleAnimated = useAnimatedStyle(() => { return { transform: [{ scale: scale.value }], }; }); return ( <TouchableOpacity activeOpacity={0.7} style={[{top:10,flex:1,alignItems:'center'}]} onPress={onPress}> <Animated.View style={tabMenuStyleAnimated}> {props.icon(focused)} </Animated.View> <Gap height={4} /> <Text style={[Fonts.fontMedium, Fonts.sizeXS, focused ? Fonts.textMainColor : Fonts.textPrimary,Fonts.textCenter]}>{props.title}</Text> </TouchableOpacity> ) }
关键优化点
- 用
useEffect监听focused状态,确保只有当焦点状态真正变化时才触发动画,避免渲染时重复创建动画实例。 - 保留
scale共享值的连续性,无论标签获取还是失去焦点,都能基于当前值执行完整的弹簧动画过渡。
内容的提问来源于stack exchange,提问作者Bagus Amrullah Fikri
相关产品推荐
相关产品推荐

