动画
本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →
动画对于打造出色的用户体验至关重要。静止物体开始移动时必须克服惯性,运动中的物体具有动量,很少会立即停止。动画能帮助你在界面中呈现符合物理规律的运动效果。
React Native 提供两套互补的动画系统:Animated 用于对特定值进行精细的交互控制,LayoutAnimation 则用于全局布局变换的动画处理。
Animated API
Animated API 旨在以高性能方式简洁地表达各种有趣的动画和交互模式。Animated 专注于输入与输出之间的声明式关系,支持可配置的转换过程,并通过 start/stop 方法控制基于时间的动画执行。
Animated 导出六种可动画化组件类型:View、Text、Image、ScrollView、FlatList 和 SectionList,你还可以使用 Animated.createAnimatedComponent() 创建自定义组件。
例如,一个在挂载时淡入的容器视图可能如下所示:
- TypeScript
- JavaScript
让我们解析这段代码:在 FadeInView 的渲染方法中,通过 useRef 初始化了名为 fadeAnim 的 Animated.Value。View 的不透明度属性映射到这个动画值。在底层,系统会提取数值并用于设置透明度。
组件挂载时,不透明度被设为 0。随后在 fadeAnim 动画值上启动缓动动画,该值在逐帧变化至最终值 1 的过程中,会更新所有依赖映射(本例中仅不透明度)。
这种优化实现方式比调用 setState 和重新渲染更快。由于整个配置是声明式的,未来还能实现更多优化,例如序列化配置并在高优先级线程运行动画。
配置动画
动画具有高度可配置性。根据动画类型不同,可调整自定义/预定义的缓动函数、延迟时间、持续时间、衰减因子、弹簧系数等参数。
Animated 提供多种动画类型,最常用的是 Animated.timing()。它支持使用预定义缓动函数随时间变化动画值,也支持自定义函数。缓动函数通常用于表现物体逐渐加速或减速的运动过程。
默认情况下,timing 采用 easeInOut 曲线,呈现逐渐加速至全速再逐渐减速停止的效果。可通过 easing 参数指定不同缓动函数,还支持自定义 duration 甚至动画开始前的 delay。
例如,要创建持续 2 秒的动画,让物体先略微回退再移动到最终位置:
Animated.timing(this.state.xPosition, {
toValue: 100,
easing: Easing.back(),
duration: 2000,
useNativeDriver: true,
}).start();
查看 Animated API 参考文档的配置动画部分,了解内置动画支持的所有配置参数。
组合动画
动画可以组合并按顺序或并行播放。顺序动画可在前一个动画结束后立即播放,也可延迟指定时间启动。Animated API 提供 sequence()、delay() 等方法,它们接收动画数组并自动按需调用 start()/stop()。
例如,以下动画先滑行停止,然后同时回弹并旋转:
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倍):
const a = new Animated.Value(1);
const b = Animated.divide(1, a);
Animated.spring(a, {
toValue: 2,
useNativeDriver: true,
}).start();
插值
每个属性都可以先经过插值处理。插值将输入范围映射到输出范围,通常采用线性插值方式,但也支持缓动函数。默认情况下,它会推断给定范围之外的曲线走向,你也可以将其设置为钳制输出值。
将0-1范围基本映射到0-100范围的示例如下:
value.interpolate({
inputRange: [0, 1],
outputRange: [0, 100],
});
例如,你可能希望将 Animated.Value 视为从0到1的变化,但将位置从150px动画到0px,并将不透明度从0动画到1。这可以通过修改上面示例中的 style 来实现,如下所示:
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),您可以这样实现:
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() 还支持映射到字符串,让你既能动画化颜色也能动画化带单位的数值。例如,要动画化旋转效果,可以这样实现:
value.interpolate({
inputRange: [0, 360],
outputRange: ['0deg', '360deg'],
});
interpolate() 还支持任意的缓动函数,其中许多已在 Easing 模块中实现。interpolate() 还具有可配置的行为用于外推 outputRange。你可以通过设置 extrapolate、extrapolateLeft 或 extrapolateRight 选项来配置外推方式。默认值为 extend,但你可以使用 clamp 来防止输出值超出 outputRange。
跟踪动态值
动画值可通过将 toValue 设置为另一个动画值(而非固定数值)来追踪动态变化。例如实现类似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 实例及辅助函数,在多数场景中 ValueXY 可直接替代 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 示例
使用 PanResponder 时,可以通过以下代码从 gestureState.dx 和 gestureState.dy 提取 x 和 y 坐标。我们在数组首位使用 null,因为我们只关注传递给 PanResponder 处理函数的第二个参数——即 gestureState。
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 中会报类型错误)。
Animated.timing(this.state.animatedValue, {
toValue: 1,
duration: 500,
useNativeDriver: true, // <-- Set this to true
}).start();
动画值仅兼容单一驱动模式,因此若对某值启用原生驱动,需确保该值的所有动画均使用原生驱动
原生驱动同样适用于 Animated.event。这在跟随滚动位置的动画中尤其重要——若未启用原生驱动,由于 React Native 的异步特性,动画将始终滞后手势一帧。
<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 应用,然后加载原生动画示例来查看原生驱动的实际效果。你也可以查看源代码了解这些示例的制作方式。
注意事项
原生驱动目前不支持 Animated 的所有功能。主要限制包括:
- 仅支持动画化非布局属性:
transform和opacity有效,但 Flexbox 和定位属性无效 Animated.event仅支持直接事件(如ScrollView#onScroll),不支持冒泡事件(故不兼容PanResponder)
动画运行时可能阻止 VirtualizedList 渲染新行项。若需在用户滚动列表时执行长时/循环动画,可在动画配置中添加 isInteraction: false 避免此问题。
重要提示
在使用诸如 rotateY、rotateX 等变换样式时,请确保同时设置 perspective 变换样式。目前某些动画在 Android 平台上缺少此样式可能无法正常渲染。示例如下:
<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 允许全局配置将在下一个渲染/布局周期中应用于所有视图的 create 和 update 动画。这在处理 Flexbox 布局更新时特别有用,无需测量或计算特定属性即可直接实现动画效果。当布局变更影响父级元素时尤其便利,例如展开"查看更多"内容会同时增大父容器尺寸并下推后续行元素,若手动实现则需要组件间显式协调才能同步动画。
请注意,尽管 LayoutAnimation 功能强大且实用,但其控制粒度远不如 Animated 和其他动画库。若无法通过 LayoutAnimation 实现预期效果,可能需要采用其他方案。
注意:要在 Android 上生效,需通过 UIManager 设置以下标志:
UIManager.setLayoutAnimationEnabledExperimental(true);
本示例使用预设值,您可根据需求自定义动画配置,详见 LayoutAnimation.js。
补充说明
requestAnimationFrame
requestAnimationFrame 是浏览器环境的 polyfill 方法,开发者可能已熟悉其用法。它接收函数作为唯一参数,并在下次重绘前调用该函数。作为所有 JavaScript 动画 API 的基础构件,它负责管理动画帧更新。通常无需手动调用此方法——动画 API 会自动处理帧更新。
setNativeProps
如直接操作章节所述,setNativeProps 允许直接修改原生组件(由原生视图支撑,而非复合组件)的属性,无需通过 setState 触发组件树重渲染。
可在 Rebound 示例中使用此方法更新缩放比例——当待更新的组件嵌套层级较深且未实现 shouldComponentUpdate 优化时,这种方法尤其有效。
若发现动画掉帧(帧率低于 60 FPS),可尝试通过 setNativeProps 或 shouldComponentUpdate 进行优化。也可使用 useNativeDriver 选项在 UI 线程而非 JavaScript 线程运行动画。此外建议使用 InteractionManager 将计算密集型任务延迟到动画结束后执行。可通过应用内开发者菜单的"FPS Monitor"工具监控帧率。