第9章 React Native 动画系统详解:Animated 与 LayoutAnimation
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);