dwqs / blog

:dog: :clap: :star2: Welcome to star
MIT License
3.78k stars 442 forks source link

[译]详解React Native动画 #41

Open dwqs opened 7 years ago

dwqs commented 7 years ago

大多数情况下,在 React Native 中创建动画是推荐使用 Animated API 的,其提供了三个主要的方法用于创建动画:

  1. Animated.timing() -- 推动一个值按照一个过渡曲线而随时间变化。Easing 模块定义了很多缓冲曲线函数。
  2. Animated.decay() -- 推动一个值以一个初始的速度和一个衰减系数逐渐变为0。
  3. Animated.spring() -- 产生一个基于 ReboundOrigami 实现的Spring动画。它会在 toValue 值更新的同时跟踪当前的速度状态,以确保动画连贯。

译者注:React Native(0.37) 目前只支持Animated.Text/Animated.View/Animated.Image

以我的经验来看,Animated.timing()Animated.spring() 在创建动画方面是非常有效的。

除了这三个创建动画的方法,对于每个独立的方法都有三种调用该动画的方式:

  1. Animated.parallel() -- 同时开始一个动画数组里的全部动画。默认情况下,如果有任何一个动画停止了,其余的也会被停止。你可以通过stopTogether 选项来改变这个效果。
  2. Animated.sequence() -- 按顺序执行一个动画数组里的动画,等待一个完成后再执行下一个。如果当前的动画被中止,后面的动画则不会继续执行。
  3. Animated.stagger() -- 一个动画数组,里面的动画有可能会同时执行(重叠),不过会以指定的延迟来开始。

1. Animated.timing()

timing

第一个要创建的动画是使用 Animated.timing 创建的旋转动画。

// Example implementation:
Animated.timing(
  someValue,
  {
    toValue: number,
    duration: number,
    easing: easingFunction,
    delay: number
  }
)

这种方式常用于创建需要loading指示的动画,在我使用React Native的项目中,这也是创建动画最有效的方式。这个理念也可以用于其它诸如按比例放大和缩小类型的指示动画。

开始之前,我们需要创建一个新的React Native 项目或者一个空的React Native项目。创建新项目之前,需要输入 react-native init 来初始化一个项目,并切换到该项目目录:

react-native init animations
cd animations

然后打开 index.android.jsindex.ios.js

现在已经创建了一个新项目,则第一件事是在已经引入的 View 之后从 react native 中引入 Animated,ImageEasing

import {
  AppRegistry,
  StyleSheet,
  Text,
  View,
  Animated,
  Image,
  Easing
} from 'react-native'

Animated 是我们将用于创建动画的库,和React Native交互的载体。

Image 用于在UI中显示图片。

Easing 也是用React Native创建动画的载体,它允许我们使用已经定义好的各种缓冲函数,例如:linear, ease, quad, cubic, sin, elastic, bounce, back, bezier, in, out, inout 。由于有直线运动,我们将使用 linear。在这节(阅读)完成之后,对于实现直线运动的动画,你或许会有更好的实现方式。

接下来,需要在构造函数中初始化一个带动画属性的值用于旋转动画的初始值:

constructor () {
  super()
  this.spinValue = new Animated.Value(0)
}

我们使用 Animated.Value 声明了一个 spinValue 变量,并传了一个 0 作为初始值。

然后创建了一个名为 spin 的方法,并在 componentDidMount 中调用它,目的是在 app 加载之后运行动画:

componentDidMount () {
  this.spin()
}
spin () {
  this.spinValue.setValue(0)
  Animated.timing(
    this.spinValue,
    {
      toValue: 1,
      duration: 4000,
      easing: Easing.linear
    }
  ).start(() => this.spin())
}

