詳解React Native動畫

JorjaDeMais 8年前發布 | 18K 次閱讀 ReactNative 移動開發 React Native

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

  1. Animated.timing() -- 推動一個值按照一個過渡曲線而隨時間變化。 Easing 模塊定義了很多緩沖曲線函數。
  2. Animated.decay() -- 推動一個值以一個初始的速度和一個衰減系數逐漸變為0。
  3. Animated.spring() -- 產生一個基于 Rebound 和 Origami 實現的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()

第一個要創建的動畫是使用 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.js 和 index.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。我們傳遞了 inputRange 和 outputRange 參數給 interpolate 方法,并分別賦值為 [0,1] 和 & [‘0deg’, ‘360deg’]
  2. 我們返回了一個帶 container 樣式值的 View 和 帶 height , widthtransform 屬性的 Animated.Image ,并將 spin 的值賦給 transform 的 rotate 屬性,這也是動畫發生的地方:
transform: [{rotate: spin}]

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

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

這個示例動畫的最終代碼在 這里 。

關于Easing

這是 Easing 模塊的源碼鏈接,從源碼中可以看到每一個 easing 方法。

我創建了另外一個示例項目,里面包含了大部分 easing 動畫的實現,可以供你參考,鏈接在 這里 。(項目的運行截圖)依據在下面:

2. Animated.timing 示例

上文已經說過了 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()

接下來,我們將會使用 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()

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()

先看一下這個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()

先看一下這個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);

參考

React Native Animations Using the Animated API

Animated Docs

 

來自:https://github.com/dwqs/blog/issues/41

 

 本文由用戶 JorjaDeMais 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!