进阶 reactnative.dev 2026-10-11 06:04:21 · 21 阅读

第9章 React Native 动画系统详解:Animated 与 LayoutAnimation

版本:0.77 动画 动画对于打造出色的用户体验至关重要。静止的物体起步需要克服惯性,运动的物体拥有惯性,很少会立即停下。动画让你能在界面中呈现符合物理规律的动态效果。 React Native 提供两套互补的动画系统:Animated 用于精细、交互式地控制具体数值;LayoutAnimation 则处理全局布局的动画事务。 Animated API Animated API 旨在以极高性能的方式,简洁地表达多种有趣的动画和交互模式。它专注于输入与输出之间的声明式关系,中间可配置转换,并提供启动/停止方法来控制基于时间的动画执行。 Animated 导出了六种可动画的组件类型:View、Text、Image、ScrollView、FlatList 和 SectionList。你也可以通过 Animated.createAnimatedComponent() 创建自定义组件。 例如,一个在挂载时淡入显示的容器视图可以这样实现: TypeScriptJavaScript 来拆解一下这里发生了什么。在 FadeInView 的 render 方法中,使用 useRef 初始化了一个名为 fadeAnim 的新 Animated.Value。View 的 opacity 属性映射到了这个动画值。幕后,系统会提取数值并据此设置不透明度。 组件挂载时,不透明度设为 0。随后,在 fadeAnim 上启动一个缓动动画,随着该数值逐帧动画至最终的 1,其所有依赖映射(此处仅为 opacity)都会同步更新。 这一过程经过优化,比调用 setState 并重新渲染更快。由于整个配置都是声明式的,未来我们还能实现进一步优化:将配置序列化,并在高优先级线程上运行动画。 配置动画 动画的可配置性很强。你可以自定义或预置缓动函数、延迟、持续时间、衰减系数、弹簧常数等,具体取决于动画类型。 Animated 提供多种动画类型,其中最常用的是 Animated.timing()。它支持使用各种预定义缓动函数(或自定义函数)对数值随时间进行动画处理。缓动函数通常用于表现物体的加速和减速过程。 默认情况下,timing 使用 easeInOut 曲线,表现为逐渐加速至全速,最后逐渐减速停止。你可以通过传入 easing 参数指定不同的缓动函数,也可自定义持续时间,甚至设置动画开始前的延迟。 例如,如果想创建一个 2 秒的动画,让物体先略微后退,再移动到最终位置: React TSX Animated.timing(this.state.xPosition, {
toValue: 100,
easing: Easing.back(),
duration: 2000,
useNativeDriver: true,
}).start();
想了解内置动画支持的全部配置参数,请参阅 Animated API 参考文档中的「配置动画」部分。 组合动画​ 多个动画可以组合起来,按顺序或并行播放。顺序动画可以在上一个动画结束后立即播放,也可以延迟指定时间后开始。Animated API 提供了 sequence() 和 delay() 等方法,它们都接收一个动画数组,并自动在合适的时机调用 start()/stop()。 例如,下面的动画先减速滑行至停止,然后在旋转的同时弹回原位: React TSXAnimated.sequence([
// 先 decay,再 spring 回起点并旋转
Animated.decay(position, {
// 减速滑行至停止
velocity: {x: gestureState.vx, y: gestureState.vy}, // 手势松开时的速度
deceleration: 0.997,
useNativeDriver: true,
}),
Animated.parallel([
// decay 之后,并行执行:
Animated.spring(position, {
toValue: {x: 0, y: 0}, // 回到起点
useNativeDriver: true,
}),
Animated.timing(twirl, {
// 同时旋转
toValue: 360,
useNativeDriver: true,
}),
]),
]).start(); // 启动这组顺序动画
如果某个动画被停止或中断,组内的其他动画也会一并停止。Animated.parallel 提供了 stopTogether 选项,将其设为 false 即可关闭这一行为。 完整的组合方法列表请参阅 Animated API 参考文档中的「组合动画」部分。 组合动画值​ 两个动画值可以通过加法、乘法、除法或取模运算,组合成一个新的动画值。 有些场景下,一个动画值需要取另一个动画值的倒数来参与计算,比如缩放取倒数(2x --> 0.5x): React TSXconst a = new Animated.Value(1);
const b = Animated.divide(1, a);

