You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 21:26:13