跳至主内容
版本:0.79

动画

非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

动画对于打造出色的用户体验至关重要。静止物体开始移动时必须克服惯性,运动中的物体具有动量,很少会立即停止。动画能帮助你在界面中呈现符合物理规律的运动效果。

React Native 提供两套互补的动画系统:Animated 用于对特定值进行精细的交互控制,LayoutAnimation 则用于全局布局变换的动画处理。

Animated API

Animated API 旨在以高性能方式简洁地表达各种有趣的动画和交互模式。Animated 专注于输入与输出之间的声明式关系,支持可配置的转换过程,并通过 start/stop 方法控制基于时间的动画执行。

Animated 导出六种可动画化组件类型:ViewTextImageScrollViewFlatListSectionList,你还可以使用 Animated.createAnimatedComponent() 创建自定义组件。

例如,一个在挂载时淡入的容器视图可能如下所示:

让我们解析这段代码:在 FadeInView 的渲染方法中,通过 useRef 初始化了名为 fadeAnimAnimated.ValueView 的不透明度属性映射到这个动画值。在底层,系统会提取数值并用于设置透明度。

组件挂载时,不透明度被设为 0。随后在 fadeAnim 动画值上启动缓动动画,该值在逐帧变化至最终值 1 的过程中,会更新所有依赖映射(本例中仅不透明度)。

这种优化实现方式比调用 setState 和重新渲染更快。由于整个配置是声明式的,未来还能实现更多优化,例如序列化配置并在高优先级线程运行动画。

配置动画

动画具有高度可配置性。根据动画类型不同,可调整自定义/预定义的缓动函数、延迟时间、持续时间、衰减因子、弹簧系数等参数。

Animated 提供多种动画类型,最常用的是 Animated.timing()。它支持使用预定义缓动函数随时间变化动画值,也支持自定义函数。缓动函数通常用于表现物体逐渐加速或减速的运动过程。

默认情况下,timing 采用 easeInOut 曲线,呈现逐渐加速至全速再逐渐减速停止的效果。可通过 easing 参数指定不同缓动函数,还支持自定义 duration 甚至动画开始前的 delay

例如,要创建持续 2 秒的动画,让物体先略微回退再移动到最终位置:

tsx
Animated.timing(this.state.xPosition, {
toValue: 100,
easing: Easing.back(),
duration: 2000,
useNativeDriver: true,
}).start();

查看 Animated API 参考文档的配置动画部分,了解内置动画支持的所有配置参数。

组合动画

动画可以组合并按顺序或并行播放。顺序动画可在前一个动画结束后立即播放,也可延迟指定时间启动。Animated API 提供 sequence()delay() 等方法,它们接收动画数组并自动按需调用 start()/stop()

例如,以下动画先滑行停止,然后同时回弹并旋转:

tsx
Animated.sequence([
// decay, then spring to start and twirl
Animated.decay(position, {
// coast to a stop
velocity: {x: gestureState.vx, y: gestureState.vy}, // velocity from gesture release
deceleration: 0.997,
useNativeDriver: true,
}),
Animated.parallel([
// after decay, in parallel:
Animated.spring(position, {
toValue: {x: 0, y: 0}, // return to start
useNativeDriver: true,
}),
Animated.timing(twirl, {
// and twirl
toValue: 360,
useNativeDriver: true,
}),
]),
]).start(); // start the sequence group

若组内某个动画被停止或中断,则其他动画也会停止。Animated.parallel 提供 stopTogether 选项,设置为 false 可禁用此行为。

你可以在 Animated API 参考文档的组合动画章节中找到完整的组合方法列表。

组合动画值

你可以通过加法、乘法、除法或取模运算来组合两个动画值,从而生成一个新的动画值。

在某些情况下,动画值需要取另一个动画值的倒数进行计算。例如缩放取反的场景(2倍变为0.5倍):

tsx
const a = new Animated.Value(1);
const b = Animated.divide(1, a);

Animated.spring(a, {
toValue: 2,
useNativeDriver: true,
}).start();

插值