spin() 方法的作用如下:

  1. this.spinValue 重置成 0
  2. 调用 Animated.timing ,并驱动 this.spinValue 的值以 Easing.linear 的动画方式在 4000 毫秒从 0 变成 1。Animated.timing 需要两个参数,一个要变化的值(本文中是 this.spinValue) 和一个可配置对象。这个配置对象有四个属性:toValue(终值)、duration(一次动画的持续时间)、easing(缓存函数)和delay(延迟执行的时间)
  3. 调用 start(),并将 this.spin 作为回调传递给 start,它将在(一次)动画完成之后调用,这也是创建无穷动画的一种基本方式。start() 需要一个完成回调,该回调在动画正常的运行完成之后会被调用,并有一个参数是 {finished: true},但如果动画是在它正常运行完成之前而被停止了(如:被手势动作或者其它动画中断),则回调函数的参数变为 {finished: false}

译者注:如果在回调中将动画的初始值设置成其终值,该动画就不会再执行。如将 this.spinValue.setValue(0) 改为 this.spinValue.setValue(1),spin动画不会执行了

现在方法已经创建好了,接下来就是在UI中渲染动画了。为了渲染动画,需要更新 render 方法:

render () {
  const spin = this.spinValue.interpolate({
    inputRange: [0, 1],
    outputRange: ['0deg', '360deg']
  })
  return (
    <View style={styles.container}>
      <Animated.Image
        style={{
          width: 227,
          height: 200,
          transform: [{rotate: spin}] }}
          source={{uri: 'https://s3.amazonaws.com/media-p.slid.es/uploads/alexanderfarennikov/images/1198519/reactjs.png'}}
      />
    </View>
  )
}
  1. render 方法中,创建了一个 spin 变量,并调用了 this.spinValueinterpolate 方法。interpolate 方法可以在任何一个 Animated.Value 返回的实例上调用,该方法会在属性更新之前插入一个新值,如将 0~1 映射到 1~10。在我们的demo中,利用 interpolate 方法将数值 0~1 映射到了 0deg~360deg。我们传递了 inputRangeoutputRange 参数给interpolate 方法,并分别赋值为 [0,1] 和 &[‘0deg’, ‘360deg’]
  2. 我们返回了一个带 container 样式值的 View和 带 height, widthtransform 属性的Animated.Image,并将 spin 的值赋给 transformrotate 属性,这也是动画发生的地方:
transform: [{rotate: spin}]

最后,在 container 样式中,使所有元素都居中:

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center'
  }
})

这个示例动画的最终代码在这里

关于Easing

这是 Easing 模块的源码链接,从源码中可以看到每一个 easing 方法。

我创建了另外一个示例项目,里面包含了大部分 easing 动画的实现,可以供你参考,链接在这里。(项目的运行截图)依据在下面:

gist

该项目实现的 easing 动画在 RNPlay 的地址在这里

2. Animated.timing 示例

timing exam

上文已经说过了 Animated.timing 的基础知识,这一节会例举更多使用 Animated.timinginterpolate 结合实现的动画示例。

下一个示例中,会声明一个单一的动画值, this.animatedValue ,然后将该值和 interpolate 一起使用来驱动下列属性值的变化来创建复杂动画:

  1. marginLeft
  2. opacity
  3. fontSize
  4. rotateX

在开始之前,可以创建一个新分支或者清除上一个项目的旧代码。

第一件事是在构造函数中初始化一个需要用到的动画属性值:

constructor () {
  super()
  this.animatedValue = new Animated.Value(0)
}

接下来,创建一个名为animate的方法,并在 componentDidMount() 中调用该方法:

componentDidMount () {
  this.animate()
}
animate () {
  this.animatedValue.setValue(0)
  Animated.timing(
    this.animatedValue,
    {
      toValue: 1,
      duration: 2000,
      easing: Easing.linear
    }
  ).start(() => this.animate())
}

render 方法中,我们创建 5 个不同的插值变量:

render () { 
  const marginLeft = this.animatedValue.interpolate({
    inputRange: [0, 1],
    outputRange: [0, 300]
  })
  const opacity = this.animatedValue.interpolate({
    inputRange: [0, 0.5, 1],
    outputRange: [0, 1, 0]
  })
  const movingMargin = this.animatedValue.interpolate({
    inputRange: [0, 0.5, 1],
    outputRange: [0, 300, 0]
  })
  const textSize = this.animatedValue.interpolate({
    inputRange: [0, 0.5, 1],
    outputRange: [18, 32, 18]
  })
  const rotateX = this.animatedValue.interpolate({
    inputRange: [0, 0.5, 1],
    outputRange: ['0deg', '180deg', '0deg']
  })
...
}

