查看: 175|回复: 0

鸿蒙 RNOH Animated.Value 监听不触发与偏移适配

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
背景:为什么鸿蒙上的 Animated.Value 要单独适配

React Native 的 Animated.Value 是 Animated 动画库的核心,本质上是一个驱动动画的一维标量值,很多组件样式和手势逻辑都围绕它展开。原文作者在 iOS 上用 addListener 监听值变化来同步 scroll 偏移跟踪,运行正常;换到鸿蒙后,addListener 回调完全没触发。进一步排查发现,useNativeDriver: true 时,鸿蒙上的监听器可能不会实时回调。本文基于 React Native 0.84 + RNOH 0.84.1,测试设备为 HarmonyOS 6.0。不同版本之间存在差异,最终以真机实测为准。

Animated.Value 的定位与创建

Animated.Value 可以作为动画数据源,创建时传入初始值。组件内推荐用 useRef 保存,避免重复创建。一个 Animated.Value 可以同步驱动多个属性,但每次只能以一种动画机制变化;开始新动画或调用 setValue 会停止先前的动画;也可以通过 addListener 监听值变化。
  1. import { Animated } from 'react-native';
  2. const animValue = new Animated.Value(0);
  3. const animValueRef = useRef(new Animated.Value(0)).current;
复制代码

setValue、setOffset 与偏移合并

setValue 用于直接设置动画值,立即生效,没有过渡动画。它适合初始化、重置和不需要过渡的即时更新。鸿蒙注意:在原生驱动的动画中,setValue 可能无法立即生效。
  1. const anim = new Animated.Value(0);
  2. anim.setValue(1);
复制代码

setOffset 设置偏移量,叠加在基础值之上。最终输出值等于基础值加偏移量,常用于手势动画记录起始位置和平移手势的偏移补偿。
  1. const panX = new Animated.Value(0);
  2. panX.setOffset(100);
复制代码

偏移管理中有两个容易混淆的方法:flattenOffset 把偏移值合并到基础值中,偏移置零;extractOffset 把偏移值设为基准值,基础值置零。两者输出值在示例中相同,但内部基础值和偏移量的归属不同。
  1. const anim = new Animated.Value(10);
  2. anim.setOffset(20);
  3. // 输出:10 + 20 = 30
  4. anim.flattenOffset();
  5. // 基础值变 30,偏移量变 0,输出:30 + 0 = 30
复制代码
  1. const anim = new Animated.Value(10);
  2. anim.setOffset(20);
  3. // 输出:10 + 20 = 30
  4. anim.extractOffset();
  5. // 偏移量变 30,基础值变 0,输出:0 + 30 = 30
复制代码

手势场景中,常见做法是手势结束时合并偏移,或在下一次手势开始时提取偏移:
  1. const pan = useRef(new Animated.Value(0)).current;
  2. const handlePanResponderRelease = () => {
  3.   pan.extractOffset();
  4. };
  5. const handleReset = () => {
  6.   pan.flattenOffset();
  7. };
复制代码

addListener:鸿蒙上最需要警惕的 API

addListener 添加异步监听器,用于观察动画值变化,返回监听器 ID;removeListener 移除指定监听,removeAllListeners 移除全部监听。它常用于实时更新 UI、同步动画值到 state,以及调试动画。组件卸载时一定要移除监听器。
  1. const anim = new Animated.Value(0);
  2. const listenerId = anim.addListener(({ value }) => {
  3.   console.log('当前值:', value);
  4. });
  5. anim.removeListener(listenerId);
  6. anim.removeAllListeners();
复制代码

下面是一个把动画值同步到文本的示例:
  1. const FollowerComponent = () => {
  2.   const anim = useRef(new Animated.Value(0)).current;
  3.   const [text, setText] = useState('');
  4.   useEffect(() => {
  5.     const id = anim.addListener(({ value }) => {
  6.       setText(`当前值: ${value.toFixed(2)}`);
  7.     });
  8.     return () => anim.removeListener(id);
  9.   }, []);
  10.   return <Text>{text}</Text>;
  11. };
复制代码

鸿蒙注意:useNativeDriver: true 时,addListener 的回调可能不会实时触发,甚至可能完全不触发。因此,依赖监听器同步 state 的逻辑不能假设它一定实时,关键 UI 同步要考虑其他实现方式或降级。

stopAnimation、resetAnimation 与 interpolate

