React Native视频控制器动画报错:Trying to remove a child that doesn't exist
问题原因分析
- 动画目标值逻辑错误:
triggerShowHide中第一个动画的toValue直接传入了topControllPosition、rightControllPosition(Animated.Value实例),而非对应的显示状态数值,导致动画执行逻辑混乱。 - 直接替换Animated.Value实例:切换方向时调用
setTopControllPosition(new Animated.Value(25)),旧的Animated.Value实例被替换后,之前未完成的动画仍在引用它,动画系统试图操作已被移除的节点,触发“Trying to remove a child that doesn't exist”警告。 - 窗口尺寸获取方式过时:用
Dimensions.get("window")获取尺寸无法自动响应方向变化,导致控制器位置计算错误,应使用useWindowDimensions返回的实时尺寸。 - useCallback依赖不全:
triggerShowHide的依赖数组仅包含orientation,缺少topControllPosition、rightControllPosition、timeoutId等关键变量,导致函数缓存旧状态,运行时出现不一致。
解决办法
1. 修正动画目标值
显示控制器时,toValue需设置为明确的显示数值(如顶部控制器的25,右侧控制器的width-50),而非Animated.Value实例本身。
2. 更新Animated.Value数值而非替换实例
使用setValue方法更新现有Animated.Value的数值,避免旧动画引用失效。
3. 使用实时窗口尺寸
方向切换时直接使用useWindowDimensions返回的width/height计算位置,确保尺寸与当前方向同步。
4. 补全useCallback依赖数组
将triggerShowHide用到的所有状态、变量加入依赖数组,保证函数能获取最新状态。
修改后的完整代码
import { useState, useEffect, useCallback, useRef } from 'react'; import { Pressable, Animated, useWindowDimensions, Text, View, Easing } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import Orientation from 'react-native-orientation-locker'; // 假设以下常量/工具已定义 const PORTRAIT = 'PORTRAIT'; const LANDSCAPE = 'LANDSCAPE'; const VIDEO_CONTROLLS_HIDE_TIMEOUT = 3000; // const themeColors = {}; // const styles = {}; // const globalStyle = {}; // const shortenName = (name, len) => name.slice(0, len); export const VideoPlayerControll = ({ videoName, navigation }) => { const [orientation, setOrientation] = useState(null); const { width, height } = useWindowDimensions(); // 用useRef保存Animated.Value实例,避免组件重渲染时重建 const topControllPosition = useRef(new Animated.Value(25)).current; const rightControllPosition = useRef(new Animated.Value(width - 50)).current; const [timeoutId, setTimeoutId] = useState(null); useEffect(() => { // 初始化获取当前方向 Orientation.getOrientation((orient) => { setOrientation(orient); }); // 监听方向变化,实时更新状态 const orientationListener = Orientation.addOrientationListener((orient) => { setOrientation(orient); }); // 清理函数:移除监听器+清除未执行的定时器 return () => { Orientation.removeOrientationListener(orientationListener); clearTimeout(timeoutId); }; }, [timeoutId]); const triggerShowHide = useCallback(() => { console.log("triggerShowHide"); clearTimeout(timeoutId); // 显示控制器:动画到目标显示位置 Animated.parallel([ Animated.timing(topControllPosition, { toValue: 25, duration: 500, useNativeDriver: true, easing: Easing.linear }), Animated.timing(rightControllPosition, { toValue: width - 50, duration: 500, useNativeDriver: true, easing: Easing.linear }), ]).start(); const hideTimeout = setTimeout(() => { // 隐藏控制器:动画到超出屏幕的位置 Animated.parallel([ Animated.timing(topControllPosition, { toValue: -50, duration: 500, useNativeDriver: true, easing: Easing.linear }), Animated.timing(rightControllPosition, { toValue: width + 100, // 适配不同方向的屏幕宽度 duration: 500, useNativeDriver: true, easing: Easing.linear }), ]).start(); }, VIDEO_CONTROLLS_HIDE_TIMEOUT); setTimeoutId(hideTimeout); }, [timeoutId, width, topControllPosition, rightControllPosition, VIDEO_CONTROLLS_HIDE_TIMEOUT]); const onChangeOrientation = () => { if (orientation === PORTRAIT) { Orientation.lockToLandscape(); setOrientation(LANDSCAPE); } else { Orientation.lockToPortrait(); setOrientation(PORTRAIT); } // 更新Animated.Value的数值,而非替换实例 topControllPosition.setValue(25); // 根据当前方向计算右侧控制器的目标位置 const targetRightPos = orientation === PORTRAIT ? width - 50 : height - 50; rightControllPosition.setValue(targetRightPos); }; const onPressBackButton = () => { Orientation.lockToPortrait(); navigation.goBack(); }; return ( <Pressable onPress={triggerShowHide} style={{ flex: 1 }}> <Animated.View style={{ flex: 1 }}> <Animated.View style={{ position: "absolute", transform: [{ translateY: topControllPosition }], width: width }}> <View style={styles.topControllCont}> <Pressable onPress={onPressBackButton} style={globalStyle.displayItemInRow} > <Ionicons name={"arrow-back"} size={28} color={themeColors.primaryColor} /> <Text style={{ color: themeColors.primaryColor, marginLeft: 10 }}>{shortenName(videoName, 30)}</Text> </Pressable> <Pressable style={globalStyle.displayItemInRow} > <Ionicons name={"share-social-sharp"} size={28} color={themeColors.primaryColor} /> </Pressable> </View> </Animated.View> <Animated.View style={{ position: "absolute", transform: [{ translateY: height / 2 - 50 }, { translateX: rightControllPosition }], }} > <Pressable onPress={onChangeOrientation} style={styles.contBtn} > <MaterialCommunityIcons name={"phone-rotate-landscape"} size={24} color={themeColors.primaryColor} /> </Pressable> <Pressable style={styles.contBtn} > <MaterialCommunityIcons name="cellphone-screenshot" size={24} color={themeColors.primaryColor} /> </Pressable> </Animated.View> </Animated.View> </Pressable> ); };
额外优化点
- 新增
Orientation.addOrientationListener监听方向变化,确保orientation状态实时同步设备方向。 - 隐藏右侧控制器时用
width + 100作为目标值,适配不同方向的屏幕宽度,避免固定值导致的位置错误。 - 用
useRef保存Animated.Value实例,避免组件重渲染时重建实例,防止动画引用失效。
内容的提问来源于stack exchange,提问作者Mohan kumar SAH
相关产品推荐
相关产品推荐