每个属性都可以先经过插值处理。插值将输入范围映射到输出范围,通常采用线性插值方式,但也支持缓动函数。默认情况下,它会推断给定范围之外的曲线走向,你也可以将其设置为钳制输出值。

将0-1范围基本映射到0-100范围的示例如下:

tsx
value.interpolate({
inputRange: [0, 1],
outputRange: [0, 100],
});

例如,你可能希望将 Animated.Value 视为从0到1的变化,但将位置从150px动画到0px,并将不透明度从0动画到1。这可以通过修改上面示例中的 style 来实现,如下所示:

tsx
  style={{
opacity: this.state.fadeAnim, // Binds directly
transform: [{
translateY: this.state.fadeAnim.interpolate({
inputRange: [0, 1],
outputRange: [150, 0] // 0 : 150, 0.5 : 75, 1 : 0
}),
}],
}}

interpolate() 同样支持多范围分段,这对于定义死区和其他实用技巧非常方便。例如,要在 -300 处建立负向关系(值降至 0),在 -100 处回升至 1,在 0 处再次降至 0,然后在 100 之后设置保持为 0 的死区(该区域外的所有值均保持为 0),您可以这样实现:

tsx
value.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() 还支持映射到字符串,让你既能动画化颜色也能动画化带单位的数值。例如,要动画化旋转效果,可以这样实现:

tsx
value.interpolate({
inputRange: [0, 360],
outputRange: ['0deg', '360deg'],
});

interpolate() 还支持任意的缓动函数,其中许多已在 Easing 模块中实现。interpolate() 还具有可配置的行为用于外推 outputRange。你可以通过设置 extrapolateextrapolateLeftextrapolateRight 选项来配置外推方式。默认值为 extend,但你可以使用 clamp 来防止输出值超出 outputRange

跟踪动态值

动画值可通过将 toValue 设置为另一个动画值(而非固定数值)来追踪动态变化。例如实现类似Messenger的"聊天头像"动画效果:用 spring() 绑定到其他动画值,或使用 timing() 并设 duration 为0实现刚性追踪。它们还能与插值组合使用:

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

leaderfollower 动画值可通过 Animated.ValueXY() 实现。ValueXY 是处理二维交互(如平移/拖拽)的便捷工具,它封装了两个 Animated.Value 实例及辅助函数,在多数场景中 ValueXY 可直接替代 Value。这样我们就能在上述示例中同时追踪x和y值。

追踪手势

手势操作(如平移/滚动)和其他事件可通过 Animated.event 直接映射到动画值。该功能采用结构化映射语法,支持从复杂事件对象中提取值。第一层是数组结构,用于跨多个参数映射,数组内包含嵌套对象。

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

tsx
 onScroll={Animated.event(
// scrollX = e.nativeEvent.contentOffset.x
[{nativeEvent: {
contentOffset: {
x: scrollX
}
}
}]
)}

以下示例实现了一个水平滚动轮播组件,其滚动位置指示器通过 ScrollView 中使用的 Animated.event 实现动画效果

使用动画事件的 ScrollView 示例

使用 PanResponder 时,可以通过以下代码从 gestureState.dxgestureState.dy 提取 x 和 y 坐标。我们在数组首位使用 null,因为我们只关注传递给 PanResponder 处理函数的第二个参数——即 gestureState