Animated.spring(a, {
toValue: 2,
useNativeDriver: true,
}).start();
插值​ 每个属性都可以先经过插值处理。插值将输入范围映射到输出范围,通常使用线性插值,同时也支持缓动函数。默认情况下,它会 extrapolate(外推)给定范围之外的曲线值,但你也可以限制输出值,使其不超出指定范围。 将 0-1 范围转换为 0-100 范围的基本映射如下: React TSXvalue.interpolate({
inputRange: [0, 1],
outputRange: [0, 100],
});
例如,你可以将 Animated.Value 想象为从 0 到 1 变化,但让位置从 150px 动画到 0px,不透明度从 0 动画到 1。只需修改上述示例中的 style 即可实现: React TSX style={{
opacity: this.state.fadeAnim, // 直接绑定
transform: [{
translateY: this.state.fadeAnim.interpolate({
inputRange: [0, 1],
outputRange: [150, 0] // 0 : 150, 0.5 : 75, 1 : 0
}),
}],
}}
interpolate() 还支持多个范围段,这在定义死区及其他技巧时非常有用。例如,若要在 -300 处建立负相关关系,在 -100 处变为 0,在 0 处回到 1,然后在 100 处降回 0,此后所有值都保持在 0 的死区,可以这样实现: React TSXvalue.interpolate({
inputRange: [-300, -100, 0, 100, 101],
outputRange: [300, 0, 1, 0, 0],
});
映射结果如下: Input | Output
------|-------
-400| 450
-300| 300
-200| 150
-100| 0
-50| 0.5
0| 1
50| 0.5
100| 0
101| 0
200| 0
interpolate() 还支持映射到字符串,因此不仅可以动画化数值,还可以动画化颜色等带单位的值。例如,若要动画化旋转角度,可以这样处理: React TSXvalue.interpolate({
inputRange: [0, 360],
outputRange: ['0deg', '360deg'],
});

interpolate() 也支持任意缓动函数,其中许多已在 Easing 模块中实现。此外,interpolate() 还允许配置 outputRange 的延伸行为。通过设置 extrapolate、extrapolateLeft 或 extrapolateRight 选项,可以指定延伸方式。默认值为 extend,也可以使用 clamp 来防止输出值超出 outputRange。

追踪动态值

动画值可以追踪其他值。只需将动画的 toValue 设置为另一个动画值而非普通数字即可实现。例如,类似 Android Messenger 使用的“对话头像”动画,可以通过 spring() 锁定在另一个动画值上实现,也可以使用 timing() 并设置 duration 为 0 来刚性追踪。这些动画还可以与插值组合使用:

Animated.spring(follower, { toValue: leader }).start();
Animated.timing(opacity, {
  toValue: pan.x.interpolate({
    inputRange: [0, 300],
    outputRange: [1, 0],
  }),
  useNativeDriver: true,
}).start();

示例中的 leader 和 follower 动画值可以使用 Animated.ValueXY() 实现。ValueXY 是处理平移或拖拽等二维交互的便捷方式。它作为一个基础包装器,包含两个 Animated.Value 实例,并提供了调用这些实例的辅助函数,因此在许多场景中可以直接替代 Value。在上例中,我们可以使用它来同时追踪 x 和 y 值。

追踪手势

包括平移、滚动在内的手势以及其他事件,可以通过 Animated.event 直接映射到动画值。这需要使用结构化映射语法,以便从复杂的事件对象中提取值。第一层为数组,允许跨多个参数进行映射,该数组包含嵌套的对象。

例如,在处理水平滚动手势时,可以按以下方式将 event.nativeEvent.contentOffset.x 映射到 scrollX(一个 Animated.Value):

