过去一年,我们持续优化基于 Animated 库的动画性能。动画对打造卓越用户体验至关重要,但实现流畅效果往往面临挑战。我们的目标是让开发者轻松创建高性能动画,无需担心代码可能导致卡顿。
什么是原生驱动?
Animated API 在设计时遵循了核心约束条件:可序列化。这意味着我们能在动画开始前将所有参数发送到原生端,使原生代码直接在 UI 线程执行动画,无需每帧都通过桥接层通信。这种机制特别有价值——当动画启动后,即使 JS 线程被阻塞,动画仍能流畅运行。实际场景中这种情况很常见:用户代码运行在 JS 线程,而 React 渲染也可能长时间占用 JS 线程。
发展历程
该项目始于一年前,当时 Expo 为 Android 构建 li.st 应用。Krzysztof Magiera 受聘完成 Android 端的初始实现。方案运行效果出色,使 li.st 成为首个搭载原生驱动 Animated 动画的应用。数月后,Brandon Withrow 完成了 iOS 端的实现。随后,Ryan Gomba 与我共同完善缺失功能(如支持 Animated.event)并修复生产环境发现的缺陷。这真正体现了社区协作的力量,感谢所有参与者及赞助大部分开发工作的 Expo。该方案现已应用于 React Native 的 Touchable 组件,以及新发布的 React Navigation 库的转场动画。
工作原理
首先了解当前基于 JS 驱动的 Animated 工作流程。使用 Animated 时,您需声明表示动画的节点关系图,然后通过驱动按预设曲线更新 Animated 值。您也可通过 Animated.event 将 Animated 值关联到 View 的事件。

动画执行步骤及位置分解:
-
JS:动画驱动使用 requestAnimationFrame 逐帧执行,根据动画曲线计算新值并更新目标值
-
JS:计算中间值并传递至关联 View 的属性节点
-
JS:通过 setNativeProps 更新 View
-
跨越 JS 到原生桥接层
-
Native:直接更新UIView或android.View
可见大部分工作发生在 JS 线程。若该线程阻塞将导致动画掉帧,且每帧更新都需要通过 JS 到原生桥接层。
原生驱动的核心价值是将全流程迁移至原生端。由于 Animated 生成的是动画节点关系图,可在动画启动时一次性序列化发送到原生端,消除回调 JS 线程的需求;原生代码能直接在 UI 线程逐帧更新视图。
以下演示如何序列化动画值及插值节点(非实际实现,仅作示例):
创建将被动画化的原生值节点:
NativeAnimatedModule.createNode({
id: 1,
type: 'value',
initialValue: 0,
});
创建原生插值节点,用于告知原生驱动如何对值进行插值处理:
NativeAnimatedModule.createNode({
id: 2,
type: 'interpolation',
inputRange: [0, 10],
outputRange: [10, 0],
extrapolate: 'clamp',
});
创建原生属性节点,用于告知原生驱动其关联的视图属性:
NativeAnimatedModule.createNode({
id: 3,
type: 'props',
properties: ['style.opacity'],
});
将节点连接起来:
NativeAnimatedModule.connectNodes(1, 2);
NativeAnimatedModule.connectNodes(2, 3);
将属性节点关联到视图:
NativeAnimatedModule.connectToView(3, ReactNative.findNodeHandle(viewRef));
至此,原生动画模块已具备直接更新原生视图所需的全部信息,无需通过JS线程计算任何值。
最后只需通过指定动画曲线类型和待更新的动画值来启动动画。时序动画可通过在JS中预计算所有动画帧来简化实现,从而减小原生端代码量。
NativeAnimatedModule.startAnimation({
type: 'timing',
frames: [0, 0.1, 0.2, 0.4, 0.65, ...],
animatedValueId: 1,
});
动画运行时实际流程如下:
-
Native:原生动画驱动使用CADisplayLink(iOS)或android.view.Choreographer(Android)逐帧执行,根据动画曲线计算新值并更新驱动的动画值
-
Native:计算中间值并传递给关联原生视图的属性节点
-
Native:直接更新UIView或android.View
可见,整个过程不再依赖JS线程和桥接通信,从而实现更流畅的动画效果!🎉🎉
如何在应用中使用?
常规动画配置非常简单:在启动动画时添加useNativeDriver: true参数即可
修改前:
Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
}).start();
修改后:
Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
useNativeDriver: true,
}).start();
动画值仅兼容单一驱动模式,因此若对某值启用原生驱动,需确保该值的所有动画均使用原生驱动
该机制同样适用于Animated.event,对于需要跟随滚动位置变化的动画尤其重要——未启用原生驱动时,由于React Native的异步特性,动画总会比手势延迟一帧
修改前:
<ScrollView
scrollEventThrottle={16}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.animatedValue } } }]
)}
>
{content}
</ScrollView>
修改后:
<Animated.ScrollView
scrollEventThrottle={1}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.animatedValue } } }],
{ useNativeDriver: true }
)}
>
{content}
</Animated.ScrollView>
注意事项
当前原生动画驱动尚未支持Animated的所有功能。主要限制包括:
- 仅支持非布局属性动画(如
transform和opacity生效,Flexbox和位置属性无效)
Animated.event仅支持直接事件(如ScrollView#onScroll),不支持冒泡事件(故无法用于PanResponder)
原生动画驱动虽已集成在React Native中较长时间,但因处于实验阶段从未正式文档化。建议使用React Native 0.40+版本以获取稳定支持
扩展资源
推荐观看Christopher Chedeau的专题讲座深入了解Animated
若需深度探讨动画技术及原生化如何提升用户体验,可观看Krzysztof Magiera的技术分享