interpolate 是一个很强大的方法,允许我们用多种方式来使用单一的动画属性值:this.animatedValue。因为 this.animatedValue 只是简单的从0变到1,因而我们能将这个值插入到 opacity、margins、text sizes 和 rotation 等样式属性中。

最后,返回实现了上述变量的 Animated.View 和 Animated.Text 组件:

return (
    <View style={styles.container}>
      <Animated.View
        style={{
          marginLeft,
          height: 30,
          width: 40,
          backgroundColor: 'red'}} />
      <Animated.View
        style={{
          opacity,
          marginTop: 10,
          height: 30,
          width: 40,
          backgroundColor: 'blue'}} />
      <Animated.View
        style={{
          marginLeft: movingMargin,
          marginTop: 10,
          height: 30,
          width: 40,
          backgroundColor: 'orange'}} />
      <Animated.Text
        style={{
          fontSize: textSize,
          marginTop: 10,
          color: 'green'}} >
          Animated Text!
      </Animated.Text>
      <Animated.View
        style={{
          transform: [{rotateX}],
          marginTop: 50,
          height: 30,
          width: 40,
          backgroundColor: 'black'}}>
        <Text style={{color: 'white'}}>Hello from TransformX</Text>
      </Animated.View>
    </View>
)

当然,也需要更新下 container 样式:

const styles = StyleSheet.create({
  container: {
    flex: 1,
    paddingTop: 150
  }
})

这个示例动画的最终代码在这里

3. Animated.spring()

sring

接下来,我们将会使用 Animated.spring() 方法创建动画。

// Example implementation:
Animated.spring(
    someValue,
    {
      toValue: number,
      friction: number
    }
)

我们继续使用上一个项目,并只需要更新少量代码就行。在构造函数中,创建一个 springValue 变量,初始化其值为0.3:

constructor () {
  super()
  this.springValue = new Animated.Value(0.3)
}

然后,删除 animated 方法和componentDidMount方法,创建一个新的 spring 方法:

spring () {
  this.springValue.setValue(0.3)
  Animated.spring(
    this.springValue,
    {
      toValue: 1,
      friction: 1
    }
  ).start()
}
  1. springValue 的值重置为 0.3
  2. 调用 Animated.spring 方法,并传递两个参数:一个要变化的值和一个可配置对象。可配置对象的属性可以是下列的任何值:toValue (number), overshootClamping (boolean), restDisplacementThreshold (number), restSpeedThreshold (number), velocity (number), bounciness (number), speed (number), tension(number), 和 friction (number)。除了 toValue 是必须的,其他值都是可选的,但 frictiontension 能帮你更好地控制 spring 动画。
  3. 调用 start() 启动动画

动画已经设置好了,我们将其放在 View 的click事件中,动画元素依然是之前使用过的 React logo 图片:

<View style={styles.container}>
  <Text
    style={{marginBottom: 100}}
    onPress={this.spring.bind(this)}>Spring</Text>
    <Animated.Image
      style={{ width: 227, height: 200, transform: [{scale: this.springValue}] }}
      source={{uri: 'https://s3.amazonaws.com/media-p.slid.es/uploads/alexanderfarennikov/images/1198519/reactjs.png'}}/>
</View>
  1. 我们返回一个Text组件,并将 spring() 添加到组件的onPress事件中
  2. 我们返回一个 Animated.Image,并为其 scale 属性添加 this.springValue

这个示例动画的最终代码在这里

4. Animated.parallel()

parallel

Animated.parallel() 会同时开始一个动画数组里的全部动画。

先看一下这个api是怎么调用的:

// API
Animated.parallel(arrayOfAnimations)
// In use:
Animated.parallel([
  Animated.spring(
    animatedValue,
    {
      //config options
    }
  ),
  Animated.timing(
     animatedValue2,
     {
       //config options
     }
  )
])

