动画
什么是动画
React Native 提供了丰富的动画 API,以满足实际业务中各种场景下的动画需求。React Native 的动画在性能上是高效的,特别是在 Native Driver 推出之后,React Native 的动画性能几乎可媲美原生应用。
人眼观看物体时,成像于视网膜上,并由视神经输入人脑,感觉到物体的像。物体在快速运动时, 当人眼所看到的影像消失后,人眼仍能继续保留其影像0.1-0.4秒左右的图像,这种现象被称为视觉暂留。
动画,则是在屏幕上,将一段连续的动作拆成一系列离散的静止画面,并在屏幕上快速逐个播放。利用视觉暂留这一生理现象,前一帧(或多帧)的画面会和当前帧叠加,并让人感觉动作是连续发生的。
React Native 动画的原理也是基于此。例如,当我们要播放一段让方块从左侧移动到右侧的动画,我需要在手机屏幕上逐次画出方块移动的每一个中间状态。说得详细一点,我们需要……
- 绘制出方块在初始位置的画面;
- 等待一个短暂的时间,然后更新画面,让方块往目标位置的方向稍微移动一点点;
- 再等待一个短暂的时间,再让方块往目标位置方向稍微移动一点点;
- 重复步骤 3,直到方块抵达目标位置。
我们注意到,我们画出了很多帧(画面),帧与帧之间,有一个时间间隔。这个时间间隔在理想情况应该是等长的,但因为种种原因(如业务逻辑执行时间的不确定性、设备的性能限制、线程被抢占),实际设备最终渲染出的动画的帧间隔时间并不能完美地等长。此时,人在看动画时会有不自然的感觉,从而影响体验。因此,帧间隔时间能否尽可能等长,是我们评判动画好坏的标准之一。
此外帧间隔时间是一个很短暂的时间,到底该多短暂呢?人类视觉的时间敏感性和分辨率根据视觉刺激的类型和特征而变化,并且在个体之间是不同的。由于人类眼睛的特殊生理结构,如果所看画面的帧间隔时间处于83毫秒-100毫秒的时候,就会认为是连贯的。
但我们一般不使用帧间隔时间来,而使用另一个概念——帧率。所谓帧率,是指动画一秒内能实际播放的帧数。显然,帧间时间越短,帧率越大。一般认为,帧率低于 23,则人在观看动画时会感受到明显的卡顿和不自然。对于移动端 App,理想情况下,帧率应该稳定在 30。因此,帧率是否能尽可能稳定在 30,也是我们评判动画好坏的标准之一。
一段动画由很多帧静止的画面构成,但是帧与帧之间的地位并不是平等的。一般,我们认为第一帧和最后一帧很重要,我们把它们称之为关键帧。还是以如上移动方块的动画为例,描述方块初始位置的首帧和描述方块目标位置的末帧都是关键帧。当我们知道了方块的始末位置,我们就可以算出这段动画中,任意一个时刻,方块所在的位置。换句话说,我们仅仅知道关键帧的信息,而无需知道其他帧的信息,即可描述出这段动画的所有细节。因此,我们把非关键帧称之为补间帧。
渐入动画效果例子
让我们从一个简单的例子开始,来逐渐窥探 React Native 动画的全貌。
import React from 'react';
import { Animated, Text, View } from 'react-native';
export default class App extends React.Component {
constructor() {
state = {
fadeAnim: new Animated.Value(0),
}
}
componentDidMount() {
Animated.timing(
this.state.fadeAnim,
{
toValue: 1,
duration: 10000,
}
).start();
}
render() {
return (
<Animated.View
style={{
width: 250,
height: 50,
backgroundColor: "powderblue",
opacity: this.state.fadeAnim,
}}
>
<Text style={{fontSize: 28, textAlign: 'center', margin: 10}}>渐入文本</Text>
</Animated.View>
);
}
}
稍后我们会逐步分析这段代码。这段代码执行的结果,我们最开始将看到白茫茫的一片,然后,从空白的背景中,渐入一个青底黑字的「渐入文本」字样。
这个「渐入文本」字样可以用 Text
组件来做。整个动画过程中,React Native 将 Text
组件的 opacity
属性(表示不透明度,该值越小,则越透明,为 0 时完全看不见,为 1 时完全清晰可见)从 0 逐渐增加到了 1。
为了描述这个随着动画进行而一直变化的不透明度,我们给组件的 state
加上一个成员 fadeAnim
。
state = {
fadeAnim: new Animated.Value(0),
}
这里我们用到了 new Animated.Value(0)
这个对象表示一个可随着动画变化的值,一开始它是 0 。这个值最终被写到了 Animated.View
的 props.style
中。
<Animated.View
style={{
width: 250,
height: 50,
backgroundColor: "powderblue",
opacity: this.state.fadeAnim,
}}
>
<Text style={{fontSize: 28, textAlign: 'center', margin: 10}}>渐入文本</Text>
</Animated.View>
对于 Animated.View
,我们可以理解为它就是 View
,只不过它的style
属性的值可以是Animated.Value
类型的实例。当我们通过 opacity: this.state.fadeAnim
的形式,将它的 opacity
和 fadeAnim
绑定在一起时,在动画播放过程中,随着 state.fadeAnim
的值逐渐变大,Animated.View
的不透明度也会被驱动着逐渐变大。
之后,我们通过启动一个动画来修改 state.fadeAnim
的值,来驱动 Animated.View
的不透明度变化。
Animated.timing(
this.state.fadeAnim,
{
toValue: 1,
duration: 10000,
}
).start();
这里,我们通过调用Animated.timing(…)
来生成一个 timing 类型的动画。这个动画和 this.state.fadeAnim
绑定,其中 toValue:1
表明动画的目标值是 1,duration: 10000
表明这个动画会持续 10000 毫秒。最后,我们调用 .start()
方法启动这个动画。由于 this.state.fadeAnim
的初始值是 0,这个动画会在 1000 毫秒内将 this.state.fadAnim
的值从 0 连续地变化到 1。从而驱动 Animated.View
的不透明度从 0 变化到 1。
一种动画的伪实现
通过上一章节的例子,我们知道了一个最简单的 React Native 动画应该如何实现。同时,通过这个例子,我们认识到了一大堆 React Native 动画的概念。如果你拥有其他拥有动画功能的框架或平台的开发经验,也许你能立即理解这些 React Native 的动画概念——它们不过是改头换面了一次罢了。但是如果你对这些概念还没有什么概念,不用担心,接下来通过这个动画的伪实现例子,便可体会这些概念到底对我们有何用处。
接下来,我们假装并不知道 React Native 的动画 API。这种情况下,我们如何做一个类似的动画效果呢?
import React from 'react';
import { Animated, Text, View } from 'react-native';
export default class App extends React.Component {
constructor() {
state = {
fadeAnim: 0,
}
}
render() {
return (
<View
style={{
width: 250,
height: 50,
backgroundColor: "powderblue",
opacity: this.state.fadeAnim,
}}
>
<Text style={{fontSize: 28, textAlign: 'center', margin: 10}}>渐入文本</Text>
</View>
);
}
}
注意到,我们直接将 state.fadeAnim
初始化为 0。现在,我们没有 Animated.Value
,也没有 Animated.View
,更没有 Animated.timing()
。一切只能靠我们自己了。
基于动画的原理,动画不过是每隔一个短暂的时间,就把屏幕上的图形的某个特征改变一点点,依次循环直到该特征达到目标值。因此,我们可以通过 setTimeout
方法,来重复修改 state.fadeAnim
以展现动画效果。
现在,我们插入如下方法。
componentDidMount() {
const toValue = 1;
const duration = 10000;
const frameInterval = 34; // 帧率为 30 时 1000/30 === 34
let frameIndex = 0;
const updateFadeAnim = () => {
const pastTimeInterval = frameInterval * frameIndex;
this.setState({
fadeAnim: (pastTimeInterval / duration) * toValue,
});
if (pastTimeInterval < duration) {
setTimeout(updateFadeAnim, frameInterval);
}
};
setTimeout(updateFadeAnim, frameInterval);
}
这段代码里,我们在动画开始后,每隔 34 毫秒调用一次 updateFadeAnim()
函数,直到 10000 毫秒之后停止。updateFadeAnim()
每被调用一次,都会算出当前不透明度的值,并通过 setState()
方法更新到 state.fadeAdmin
上。这个过程会触发 render()
方法,更新不透明度,并最终渲染到手机屏幕上。
看,我们已经自己实现了一个动画效果。React Native 动画 API 不过是把我们实现的效果封装了一下罢了。如此一来,我们就好理解之前提及的 React Native 动画的概念了。
Animated.timing()
定义了 timing 个动画。这个动画可以通过调用.start()
启动。它内部有一套类似setTimeout
的机制(但不完全是)会每隔一段时间,重复的、循环的、逐步的修改绑定的值。new Animated.Value(0)
,用来表明在动画中会变化的值,它可以绑定到Animated.timing()
中。Animated.View
,它是一种特殊的View
,其props.style
可以绑定Animated.Value
的实例,从而被某个动画驱动。