动画

什么是动画

React Native 提供了丰富的动画 API,以满足实际业务中各种场景下的动画需求。React Native 的动画在性能上是高效的,特别是在 Native Driver 推出之后,React Native 的动画性能几乎可媲美原生应用。

人眼观看物体时,成像于视网膜上,并由视神经输入人脑,感觉到物体的像。物体在快速运动时, 当人眼所看到的影像消失后,人眼仍能继续保留其影像0.1-0.4秒左右的图像,这种现象被称为视觉暂留

动画,则是在屏幕上,将一段连续的动作拆成一系列离散的静止画面,并在屏幕上快速逐个播放。利用视觉暂留这一生理现象,前一帧(或多帧)的画面会和当前帧叠加,并让人感觉动作是连续发生的。

React Native 动画的原理也是基于此。例如,当我们要播放一段让方块从左侧移动到右侧的动画,我需要在手机屏幕上逐次画出方块移动的每一个中间状态。说得详细一点,我们需要……

  1. 绘制出方块在初始位置的画面;
  2. 等待一个短暂的时间,然后更新画面,让方块往目标位置的方向稍微移动一点点;
  3. 再等待一个短暂的时间,再让方块往目标位置方向稍微移动一点点;
  4. 重复步骤 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.Viewprops.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 的形式,将它的 opacityfadeAnim 绑定在一起时,在动画播放过程中,随着 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 的实例,从而被某个动画驱动。

results matching ""

    No results matching ""