开始之前,我们先直接创建三个我们需要的动画属性值:

constructor () {
  super()
  this.animatedValue1 = new Animated.Value(0)
  this.animatedValue2 = new Animated.Value(0)
  this.animatedValue3 = new Animated.Value(0)
}

然后,创建一个 animate 方法并在 componendDidMount() 中调用它:

componentDidMount () {
  this.animate()
}
animate () {
  this.animatedValue1.setValue(0)
  this.animatedValue2.setValue(0)
  this.animatedValue3.setValue(0)
  const createAnimation = function (value, duration, easing, delay = 0) {
    return Animated.timing(
      value,
      {
        toValue: 1,
        duration,
        easing,
        delay
      }
    )
  }
  Animated.parallel([
    createAnimation(this.animatedValue1, 2000, Easing.ease),
    createAnimation(this.animatedValue2, 1000, Easing.ease, 1000),
    createAnimation(this.animatedValue3, 1000, Easing.ease, 2000)        
  ]).start()
}

animate 方法中,我们将三个动画属性值重置为0。此外,还创建了一个 createAnimation 方法,该方法接受四个参数:value, duration, easing, delay(默认值是0),返回一个新的动画。

然后,调用 Animated.parallel(),并将三个使用 createAnimation 创建的动画作为参数传递给它。

render 方法中,我们需要设置插值:

render () {
  const scaleText = this.animatedValue1.interpolate({
    inputRange: [0, 1],
    outputRange: [0.5, 2]
  })
  const spinText = this.animatedValue2.interpolate({
    inputRange: [0, 1],
    outputRange: ['0deg', '720deg']
  })
  const introButton = this.animatedValue3.interpolate({
    inputRange: [0, 1],
    outputRange: [-100, 400]
  })
  ...
}
  1. scaleText -- 插值的输出范围是从0.5到2,我们会用这个值对文本按0.5到2的比例进行缩放
  2. spinText -- 插值的输出范围是 0 degrees 到 720 degrees,即将元素旋转两周
  3. introButton -- 插值的输出范围是 -100 到 400,该值会用于 View 的 margin 属性

最后,我们用一个主 View 包裹三个 Animated.Views:

<View style={[styles.container]}>
  <Animated.View 
    style={{ transform: [{scale: scaleText}] }}>
    <Text>Welcome</Text>
  </Animated.View>
  <Animated.View
    style={{ marginTop: 20, transform: [{rotate: spinText}] }}>
    <Text
      style={{fontSize: 20}}>
      to the App!
    </Text>
  </Animated.View>
  <Animated.View
    style={{top: introButton, position: 'absolute'}}>
    <TouchableHighlight
      onPress={this.animate.bind(this)}
      style={styles.button}>
      <Text
        style={{color: 'white', fontSize: 20}}>
        Click Here To Start
      </Text>
   </TouchableHighlight>
  </Animated.View>
</View>

animate() 被调用时,三个动画会同时执行。

这个示例动画的最终代码在这里

5. Animated.Sequence()

sequence

先看一下这个api是怎么调用的:

// API
Animated.sequence(arrayOfAnimations)
// In use
Animated.sequence([
  Animated.timing(
    animatedValue,
    {
      //config options
    }
  ),
  Animated.spring(
     animatedValue2,
     {
       //config options
     }
  )
])

Animated.parallel() 一样, Animated.sequence() 接受一个动画数组。但不同的是,Animated.sequence() 是按顺序执行一个动画数组里的动画,等待一个完成后再执行下一个。

import React, { Component } from 'react';

import {

  AppRegistry,

  StyleSheet,

  Text,

  View,

  Animated

} from 'react-native'

const arr = []

for (var i = 0; i < 500; i++) {

  arr.push(i)

}

class animations extends Component {

  constructor () {

    super()

    this.animatedValue = []

    arr.forEach((value) => {

      this.animatedValue[value] = new Animated.Value(0)

    })

  }