stopAnimation 停止正在运行的动画,并能通过回调拿到最终值,适合在停止后更新 state 以匹配布局位置。
  1. const anim = new Animated.Value(0);
  2. Animated.timing(anim, {
  3.   toValue: 100,
  4.   duration: 10000,
  5.   useNativeDriver: true,
  6. }).start();
  7. anim.stopAnimation((finalValue) => {
  8.   console.log('动画停止时的值:', finalValue);
  9. });
复制代码

resetAnimation 停止动画并把值重置为初始值:
  1. const anim = new Animated.Value(0);
  2. Animated.timing(anim, {
  3.   toValue: 100,
  4.   duration: 5000,
  5.   useNativeDriver: true,
  6. }).start();
  7. anim.resetAnimation(() => {
  8.   console.log('已重置为初始值');
  9. });
复制代码

interpolate 用于把输入范围映射到输出范围,可以用于透明度、位移、旋转、颜色和缩放等属性。extrapolate 有三个选项:extend 允许超出范围,clamp 限制在范围内,identity 超出范围时返回输入值。
  1. const anim = new Animated.Value(0);
  2. const opacity = anim.interpolate({
  3.   inputRange: [0, 1],
  4.   outputRange: [0, 1],
  5. });
  6. const translateX = anim.interpolate({
  7.   inputRange: [0, 1],
  8.   outputRange: [0, 100],
  9.   extrapolate: 'clamp',
  10. });
  11. const rotate = anim.interpolate({
  12.   inputRange: [0, 1],
  13.   outputRange: ['0deg', '360deg'],
  14. });
  15. const color = anim.interpolate({
  16.   inputRange: [0, 0.5, 1],
  17.   outputRange: ['#0A59F7', '#10B981', '#EF4444'],
  18. });
  19. const scale = anim.interpolate({
  20.   inputRange: [0, 0.5, 1],
  21.   outputRange: [1, 1.2, 1],
  22. });
复制代码

鸿蒙注意:颜色插值不支持,interpolate 的颜色映射在鸿蒙上不生效。涉及颜色变化的动画,需要改用其他属性或方案。

完整示例:手势偏移管理

下面这个例子把偏移提取、手势移动和偏移合并串起来,用 Animated.View 的 translateX 驱动位移:
  1. const PanGesture = () => {
  2.   const panX = useRef(new Animated.Value(0)).current;
  3.   const handleGrant = () => {
  4.     panX.setOffset(panX.__getValue());
  5.     panX.setValue(0);
  6.   };
  7.   const handleMove = (dx) => {
  8.     panX.setValue(dx);
  9.   };
  10.   const handleRelease = () => {
  11.     panX.flattenOffset();
  12.   };
  13.   return (
  14.     <Animated.View style={{ transform: [{ translateX: panX }] }} />
  15.   );
  16. };
复制代码

注意:__getValue() 是双下划线内部方法,原文明确建议不要依赖。上面的写法只用于还原示例场景,生产代码应避免依赖内部实现。

项目使用建议与踩坑清单

在实际项目中,Animated.Value 主要承担四类工作:一是作为数据源驱动 UI 动画,通过 setValue、timing、spring 驱动 Animated.View 等组件样式;二是跟踪手势位置,用 setValue 记录位置,用 setOffset、extractOffset、flattenOffset 管理偏移;三是监听动画状态,用 addListener 获取动画值并同步到 state 或执行其他逻辑;四是用 interpolate 把一个数值范围映射到多个属性,实现复杂动画。

鸿蒙上的踩坑点集中在以下几处:
1. addListener 延迟触发:useNativeDriver: true 时监听器可能不触发。
2. 颜色插值不支持:interpolate 的颜色映射在鸿蒙上不生效。
3. setValue 延迟:在原生驱动动画中 setValue 可能不立即生效。
4. __getValue() 不可靠:双下划线方法是内部实现,不要依赖。

给后来者的建议:调试动画时善用 addListener,但生产环境要关注性能;组件卸载时管理好监听器,记得 removeListener;在鸿蒙真机上测试动画的各种表现;优先使用 transform,兼容性最好;持续关注 RNOH 更新,Animated.Value 的支持在不断完善中。

本文内容与示例基于 React Native 0.84 + RNOH 0.84.1、HarmonyOS 6.0。不同版本之间可能存在差异,以实际测试结果为准。原文中的代码示例已在鸿蒙设备上测试通过,可直接参考。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-9-15 11:58 , Processed in 0.025145 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部