动画
动画对于创造良好的用户体验非常重要。静止的物体在开始移动时必须克服惯性。运动中的物体具有动量,很少会立即停止。动画允许您在界面中传达物理上可信的运动。
React Native 提供了两种互补的动画系统:Animated
用于对特定值的细粒度和交互式控制,以及 LayoutAnimation
用于动画全局布局事务。
Animated
API
Animated
API 旨在以非常高效的方式简洁地表达各种有趣的动画和交互模式。Animated
专注于输入和输出之间的声明式关系,以及介于两者之间的可配置转换,以及 start
/stop
方法来控制基于时间的动画执行。
Animated
导出了六种可动画组件类型:View
、Text
、Image
、ScrollView
、FlatList
和 SectionList
,但您也可以使用 Animated.createAnimatedComponent()
创建自己的组件。
例如,一个在挂载时淡入的容器视图可能如下所示
- TypeScript
- JavaScript
让我们分解一下这里发生了什么。在 FadeInView
构造函数中,一个名为 fadeAnim
的新的 Animated.Value
作为 state
的一部分进行初始化。View
上的 opacity 属性映射到此动画值。在幕后,数值会被提取并用于设置 opacity。
当组件挂载时,opacity 被设置为 0。然后,在 fadeAnim
动画值上启动一个缓动动画,该动画将在每一帧上更新其所有依赖映射(在本例中,只有 opacity),因为该值动画到最终值 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 参考的 组合动画 部分找到组合方法的完整列表。
组合动画值
您可以通过加法、乘法、除法或取模运算 组合两个动画值 以创建一个新的动画值。
在某些情况下,动画值需要反转另一个动画值以进行计算。例如,反转比例 (2x --> 0.5x)
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,并将 opacity 从 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 处的否定关系,在 -100 处变为 0,然后在 0 处返回到 1,然后在 100 处返回到零,然后是保持在 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 在 Android 上使用的“聊天头像”动画可以使用固定在另一个动画值上的 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
是一种处理 2D 交互(如平移或拖动)的便捷方法。它是一个基本包装器,包含两个 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
响应当前动画值
您可能会注意到,在动画过程中没有明确的方法来读取当前值。这是因为由于优化,该值可能仅在原生运行时中已知。如果您需要根据当前值运行 JavaScript,则有两种方法
spring.stopAnimation(callback)
会停止动画并在动画结束时使用最终值调用callback
。这在进行手势过渡时非常有用。spring.addListener(callback)
会在动画运行时异步调用callback
,并提供最新的值。这对于触发状态更改非常有用,例如,当用户拖动一个按钮使其更靠近新选项时,将其吸附到新选项上,因为与平移等需要以 60 fps 运行的连续手势相比,这些较大的状态更改对几帧的延迟不太敏感。
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 值仅与一个驱动程序兼容,因此,如果在启动某个值的动画时使用了原生驱动,则确保该值的每个动画也使用原生驱动。
原生驱动也适用于 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 应用,然后加载 Native Animated 示例来查看原生驱动程序的实际效果。您还可以查看源代码,了解这些示例是如何生成的。
注意事项
并非所有可以使用 Animated
执行的操作都受原生驱动程序支持。主要的限制是您只能动画化非布局属性:例如 transform
和 opacity
可以工作,但 Flexbox 和位置属性则不行。在使用 Animated.event
时,它仅适用于直接事件,而不适用于冒泡事件。这意味着它不适用于 PanResponder
,但适用于 ScrollView#onScroll
等事件。
当动画正在运行时,它可能会阻止 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 帧),请考虑使用 setNativeProps
或 shouldComponentUpdate
来优化它们。或者,您可以使用 useNativeDriver 选项在 UI 线程而不是 JavaScript 线程上运行动画。您可能还想使用InteractionManager将任何计算密集型工作推迟到动画完成后再执行。您可以使用应用内开发菜单的“FPS 监视器”工具监控帧速率。