背景:为什么鸿蒙上的 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 监听值变化。
- import { Animated } from 'react-native';
- const animValue = new Animated.Value(0);
- const animValueRef = useRef(new Animated.Value(0)).current;
复制代码
setValue、setOffset 与偏移合并
setValue 用于直接设置动画值,立即生效,没有过渡动画。它适合初始化、重置和不需要过渡的即时更新。鸿蒙注意:在原生驱动的动画中,setValue 可能无法立即生效。
- const anim = new Animated.Value(0);
- anim.setValue(1);
复制代码
setOffset 设置偏移量,叠加在基础值之上。最终输出值等于基础值加偏移量,常用于手势动画记录起始位置和平移手势的偏移补偿。
- const panX = new Animated.Value(0);
- panX.setOffset(100);
复制代码
偏移管理中有两个容易混淆的方法:flattenOffset 把偏移值合并到基础值中,偏移置零;extractOffset 把偏移值设为基准值,基础值置零。两者输出值在示例中相同,但内部基础值和偏移量的归属不同。
- const anim = new Animated.Value(10);
- anim.setOffset(20);
- // 输出:10 + 20 = 30
- anim.flattenOffset();
- // 基础值变 30,偏移量变 0,输出:30 + 0 = 30
复制代码- const anim = new Animated.Value(10);
- anim.setOffset(20);
- // 输出:10 + 20 = 30
- anim.extractOffset();
- // 偏移量变 30,基础值变 0,输出:0 + 30 = 30
复制代码
手势场景中,常见做法是手势结束时合并偏移,或在下一次手势开始时提取偏移:
- const pan = useRef(new Animated.Value(0)).current;
- const handlePanResponderRelease = () => {
- pan.extractOffset();
- };
- const handleReset = () => {
- pan.flattenOffset();
- };
复制代码
addListener:鸿蒙上最需要警惕的 API
addListener 添加异步监听器,用于观察动画值变化,返回监听器 ID;removeListener 移除指定监听,removeAllListeners 移除全部监听。它常用于实时更新 UI、同步动画值到 state,以及调试动画。组件卸载时一定要移除监听器。
- const anim = new Animated.Value(0);
- const listenerId = anim.addListener(({ value }) => {
- console.log('当前值:', value);
- });
- anim.removeListener(listenerId);
- anim.removeAllListeners();
复制代码
下面是一个把动画值同步到文本的示例:
- const FollowerComponent = () => {
- const anim = useRef(new Animated.Value(0)).current;
- const [text, setText] = useState('');
- useEffect(() => {
- const id = anim.addListener(({ value }) => {
- setText(`当前值: ${value.toFixed(2)}`);
- });
- return () => anim.removeListener(id);
- }, []);
- return <Text>{text}</Text>;
- };
复制代码
鸿蒙注意:useNativeDriver: true 时,addListener 的回调可能不会实时触发,甚至可能完全不触发。因此,依赖监听器同步 state 的逻辑不能假设它一定实时,关键 UI 同步要考虑其他实现方式或降级。
stopAnimation、resetAnimation 与 interpolate
stopAnimation 停止正在运行的动画,并能通过回调拿到最终值,适合在停止后更新 state 以匹配布局位置。
- const anim = new Animated.Value(0);
- Animated.timing(anim, {
- toValue: 100,
- duration: 10000,
- useNativeDriver: true,
- }).start();
- anim.stopAnimation((finalValue) => {
- console.log('动画停止时的值:', finalValue);
- });
复制代码
resetAnimation 停止动画并把值重置为初始值:
- const anim = new Animated.Value(0);
- Animated.timing(anim, {
- toValue: 100,
- duration: 5000,
- useNativeDriver: true,
- }).start();
- anim.resetAnimation(() => {
- console.log('已重置为初始值');
- });
复制代码
interpolate 用于把输入范围映射到输出范围,可以用于透明度、位移、旋转、颜色和缩放等属性。extrapolate 有三个选项:extend 允许超出范围,clamp 限制在范围内,identity 超出范围时返回输入值。
- const anim = new Animated.Value(0);
- const opacity = anim.interpolate({
- inputRange: [0, 1],
- outputRange: [0, 1],
- });
- const translateX = anim.interpolate({
- inputRange: [0, 1],
- outputRange: [0, 100],
- extrapolate: 'clamp',
- });
- const rotate = anim.interpolate({
- inputRange: [0, 1],
- outputRange: ['0deg', '360deg'],
- });
- const color = anim.interpolate({
- inputRange: [0, 0.5, 1],
- outputRange: ['#0A59F7', '#10B981', '#EF4444'],
- });
- const scale = anim.interpolate({
- inputRange: [0, 0.5, 1],
- outputRange: [1, 1.2, 1],
- });
复制代码
鸿蒙注意:颜色插值不支持,interpolate 的颜色映射在鸿蒙上不生效。涉及颜色变化的动画,需要改用其他属性或方案。
完整示例:手势偏移管理
下面这个例子把偏移提取、手势移动和偏移合并串起来,用 Animated.View 的 translateX 驱动位移:
- const PanGesture = () => {
- const panX = useRef(new Animated.Value(0)).current;
- const handleGrant = () => {
- panX.setOffset(panX.__getValue());
- panX.setValue(0);
- };
- const handleMove = (dx) => {
- panX.setValue(dx);
- };
- const handleRelease = () => {
- panX.flattenOffset();
- };
- return (
- <Animated.View style={{ transform: [{ translateX: panX }] }} />
- );
- };
复制代码
注意:__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。不同版本之间可能存在差异,以实际测试结果为准。原文中的代码示例已在鸿蒙设备上测试通过,可直接参考。 |