【譯】詳解React Native動畫

大多數狀況下,在 React Native 中建立動畫是推薦使用 Animated API 的,其提供了三個主要的方法用於建立動畫:html

  1. Animated.timing() -- 推進一個值按照一個過渡曲線而隨時間變化。Easing 模塊定義了不少緩衝曲線函數。react

  2. Animated.decay() -- 推進一個值以一個初始的速度和一個衰減係數逐漸變爲0。android

  3. Animated.spring() -- 產生一個基於 ReboundOrigami 實現的Spring動畫。它會在 toValue 值更新的同時跟蹤當前的速度狀態,以確保動畫連貫。ios

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

以個人經驗來看,Animated.timing()Animated.spring() 在建立動畫方面是很是有效的。github

除了這三個建立動畫的方法,對於每一個獨立的方法都有三種調用該動畫的方式:spring

  1. Animated.parallel() -- 同時開始一個動畫數組裏的所有動畫。默認狀況下,若是有任何一個動畫中止了,其他的也會被中止。你能夠經過stopTogether 選項來改變這個效果。react-native

  2. Animated.sequence() -- 按順序執行一個動畫數組裏的動畫,等待一個完成後再執行下一個。若是當前的動畫被停止,後面的動畫則不會繼續執行。api

  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

原文地址

https://github.com/dwqs/blog/...

相關文章
相關標籤/搜索