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

React Native视频控制器动画报错:Trying to remove a child that doesn't exist

问题原因分析
  1. 动画目标值逻辑错误:triggerShowHide中第一个动画的toValue直接传入了topControllPosition、rightControllPosition(Animated.Value实例),而非对应的显示状态数值,导致动画执行逻辑混乱。
  2. 直接替换Animated.Value实例:切换方向时调用setTopControllPosition(new Animated.Value(25)),旧的Animated.Value实例被替换后,之前未完成的动画仍在引用它,动画系统试图操作已被移除的节点,触发“Trying to remove a child that doesn't exist”警告。
  3. 窗口尺寸获取方式过时:用Dimensions.get("window")获取尺寸无法自动响应方向变化,导致控制器位置计算错误,应使用useWindowDimensions返回的实时尺寸。
  4. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 22:12:55