閱讀474 返回首頁    go 阿裏雲 go 技術社區[雲棲]


React Native動畫Animated詳解

在移動開發中,動畫是提高用戶體驗不可缺少的一個元素。在React Native中,動畫API提供了一些現成的組件:Animated.View,Animated.Text和Animated.Image默認支持動畫。動畫API會調用iOS或者Android的本地代碼來完成這些組件的位移、大小等動畫。

在React Native中,Animated創建過程如下:

  1. 創建Animated.Value,設置初始值,比如一個視圖的opacity屬性,最開始設置Animated.Value(0),來表示動畫的開始時候,視圖是全透明的。
  2. AnimatedValue綁定到Style的可動畫屬性,比如透明度,{opacity: this.state.fadeAnim}
  3. 使用Animated.timing來創建自動的動畫,或者使用Animated.event來根據手勢,觸摸,Scroll的動態更新動畫的狀態(本文會側重講解Animated.timing)
  4. 調用Animated.timeing.start()開始動畫

Animated簡介

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

  1. Animated.timing() -- 推動一個值按照一個過渡曲線而隨時間變化。Easing 模塊定義了很多緩衝曲線函數。
  2. Animated.decay() -- 推動一個值以一個初始的速度和一個衰減係數逐漸變為0。
  3. Animated.spring() -- 產生一個基於 Rebound 和 Origami 實現的Spring動畫。它會在 toValue 值更新的同時跟蹤當前的速度狀態,以確保動畫連貫。

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

  1. Animated.parallel() --同時開始一個動畫數組裏的全部動畫。默認情況下,如果有任何一個動畫停止了,其餘的也會被停止。你可以通過stopTogether 選項來改變這個效果。
  2. Animated.sequence() --按順序執行一個動畫數組裏的動畫,等待一個完成後再執行下一個。如果當前的動畫被中止,後麵的動畫則不會繼續執行。
  3. Animated.stagger() -- 一個動畫數組,裏麵的動畫有可能會同時執行(重疊),不過會以指定的延遲來開始。

Animated.timing()

使用 Animated.timing 創建的旋轉動畫。Animated.timing()的基本使用方法如下:

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

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

現在方法已經創建好了,接下來就是在UI中渲染動畫了。

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

實現效果:
這裏寫圖片描述

完整代碼:

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

import React, {Component} from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    Animated,
    TouchableOpacity,
    Easing,
    View
} from 'react-native';


class AnimationRotateScene extends Component {

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

    componentDidMount () {
        this.spin()
    }

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


    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'}}
                />
                <TouchableOpacity onPress={() => this.spin()} style={styles.button}>
                    <Text>啟動動畫</Text>
                </TouchableOpacity>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        marginTop: 20,
        justifyContent: 'center',
        alignItems: 'center',
    },
    button: {
        marginTop: 20,
        backgroundColor:'#808080',
        height:35,
        width:140,
        borderRadius:5,
        justifyContent: 'center',
        alignItems: 'center',
    },
});

export default AnimationRotateScene;

Animated.spring()

使用 Animated.spring() 方法創建一個放大縮小的動畫。
這裏寫圖片描述
Animated.spring() 方法使用:

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

如上圖所示,我們要使用Animated.spring()創建一個放大縮小的動畫效果。
在構造函數中,創建一個 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()
}

然後我們給View的button添加一個點擊事件,出發上麵的spring動畫。

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

完整代碼如下:

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

import React, {Component} from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    Animated,
    TouchableOpacity,
    Easing,
    View
} from 'react-native';


class AnimationRotateScene extends Component {

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

    componentDidMount () {
        this.spin()
    }

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


    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'}}
                />
                <TouchableOpacity onPress={() => this.spin()} style={styles.button}>
                    <Text>啟動動畫</Text>
                </TouchableOpacity>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        marginTop: 20,
        justifyContent: 'center',
        alignItems: 'center',
    },
    button: {
        marginTop: 20,
        backgroundColor:'#808080',
        height:35,
        width:140,
        borderRadius:5,
        justifyContent: 'center',
        alignItems: 'center',
    },
});

export default AnimationRotateScene;

Animated.parallel()

Animated.parallel() 會同時開始一個動畫數組裏的全部動畫。parallel()會接受一個動畫數組,首先看一下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]
  })
  ...
}

最後,我們用一個主 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>

完整的代碼如下:

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow 組動畫
 */

import React, {Component} from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    Animated,
    TouchableOpacity,
    TouchableHighlight,
    Easing,
    View
} from 'react-native';


class AnimationGroupScene extends Component {

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

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

    startAnimation() {
        this.state.currentAlpha = this.state.currentAlpha == 1.0 ? 0.0 : 1.0;
        Animated.timing(
            this.state.fadeAnim,
            {toValue: this.state.currentAlpha}
        ).start();
    }

    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]
        })

        return (
            <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>啟動組動畫</Text>
                    </TouchableHighlight>
                </Animated.View>

            </View>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        marginTop: 20,
        justifyContent: 'center',
        alignItems: 'center',
    },
    button: {
        marginTop: 20,
        backgroundColor: '#808080',
        height: 35,
        width: 140,
        borderRadius: 5,
        justifyContent: 'center',
        alignItems: 'center',
    },
});

export default AnimationGroupScene;

示例使用說明

這裏寫圖片描述
這裏寫圖片描述
如圖所示,我對動畫的代碼做了一個簡單的整理,大家在使用的時候直接引入AnimationRoot文件即可。
AnimationRoot文件內容如下:

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

import React, {Component} from 'react';
import { StackNavigator } from 'react-navigation';


import AnimationIndex from './AnimationIndex';
import AnimationSpringScene from './AnimationSpringScene';//縮放動畫
import AnimationRotateScene from './AnimationRotateScene';//旋轉動畫
import AnimationAlphaScene from './AnimationAlphaScene';//Alpha動畫
import AnimationGroupScene from './AnimationGroupScene';//組動畫
import AnimationFrameScene from './AnimationFrameScene';//幀動畫


const anim = StackNavigator({
    AnimationIndex: { screen: AnimationIndex },
    AnimationSpringScene: { screen: AnimationSpringScene },
    AnimationRotateScene: { screen: AnimationRotateScene },
    AnimationAlphaScene: { screen: AnimationAlphaScene },
    AnimationGroupScene: { screen: AnimationGroupScene },
    AnimationFrameScene: { screen: AnimationFrameScene },
});
export default anim;

最後是項目實現的最終結果圖,代碼地址動畫源碼

最後更新:2017-05-28 10:03:17

  上一篇:go  阿裏牽頭研製“大數據安全能力成熟度模型”國家標準
  下一篇:go  mysql常用命令書目錄