onScroll={Animated.event(
  // scrollX = e.nativeEvent.contentOffset.x
  [{
    nativeEvent: {
      contentOffset: {
        x: scrollX
      }
    }
  }]
)}
下面的示例实现了一个水平滚动的轮播图,其中的滚动位置指示器使用 ScrollView 中的 Animated.event 进行动画处理。 ScrollView 搭配 Animated Event 示例​ 使用 PanResponder 时,可以用下面的代码从 gestureState.dx 和 gestureState.dy 中提取 x、y 坐标。数组第一位放 null,因为我们只关心传给 PanResponder 处理函数的第二个参数,也就是 gestureState。 React TSXonPanResponderMove={Animated.event(
[null, // 忽略原生事件
// 从 gestureState 中提取 dx 和 dy
// 相当于 'pan.x = gestureState.dx, pan.y = gestureState.dy'
{dx: pan.x, dy: pan.y}
])}
PanResponder 配合 Animated Event 示例​ 响应当前动画值​ 你或许会注意到,动画运行期间没有明确的方式读取当前数值。这是因为出于性能优化,数值可能仅原生运行时已知。若需根据当前数值执行 JavaScript 逻辑,有两种方法: spring.stopAnimation(callback) 会停止动画,并以最终数值调用 callback。这在处理手势转场时很有用。 spring.addListener(callback) 在动画运行期间异步调用 callback,提供近期的数值值。这适用于触发状态变更,例如当用户拖动选项靠近时,将其吸附到新选项上。与需要 60 fps 运行的连续手势(如平移)相比,这类较大的状态变更对几帧延迟的敏感性较低。 Animated 旨在完全支持序列化,以便独立于常规 JavaScript 事件循环,以高性能方式运行动画。这影响了 API 设计,因此当某些操作看似比完全同步系统更复杂时,请谨记此点。可查看 Animated.Value.addListener 以绕过部分限制,但由于其未来可能存在性能影响,请谨慎使用。 使用原生驱动器​ Animated API 设计为可序列化。通过使用原生驱动器,我们在开始动画前将动画的所有信息发送至原生层,允许原生代码在 UI 线程上执行动画,无需每帧都经过 bridge。动画启动后,即使 JS 线程被阻塞,也不影响动画运行。 对常规动画使用原生驱动器只需在启动动画的配置中设置 useNativeDriver: true。出于遗留原因,未设置 useNativeDriver 属性的动画默认为 false,但会发出警告(在 TypeScript 中会报类型检查错误)。 React TSX Animated.timing(this.state.animatedValue, { toValue: 1, duration: 500, useNativeDriver: true, // <-- 将此设置为 true }).start();

Animated 值只能绑定一个渲染驱动,因此如果对某个值启用了 native driver,该值上的所有动画也都要使用 native driver。

native driver 同样支持 Animated.event。这对跟随滚动位置的动画特别有用,因为没有 native driver,React Native 的异步机制会导致动画始终比手势晚一帧。

React TSX
Animated.ScrollView // 使用 Animated 的 ScrollView 包装器
   onScroll={Animated.event(
    [
     {
      nativeEvent: {
       contentOffset: {y: this.state.animatedValue},
      },
     },
    ],
    {useNativeDriver: true}, // 设置为 true
   )}
   {content}

可以通过运行 RNTester 应用并加载 Native Animated 示例来体验 native driver 的效果。也可以查看源码,了解这些示例的实现方式。

注意事项

目前 native driver 并不支持 Animated 的全部功能。主要限制是只能动画化非布局属性:transform 和 opacity 等可以正常运作,但 Flexbox 和 position 属性不行。使用 Animated.event 时,仅支持直接事件,不支持冒泡事件。这意味着它无法配合 PanResponder 使用,但可以用于 ScrollView#onScroll 等场景。

动画运行期间可能会阻止 VirtualizedList 组件渲染更多行。如果需要在用户滚动列表时运行较长或循环的动画,可以在动画配置中设置 isInteraction: false 来避免此问题。

须知

使用 rotateY、rotateX 等 transform 样式时,务必添加 perspective。当前情况下,部分动画在 Android 上若缺少 perspective 可能无法渲染。示例如下。

React TSX
Animated.View
   style={{
    transform: [
     {scale: this.state.scale},
     {rotateY: this.state.rotateY},
     {perspective: 1000}, // 在 Android 上缺少此行会导致动画无法渲染,而 iOS 表现正常
    ],
   }}
/>

更多示例​

RNTester 应用中包含多个使用 Animated 的示例:

  • AnimatedGratuitousApp
  • NativeAnimationsExample

LayoutAnimation API​

LayoutAnimation 允许你全局配置创建和更新的动画,这些动画会作用于下一次渲染/布局周期中的所有视图。这在做 Flexbox 布局更新时非常实用——无需费心测量或计算具体属性再去直接驱动动画。当布局变化会影响到祖先组件时,它尤其有用:比如展开"查看更多"时父容器变大、下方一行被往下推,这种情况通常需要各组件显式协调才能同步动画,而 LayoutAnimation 可以自动搞定。

需要注意,虽然 LayoutAnimation 功能强大且相当好用,但它提供的控制能力远不如 Animated 和其他动画库。如果用它达不到你想要的效果,可能就得换一种方式了。

另外,在 Android 上要让 LayoutAnimation 生效,需要通过 UIManager 设置以下开关:

React TSXUIManager.setLayoutAnimationEnabledExperimental(true);

该示例使用了预设值,你可以根据需求自定义动画,更多信息请参考 LayoutAnimation.js。 补充说明 requestAnimationFrame requestAnimationFrame 是浏览器中的一个 polyfill,你可能比较熟悉。它接收一个函数作为唯一参数,并在下次重绘前调用该函数。它是基于 JavaScript 的动画 API 的核心基础组件。通常无需手动调用此方法,因为动画 API 会自动管理帧更新。 setNativeProps 如“直接操纵”一节所述,setNativeProps 允许直接修改由原生视图支撑的组件(区别于复合组件)的属性,无需通过 setState 重新渲染组件层级。 在 Rebound 示例中,我们可以利用它来更新缩放比例。如果待更新的组件嵌套较深且未通过 shouldComponentUpdate 进行优化,这种方法会很有帮助。 如果动画出现掉帧(帧率低于 60 FPS),请考虑使用 setNativeProps 或 shouldComponentUpdate 进行优化。或者,通过 useNativeDriver 选项将动画运行在 UI 线程而非 JavaScript 线程上。此外,建议使用 InteractionManager 将计算密集型工作推迟到动画完成后执行。你可以通过内置开发菜单中的“FPS 监视器”工具来监控帧率。

评论 (0)