React Native自定义抽屉导航动画失效 尺寸跳变 关闭后无法全屏
问题现象
实现带开关动画的自定义抽屉导航时,预期抽屉滑动过程中MainLayout会同步执行收缩/展开、圆角变化的平滑过渡,实际出现两个异常:
- 抽屉手势滑动过程中主布局无实时过渡动画,仅当滑动到全开/全关端点时才直接跳变到最终样式
- 点击抽屉内置关闭按钮触发关闭时,抽屉收回后主布局无法恢复到全屏无圆角的初始状态
已确认babel配置中已添加react-native-reanimated/plugin插件并清理过缓存,问题仍存在。
相关代码
自定义抽屉组件代码
import 'react-native-gesture-handler'; import React, {useState} from 'react'; import {StyleSheet, TouchableOpacity, Image, Text, View} from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; import Animated from 'react-native-reanimated'; import { createDrawerNavigator, DrawerContentScrollView, useDrawerProgress, } from '@react-navigation/drawer'; import {icons} from '../constants'; // screens import import MainLayout from '../screens/MainLayout'; const Drawer = createDrawerNavigator(); const CustomDrawerItem = ({icon, label}) => { return ( <TouchableOpacity style={[styles.drawerItem]} onPress={() => console.log('drawer item')}> <Image source={icon} style={styles.drawerItemIcon} /> <Text style={styles.drawerItemLabel}> {label} </Text> </TouchableOpacity> ); }; const CustomDrawerContent = ({navigation}) => { return ( <DrawerContentScrollView scrollEnabled={true} contentContainerStyle={{flex: 1}}> <View style={{flex: 1, paddingHorizontal: 20}}> <View style={styles.closeBtnContainer}> <TouchableOpacity style={styles.closeBtn} onPress={() => navigation.closeDrawer()}> <Image source={icons.close} style={styles.closeBtnIcon} resizeMode="contain" /> </TouchableOpacity> </View> <TouchableOpacity style={styles.profileButton} onPress={() => console.log('profile')}> <Image source={icons.avatar} style={styles.profileImage} resizeMode="cover" /> <View style={{marginLeft: 10}}> <Text style={styles.commonTxt}> John Doe </Text> <Text style={[styles.commonTxt, {fontSize: 15}]}> john@xyz.com </Text> </View> </TouchableOpacity> <View style={styles.drawerItmContainer}> <CustomDrawerItem icon={icons.qr_code} label="QR Code" /> <CustomDrawerItem icon={icons.resource} label="Resource" /> <CustomDrawerItem icon={icons.collection_report} label="Collection Report" /> <CustomDrawerItem icon={icons.change_pin} label="Change Pin" /> <View style={styles.separator}/> </View> <View style={{marginBottom: 20}}> <CustomDrawerItem icon={icons.logout} label="Logout" /> </View> </View> </DrawerContentScrollView> ); }; const CustomDrawer = () => { return ( <LinearGradient colors={['#ff9933', '#3b5998', '#192f6a']} style={styles.container}> <Drawer.Navigator screenOptions={{ headerShown: false, drawerType: 'slide', overlayColor: 'transparent', drawerStyle: {flex: 1, width: '65%', backgroundColor: 'transparent'}, sceneContainerStyle: {backgroundColor: 'transparent'}, }} drawerContent={props => { return <CustomDrawerContent navigation={props.navigation} />; }} initialRouteName="MainLayout"> <Drawer.Screen name="MainLayout"> {props => <MainLayout {...props} />} </Drawer.Screen> </Drawer.Navigator> </LinearGradient> ); }; export default CustomDrawer; const styles = StyleSheet.create({ drawerItem: { flexDirection: 'row', alignItems: 'center', marginBottom: 20, borderRadius: 10, }, drawerItemIcon: {width: 20, height: 20, tintColor: '#fff'}, drawerItemLabel: {fontSize: 18, color: '#fff', marginLeft: 15}, profileButton: {flexDirection: 'row', alignItems: 'center', marginTop: 20}, profileImage: {width: 50, height: 50, borderRadius: 10}, closeBtnContainer: {alignItems: 'flex-start', justifyContent: 'center'}, closeBtn: {alignItems: 'center', justifyContent: 'center'}, closeBtnIcon: {width: 30, height: 30, tintColor: '#fff'}, commonTxt: {fontSize: 18, color: '#fff'}, drawerItmContainer: {flex: 1, marginTop: 20}, separator: {height: 1, backgroundColor: '#fff', marginVertical: 10}, container: {flex: 1, backgroundColor: '#ff9f1c'}, });
原MainLayout组件代码
import 'react-native-gesture-handler'; import {StyleSheet, Text, View, SafeAreaView} from 'react-native'; import React from 'react'; import Animated from 'react-native-reanimated'; import {useDrawerProgress} from '@react-navigation/drawer'; const MainLayout = props => { const progress = useDrawerProgress(); const scale = Animated.interpolateNode(progress.value, { inputRange: [0, 1], outputRange: [1, 0.75], }); const borderRadius = Animated.interpolateNode(progress.value, { inputRange: [0, 1], outputRange: [0, 30], }); const animatedStyle = { borderRadius, transform: [{scale}], }; return ( <Animated.View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: '#fff', ...animatedStyle, }}> <Text>MainLayout</Text> </Animated.View> ); }; export default MainLayout; const styles = StyleSheet.create({});
依赖配置(package.json)
{ "name": "Dhananjaya", "version": "0.0.1", "private": true, "scripts": { "android": "react-native run-android", "ios": "react-native run-ios", "start": "react-native start", "test": "jest", "lint": "eslint ." }, "dependencies": { "@react-native-masked-view/masked-view": "^0.2.6", "@react-navigation/drawer": "^6.4.1", "@react-navigation/native": "^6.0.10", "@react-navigation/stack": "^6.2.1", "axios": "^0.27.2", "native-base": "^3.4.5", "react": "17.0.2", "react-native": "0.68.2", "react-native-gesture-handler": "^2.4.2", "react-native-linear-gradient": "^2.5.6", "react-native-reanimated": "^2.8.0", "react-native-safe-area-context": "^4.2.5", "react-native-screens": "^3.13.1", "react-native-svg": "^12.3.0" }, "devDependencies": { "@babel/core": "7.18.2", "@babel/runtime": "7.18.3", "@react-native-community/eslint-config": "2.0.0", "babel-jest": "26.6.3", "eslint": "7.32.0", "jest": "26.6.3", "metro-react-native-babel-preset": "0.67.0", "react-test-renderer": "17.0.2" }, "jest": { "preset": "react-native" } }
babel配置(babel.config.js)
module.exports = { presets: ['module:metro-react-native-babel-preset'], plugins: ['react-native-reanimated/plugin'], };
异常效果演示
- 滑动无实时动画效果:

- 点击关闭按钮后主布局不恢复:

问题原因
代码中Reanimated v2的API使用方式错误:
useDrawerProgress()返回的本身就是Reanimated共享值(SharedValue),旧写法直接在渲染阶段取progress.value做插值,无法触发实时的UI更新interpolateNode是Reanimated v1的旧API,在v2版本中动画样式必须通过useAnimatedStyle钩子定义,才能响应共享值的连续变化,否则只会在值到达端点(0/1)时触发普通重渲染,表现为样式跳变- 点击按钮关闭抽屉时,进度值是连续变化的,但由于样式没有被
useAnimatedStyle包裹,无法跟踪中间值的变化,导致最终样式停留在缩放状态无法恢复。
修复方案
替换MainLayout组件代码为以下版本,使用Reanimated v2标准API编写动画样式:
import 'react-native-gesture-handler'; import {StyleSheet, Text, View} from 'react-native'; import React from 'react'; import Animated, {interpolate, useAnimatedStyle} from 'react-native-reanimated'; import {useDrawerProgress} from '@react-navigation/drawer'; const MainLayout = () => { const progress = useDrawerProgress(); const animatedStyle = useAnimatedStyle(() => { // 插值计算缩放值 const scale = interpolate(progress.value, [0, 1], [1, 0.75]); // 插值计算圆角 const borderRadius = interpolate(progress.value, [0, 1], [0, 30]); // 可选:添加X轴偏移,抽屉打开时主布局同步右移,视觉效果更自然 const translateX = interpolate(progress.value, [0, 1], [0, 60]); return { borderRadius, transform: [{scale}, {translateX}], overflow: 'hidden' }; }); return ( <Animated.View style={[ { flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: '#fff', }, animatedStyle ]}> <Text>MainLayout</Text> </Animated.View> ); }; export default MainLayout; const styles = StyleSheet.create({});
修改完成后执行以下命令彻底清理缓存重启服务,确保配置生效:
# 清理metro缓存 npx react-native start --reset-cache # 安卓重新打包 npx react-native run-android # iOS重新安装依赖并打包 cd ios && pod install && cd .. && npx react-native run-ios
内容的提问来源于stack exchange,提问作者Santanu Roy
相关产品推荐
相关产品推荐