tsx
onPanResponderMove={Animated.event(
[null, // ignore the native event
// extract dx and dy from gestureState
// like '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 并提供最新值。这适用于触发状态变更,例如当用户拖动时让悬浮球吸附到新位置。因为这类宏观状态变化对少量帧延迟不敏感,而连续手势(如平移)需要保持 60fps 的流畅度。

Animated 设计为完全可序列化,以实现高性能运行(独立于常规 JavaScript 事件循环)。这会影响 API 设计,因此当某些操作比全同步系统更复杂时请理解其设计初衷。可通过 Animated.Value.addListener 部分解决限制,但请谨慎使用——未来版本可能影响性能。

使用原生驱动

Animated API 设计为可序列化。通过启用原生驱动,我们在动画开始前将所有参数发送到原生层,使得原生代码能在 UI 线程执行动画而无需每帧跨桥通信。动画启动后,即使 JS 线程阻塞也不会影响动画效果。

为普通动画启用原生驱动只需在启动配置中添加 useNativeDriver: true。未指定 useNativeDriver 属性的动画会因兼容性考虑默认为 false,但会触发警告(TypeScript 中会报类型错误)。

tsx
Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
useNativeDriver: true, // <-- Set this to true
}).start();

动画值仅兼容单一驱动模式,因此若对某值启用原生驱动,需确保该值的所有动画均使用原生驱动

原生驱动同样适用于 Animated.event。这在跟随滚动位置的动画中尤其重要——若未启用原生驱动,由于 React Native 的异步特性,动画将始终滞后手势一帧。

tsx
<Animated.ScrollView // <-- Use the Animated ScrollView wrapper
onScroll={Animated.event(
[
{
nativeEvent: {
contentOffset: {y: this.state.animatedValue},
},
},
],
{useNativeDriver: true}, // <-- Set this to true
)}>
{content}
</Animated.ScrollView>

可通过运行 RNTester 应用查看原生驱动效果,加载 Native Animated 示例即可。也可参考源码了解实现原理。

注意事项

原生驱动目前不支持 Animated 的所有功能。主要限制包括:

  • 仅支持动画化非布局属性:transformopacity 有效,但 Flexbox 和定位属性无效
  • Animated.event 仅支持直接事件(如 ScrollView#onScroll),不支持冒泡事件(故不兼容 PanResponder

动画运行时可能阻止 VirtualizedList 渲染新行项。若需在用户滚动列表时执行长时/循环动画,可在动画配置中添加 isInteraction: false 避免此问题。

重要提示

在使用诸如 rotateYrotateX 等变换样式时,请确保同时设置 perspective 变换样式。目前某些动画在 Android 平台上缺少此样式可能无法正常渲染。示例如下:

tsx
<Animated.View
style={{
transform: [
{scale: this.state.scale},
{rotateY: this.state.rotateY},
{perspective: 1000}, // without this line this Animation will not render on Android while working fine on iOS
],
}}
/>

更多示例

RNTester 应用提供了多种 Animated 的使用示例:

LayoutAnimation API

LayoutAnimation 允许全局配置将在下一个渲染/布局周期中应用于所有视图的 createupdate 动画。这在处理 Flexbox 布局更新时特别有用,无需测量或计算特定属性即可直接实现动画效果。当布局变更影响父级元素时尤其便利,例如展开"查看更多"内容会同时增大父容器尺寸并下推后续行元素,若手动实现则需要组件间显式协调才能同步动画。

请注意,尽管 LayoutAnimation 功能强大且实用,但其控制粒度远不如 Animated 和其他动画库。若无法通过 LayoutAnimation 实现预期效果,可能需要采用其他方案。

注意:要在 Android 上生效,需通过 UIManager 设置以下标志:

tsx
UIManager.setLayoutAnimationEnabledExperimental(true);

本示例使用预设值,您可根据需求自定义动画配置,详见 LayoutAnimation.js

补充说明

requestAnimationFrame

requestAnimationFrame 是浏览器环境的 polyfill 方法,开发者可能已熟悉其用法。它接收函数作为唯一参数,并在下次重绘前调用该函数。作为所有 JavaScript 动画 API 的基础构件,它负责管理动画帧更新。通常无需手动调用此方法——动画 API 会自动处理帧更新。

setNativeProps

直接操作章节所述,setNativeProps 允许直接修改原生组件(由原生视图支撑,而非复合组件)的属性,无需通过 setState 触发组件树重渲染。

可在 Rebound 示例中使用此方法更新缩放比例——当待更新的组件嵌套层级较深且未实现 shouldComponentUpdate 优化时,这种方法尤其有效。

若发现动画掉帧(帧率低于 60 FPS),可尝试通过 setNativePropsshouldComponentUpdate 进行优化。也可使用 useNativeDriver 选项在 UI 线程而非 JavaScript 线程运行动画。此外建议使用 InteractionManager 将计算密集型任务延迟到动画结束后执行。可通过应用内开发者菜单的"FPS Monitor"工具监控帧率。