  componentDidMount () {

    this.animate()

  }

  animate () {

    const animations = arr.map((item) => {

      return Animated.timing(

        this.animatedValue[item],

        {

          toValue: 1,

          duration: 50

        }

      )

    })

    Animated.sequence(animations).start()

  }

  render () {

    const animations = arr.map((a, i) => {

      return <Animated.View key={i} style={{opacity: this.animatedValue[a], height: 20, width: 20, backgroundColor: 'red', marginLeft: 3, marginTop: 3}} />

    })

    return (

      <View style={styles.container}>

        {animations}

      </View>

    )

  }

}

const styles = StyleSheet.create({

  container: {

    flex: 1,

    flexDirection: 'row',

    flexWrap: 'wrap'

  }

})

AppRegistry.registerComponent('animations', () => animations);

由于 Animated.sequence()Animated.parallel() 很相似,因而对 Animated.sequence() 就不多作介绍了。主要不同的一点是我们是使用循环创建 Animated.Values。

这个示例动画的最终代码在这里

6. Animated.Stagger()

(图片太大,上传不了) gif动态图

先看一下这个api是怎么调用的:

// API
Animated.stagger(delay, arrayOfAnimations)
// In use:
Animated.stagger(1000, [
  Animated.timing(
    animatedValue,
    {
      //config options
    }
  ),
  Animated.spring(
     animatedValue2,
     {
       //config options
     }
  )
])

和 Animated.parallel() 和 Animated.sequence() 一样, Animated.Stagger 接受一个动画数组。但不同的是,Animated.Stagger 里面的动画有可能会同时执行(重叠),不过会以指定的延迟来开始。

与上述两个动画主要的不同点是 Animated.Stagger 的第一个参数,delay 会被应用到每一个动画:

import React, { Component } from 'react';

import {

  AppRegistry,

  StyleSheet,

  Text,

  View,

  Animated

} from 'react-native'

const arr = []

for (var i = 0; i < 500; i++) {

  arr.push(i)

}

class animations extends Component {

  constructor () {

    super()

    this.animatedValue = []

    arr.forEach((value) => {

      this.animatedValue[value] = new Animated.Value(0)

    })

  }

  componentDidMount () {

    this.animate()

  }

  animate () {

    const animations = arr.map((item) => {

      return Animated.timing(

        this.animatedValue[item],

        {

          toValue: 1,

          duration: 4000

        }

      )

    })

    Animated.stagger(10, animations).start()

  }

  render () {

    const animations = arr.map((a, i) => {

      return <Animated.View key={i} style={{opacity: this.animatedValue[a], height: 20, width: 20, backgroundColor: 'red', marginLeft: 3, marginTop: 3}} />

    })

    return (

      <View style={styles.container}>

        {animations}

      </View>

    )

  }

}

const styles = StyleSheet.create({

  container: {

    flex: 1,

    flexDirection: 'row',

    flexWrap: 'wrap'

  }

})

AppRegistry.registerComponent('SampleApp', () => animations);

这个示例动画的最终代码在这里

文中使用的demo repo: react native animations

参考

React Native Animations Using the Animated API Animated Docs

pannz commented 7 years ago

nice

forrest23 commented 7 years ago

例子很详细,很棒

shaoting0730 commented 7 years ago

666666666

mogody commented 7 years ago

请问下,如何暂停一个动画呢?我在渲染一个360度旋转动画的时候(代码和你上面的一样),对Animaten.timing这个返回的对象调用stop()方法,但是却并不是停止旋转动画,而是重新开始动画,这是啥情况?

hama commented 7 years ago

666

Liqiankun commented 7 years ago

@dwqs 不行啊, 代码连接全部打不开?求解!

shaoting0730 commented 7 years ago

@wh469012917 关于暂停动画我写过一个 https://github.com/pheromone/react-native-videoDemo

s349856186 commented 6 years ago

有没有办法做成绘制移动路线 L 型?不只是单独的左右移动?

TangShengda commented 6 years ago

nice

yibingxiong commented 4 years ago

如何做一个展开收起的动画啊