React Native 動畫(Animated)

在上篇文章中介紹了 LayoutAnimation 的用法,本篇文章就來詳細介紹一下 Animated 的用法。

Animated 簡介

Animated 庫用於創建更精細的交互控制的動畫,它使得開發者可以非常容易地實現各種各樣的動畫和交互方式,並且具備極高的性能。Animated 旨在以聲明的形式來定義動畫的輸入與輸出,在其中建立一個可配置的變化函數,然後使用簡單的 start/stop 方法來控制動畫按順序執行。

Animated 提供了兩種類型的值:

  • Animated.Value() 用於單個值。
  • Animated.ValueXY() 用於矢量值。

Animated.Value() 可以綁定到樣式或是其他屬性上,也可以進行插值運算。單個 Animated.Value() 可以用在任意多個屬性上。

Animated 用於創建動畫的主要方法:

  • Animated.timing():最常用的動畫類型,使一個值按照一個過渡曲線而隨時間變化。
  • Animated.spring():彈簧效果,基礎的單次彈跳物理模型實現的 spring 動畫。
  • Animated.decay():衰變效果,以一個初始的速度和一個衰減係數逐漸減慢變爲0。

Animated 實現組合動畫的主要方式:

  • Animated.parallel():同時開始一個動畫數組裏的全部動畫。默認情況下,如果有任何一個動畫停止了,其餘的也會被停止。可以通過stopTogether 選項設置爲 false 來取消這種關聯。
  • Animated.sequence():按順序執行一個動畫數組裏的動畫,等待一個完成後再執行下一個。如果當前的動畫被中止,後面的動畫則不會繼續執行。
  • Animated.stagger():一個動畫數組,傳入一個時間參數來設置隊列動畫間的延遲,即在前一個動畫開始之後,隔一段指定時間纔開始執行下一個動畫裏面的動畫,並不關心前一個動畫是否已經完成,所以有可能會出現同時執行(重疊)的情況。

Animated 封裝了四個可以動畫化的組件:

  • Animated.View
  • Animated.Text
  • Animated.Image
  • Animated.ScrollView

也可以使用 Animated.createAnimatedComponent() 來封裝你自己的組件(用 Animated.View 包裹可以達到同樣的效果)。

合成動畫值:

  • Animated.add()
  • Animated.divide()
  • Animated.modulo()
  • Animated.multiply()

可以使用加減乘除以及取餘等運算來把兩個動畫值合成爲一個新的動畫值。

插值函數:

  • interpolate():將輸入值範圍轉換爲輸出值範圍。

譬如:把0-1映射到0-10。

接下來,我們結合一個個的例子介紹它們的用法:

1. Animated.timing()

最常用的動畫類型,使一個值按照一個過渡曲線而隨時間變化,格式如下:
Animated.timing(animateValue, conf<Object>),conf參數格式:

 

{
  duration:動畫持續的時間(單位是毫秒),默認爲500。
  easing:一個用於定義曲線的漸變函數。Easing模預置了 linear、ease、elastic、bezier 等諸多緩動特性。iOS默認爲Easing.inOut(Easing.ease),。
  delay:開始動畫前的延遲時間(毫秒),默認爲0。
}

一個簡單的例子:

 

export default class Opacity extends Component {
    constructor(props) {
        super(props)

        this.state = {
            fadeOutOpacity: new Animated.Value(1),
        }

        this.fadeOutAnimated = Animated.timing(
            this.state.fadeOutOpacity,
            {
                toValue: 0,  //透明度動畫最終值
                duration: 3000,   //動畫時長3000毫秒
                easing: Easing.linear,
            }
        );
    }

    _startAnimated() {
        this.fadeOutAnimated.start(() => this.state.fadeOutOpacity.setValue(1));
    }

    render(){
        return (
            <View style={styles.mainStyle}>
                 <Animated.View style={{width: 200, height: 300, opacity: this.state.fadeOutOpacity}}>
                    <Image ref="image" style={{width:200,height:300}}
                           source={{uri:'beauty.jpg'}}>
                    </Image>
                 </Animated.View>

                <TouchableOpacity style={styles.touchStyle} onPress={this._startAnimated.bind(this)}>
                    <Text style={{width:200,height:100,textAlign:'center',lineHeight:100}}>點擊開始動畫</Text>
                </TouchableOpacity>
            </View>
        );
    }
}

效果圖:

 

image.gif

上面的代碼做了下面這些事情:

  • 1.首先實例化動畫的初始值 fadeOutOpacity,作爲圖片的透明度值。
  • 2.然後定義了一個 Animated.timing() 動畫事件,並配置相應的參數,把它賦予 this.fadeOutAnimated 變量,在後續可以使用 .start() 或 .stop() 方法來開始/停止該動畫。
  • 3.把動畫添加到在 <Animate.View></Animate.View> 上,我們把實例化的動畫初始值傳入 style 中的 opacity。
  • 4.最後我們點擊按鈕,調用 this.fadeOutAnimated.start() 方法開啓動畫,在這裏將 () => this.state.fadeOutOpacity.setValue(1) 傳給了 start(),用於在一次動畫完成之後把圖片的透明度再次設置爲1,這也是創建無窮動畫的方式。

interpolate() 這個函數很強大,實現了數值大小、單位的映射轉換,下面我們結合插值函數:interpolate() 搞些事情:

 

export default class Mixture extends Component {

    constructor(props) {
        super(props)

        this.state = {
            animatedValue: new Animated.Value(0),
        }

        this.rotateAnimated = Animated.timing(
            this.state.animatedValue,
            {
                toValue: 1,
                duration: 3000,
                easing: Easing.in,
            }
        );
    }

    _startAnimated() {
        this.state.animatedValue.setValue(0);
        this.rotateAnimated.start(() => this._startAnimated());
    }

    render(){

        const rotateZ = this.state.animatedValue.interpolate({
            inputRange: [0, 1],
            outputRange: ['0deg', '360deg']
        });

        const opacity = this.state.animatedValue.interpolate({
            inputRange: [0, 0.5, 1],
            outputRange: [0, 1, 0]
        });

        const rotateX = this.state.animatedValue.interpolate({
            inputRange: [0, 0.5, 1],
            outputRange: ['0deg', '180deg', '0deg']
        });

        const textSize = this.state.animatedValue.interpolate({
            inputRange: [0, 0.5, 1],
            outputRange: [18, 32, 18]
        });

        const marginLeft = this.state.animatedValue.interpolate({
            inputRange: [0, 0.5, 1],
            outputRange: [0, 200, 0]
        });

        return (
            <View style={styles.mainStyle}>

                <Animated.View
                    style={{
                        marginTop: 10,
                        width: 100,
                        height: 100,
                        transform: [
                            {rotateZ:rotateZ},
                        ]
                    }}
                >
                    <Image style={{width:100,height:100}}
                           source={{uri:'out_loading_image.png'}}>
                    </Image>
                </Animated.View>

                <Animated.View
                    style={{
                        marginTop: 10,
                        width: 100,
                        height: 100,
                        opacity:opacity,
                        backgroundColor:'red',
                    }}
                />

                <Animated.Text
                    style={{
                        marginTop: 10,
                        width:100,
                        fontSize: 18,
                        color: 'white',
                        backgroundColor:'red',
                        transform: [
                            {rotateX:rotateX},
                        ]
                    }}
                >
                    窗外風好大,我沒有穿褂。
                </Animated.Text>

                <Animated.Text
                    style={{
                        marginTop: 10,
                        height: 100,
                        lineHeight: 100,
                        fontSize: textSize,
                        color: 'red'
                    }}
                >
                    IAMCJ嘿嘿嘿
                </Animated.Text>

                <Animated.View
                    style={{
                        marginTop: 10,
                        width: 100,
                        height: 100,
                        marginLeft:marginLeft,
                        backgroundColor:'red',
                    }}
                />

                <TouchableOpacity style={styles.touchStyle} onPress={this._startAnimated.bind(this)}>
                    <Text style={{width:200,height:100,textAlign:'center',lineHeight:100}}>點擊開始動畫</Text>
                </TouchableOpacity>
            </View>
        );
    }
}

效果圖:

 

Mixture.gif

 

其中 transform 是一個變換數組,常用的有 scale, scaleX, scaleY, translateX, translateY, rotate, rotateX, rotateY, rotateZ,其他用法與上面相同,在這就不多BB了。

2. Animated.spring()

彈簧效果,基礎的單次彈跳物理模型實現的 spring 動畫,格式如下:
Animated.spring(animateValue, conf<Object>),conf參數格式:

 

{
  friction: 控制“彈跳係數”、誇張係數,默認爲7。
  tension: 控制速度,默認爲40。
  speed: 控制動畫的速度,默認爲12。
  bounciness: 反彈係數,默認爲8。
}

一個 Animated.spring() 的例子:

 

export default class AnimatedSpring extends Component {

    constructor(props) {
        super(props);

        this.state = {
            springValue: new Animated.Value(1),
        };

        this.springAnimated = Animated.spring(
            this.state.springValue,
            {
                toValue: 1,
                friction: 2,    //彈跳係數
                tension: 10,   // 控制速度
            }
        );
    }

    _startAnimated() {
        this.state.springValue.setValue(0.1);
        this.springAnimated.start();
    }

    render(){
        return (
            <View style={styles.mainStyle}>

                <Animated.View
                    style={{
                        width: 282,
                        height: 51,
                        transform:[
                            {scale:this.state.springValue}
                        ]
                    }}
                >
                    <Image ref="image" style={{width:282,height:51}}
                           source={{uri:'appstore_comment_image.png'}}>
                    </Image>
                </Animated.View>

                <TouchableOpacity style={styles.touchStyle} onPress={this._startAnimated.bind(this)}>
                    <Text style={{width:200,height:100,textAlign:'center',lineHeight:100}}>點擊開始動畫</Text>
                </TouchableOpacity>
            </View>
        );
    }
}

效果圖:

 

AnimatedSpring.gif

上面的代碼做了下面這些事情:

  • 1.首先實例化動畫的初始值 springValue,作爲圖片的初始 scale 大小。
  • 2.然後定義了一個 Animated. spring() 動畫事件,並配置相應的參數,把它賦予 this. springAnimated 變量。
  • 3.把動畫添加到在 <Animate.View></Animate.View> 上,我們把實例化的動畫初始值傳入 style 中的 transform:[{scale:this.state.springValue}]。
  • 4.最後我們點擊按鈕,在動畫開啓之前把 scale 調到0.1,然後調用 this. springAnimated.start() 方法開啓動畫。

最近寫 native 也有一個類似的需求,我的實現方式是先放大一個比例如1.05,然後動畫完成之後再添加動畫縮小到正常比例,個人感覺實現起來和這個複雜度差不多,而在 native 可以根據 position 和錨點設置圖片是從哪個點以及自身的哪個方向開始放大,例如在屏幕右上角出現圖片,並且是從圖片右上角開始放大這樣,我寫了一個簡單易用的工具類,設置位置和彈出方向即可,添加任意內容,幾行代碼就可以搞定,有需要的話點擊這裏獲取。

3. Animated.decay()

衰變效果,以一個初始的速度和一個衰減係數逐漸減慢變爲0,格式如下:
Animated.decay(animateValue, conf<Object>),conf參數格式:

 

{
  velocity: 起始速度,必填參數。
  deceleration: 速度衰減比例,默認爲0.997。
}

一個 Animated.decay() 的例子:

 

export default class AnimatedDecay extends Component {

    constructor(props) {
        super(props);

        this.state = {
            decayValue: new Animated.ValueXY({x:0,y:0}),
        };

        this.decayAnimated = Animated.decay(
            this.state.decayValue,
            {
                velocity: 5,       // 起始速度,必填
                deceleration: 0.95,  // 速度衰減比例,默認爲0.997
            }
        );
    }

    _startAnimated() {
        this.decayAnimated.start();
    }

    render(){
        return (
            <View style={styles.mainStyle}>

                <Animated.View
                    style={{
                        width: 100,
                        height: 150,
                        transform:[
                            {translateX: this.state.decayValue.x}, // x軸移動
                            {translateY: this.state.decayValue.y}, // y軸移動
                        ]
                    }}
                >
                    <Image ref="image" style={{width:100,height:150}}
                           source={{uri:'beauty.jpg'}}>
                    </Image>
                </Animated.View>

                <TouchableOpacity style={styles.touchStyle} onPress={this._startAnimated.bind(this)}>
                    <Text style={{width:200,height:100,textAlign:'center',lineHeight:100}}>點擊開始動畫</Text>
                </TouchableOpacity>
            </View>
        );
    }
}

 

 

效果圖:

AnimatedDecay.gif

 

嗯,就這個樣子。

這個就不說了,使用方式和 Animated.spring()、Animated.timing() 類似,如果想要使用的話,最好能瞭解一些數學的知識。

接下來做些組合動畫:

4. Animated.parallel()

同時開始一個動畫數組裏的全部動畫。默認情況下,如果有任何一個動畫停止了,其餘的也會被停止。可以通過stopTogether 選項設置爲 false 來取消這種關聯,格式如下:
Animated.parallel(Animates<Array>, [conf<Object>]):,conf參數格式:

 

{
  stopTogether: false
}

第一個參數接受一個元素爲動畫的數組,通過執行 start() 方法可以並行執行該數組中的所有方法。如果數組中任意動畫被中斷的話,該數組內對應的全部動畫會一起停止,不過我們可以通過第二個(可選)參數 conf 中的 stopTogether 來取消這種牽連特性。

來個酷炫的例子:

 

export default class AnimatedParallel extends Component {

    constructor(props) {
        super(props);

        this.state = {
            dogOpacityValue: new Animated.Value(1),
            dogACCValue : new Animated.Value(0)
        };

        this.parallelAnimated = Animated.parallel(
            [
                Animated.timing(
                    this.state.dogOpacityValue,
                    {
                        toValue: 1,
                        duration: 1000,
                    }
                ),
                Animated.timing(
                    this.state.dogACCValue,
                    {
                        toValue: 1,
                        duration: 2000,
                        easing: Easing.linear,
                    }
                ),
            ],
            {
                stopTogether: false
            }
        );
    }

    _startAnimated() {
        this.state.dogOpacityValue.setValue(0);
        this.state.dogACCValue.setValue(0);
        this.parallelAnimated.start();
    }

    render(){

        //透明度
        const dogOpacity = this.state.dogOpacityValue.interpolate({
            inputRange: [0,0.2,0.4,0.6,0.8,1],
            outputRange: [0,1,0,1,0,1]
        });

        //項鍊上面
        const neckTop = this.state.dogACCValue.interpolate({
            inputRange: [0, 1],
            outputRange: [350, 235]
        });

        //眼鏡左邊
        const left = this.state.dogACCValue.interpolate({
            inputRange: [0, 1],
            outputRange: [-120, 127]
        });

        //眼鏡旋轉
        const rotateZ = this.state.dogACCValue.interpolate({
            inputRange: [0, 1],
            outputRange: ['0deg', '360deg']
        });

        return (
            <View style={styles.mainStyle}>

                {/*// 狗頭*/}
                <Animated.View
                    style={{
                        width: 375,
                        height: 240,
                        opacity:dogOpacity,
                    }}
                >
                    <Image ref="image" style={{width:375,height:242}}
                           source={{uri:'dog.jpg'}}>
                    </Image>
                </Animated.View>

                {/*// 項鍊*/}
                <Animated.View
                    style={{
                        width: 250,
                        height: 100,
                        position: 'absolute',
                        top:neckTop,
                        left:93,
                    }}
                >
                    <Image ref="image" style={{width:250,height:100,resizeMode:'stretch'}}
                           source={{uri:'necklace.jpg'}}>
                    </Image>
                </Animated.View>

                <View
                    style={{
                        width: 375,
                        height: 200,
                        backgroundColor:'white',
                    }}
                />

                {/*// 眼鏡*/}
                <Animated.View
                    style={{
                        width: 120,
                        height: 25,
                        position: 'absolute',
                        top:160,
                        left:left,
                        transform:[
                            {rotateZ:rotateZ}
                        ],
                    }}
                >
                    <Image ref="image" style={{width:120,height:25,resizeMode:'stretch'}}
                           source={{uri:'glasses.png'}}>
                    </Image>
                </Animated.View>

                <TouchableOpacity style={styles.touchStyle} onPress={this._startAnimated.bind(this)}>
                    <Text style={{width:200,height:100,textAlign:'center',lineHeight:100}}>點擊開始動畫</Text>
                </TouchableOpacity>
            </View>
        );
    }
}

效果圖:

 

AnimatedParallel.gif

上面的代碼做了下面這些事情:

  • 1.首先實例化動畫的初始值 dogOpacityValue,作爲狗頭圖片的初始透明度,實例化動畫的初始值 dogACCValue,作爲墨鏡、項鍊圖片的初始值。
  • 2.然後定義了兩個 Animated. timing() 動畫事件,並配置相應的參數,第一個是對應狗頭圖片的透明度,第二個對應的是墨鏡、金鍊子的 timing 動畫,然後把這兩個動畫事件放入到 Animated.parallel() 的動畫數組內,最後把 Animated.parallel() 賦予 this.parallelAnimated 變量,用法其實和單獨的動畫事件差不多。
  • 3.在 render 方法中,創建了 dogOpacity、left、rotateZ、neckTop 等變量,並調用了 interpolate() 方法對它們一一映射,分別對應的是透明度、金鍊子 top 的距離、墨鏡 left 距離、眼鏡旋轉的角度,結合上面代碼查看具體信息。
  • 4.把動畫添加到在不同的 <Animate.View></Animate.View> 上,我們把實例化的動畫初始值傳入 style 中。
  • 5.最後動畫開啓之前把 dogOpacityValue、dogACCValue 調到0,然後調用 this. parallelAnimated.start() 方法開啓動畫。

5.Animated.sequence()

按順序執行一個動畫數組裏的動畫,等待一個完成後再執行下一個。如果當前的動畫被中止,後面的動畫則不會繼續執行,格式如下:
Animated.sequence(Animates<Array>)
剛纔知道了 Animated.parallel() 的用法,那麼使用起來 Animated.sequence() 會上手更快,他們用法幾乎一樣,區別就在於 Animated.sequence() 順序執行一個動畫數組裏的動畫,可以理解爲同步操作,而 Animated.parallel() 是異步操作,這兩種方式並沒有好與壞,只有適合不適合,例如下面的這個效果就是適合使用 Animated.sequence() 來完成,而使用 Animated.parallel() 的話則不行。

再來一個酷炫的效果:

 

export default class AnimatedSequence extends Component {

    constructor(props) {
        super(props);

        this.state = {
            turnRotateValue: new Animated.Value(0),
            turnShakeValue : new Animated.Value(0),
            macValue : new Animated.Value(0),
        };

        this.sequenceAnimated = Animated.sequence(
            [
                Animated.timing(
                    this.state.turnRotateValue,
                    {
                        toValue: 1,
                        duration: 5000,
                        easing: Easing.in,
                    }
                ),
                Animated.timing(
                    this.state.turnShakeValue,
                    {
                        toValue: 1,
                        duration: 500,
                        easing: Easing.in,
                        delay:300,
                    }
                ),
                Animated.spring(
                    this.state.macValue,
                    {
                        toValue: 1,
                        friction: 3,
                        tension:10,
                    }
                ),
            ]
        );
    }

    _startAnimated() {
        this.sequenceAnimated.start();
    }

    render(){

        //轉盤旋轉
        const turnRotateZ = this.state.turnRotateValue.interpolate({
            inputRange: [0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1],
            outputRange: [
                '0deg',
                '180deg',
                '360deg',
                '720deg',
                '1080deg',
                '1800deg',
                '2520deg',
                '3060deg',
                '3420deg',
                '3600deg',
                '3690deg',
            ]
        });

        //轉盤震動
        const marginLeft = this.state.turnShakeValue.interpolate({
            inputRange: [0,0.2,0.4,0.6,0.8,1],
            outputRange: [0,-40,40,-40,40,0]
        });

        //MacTop
        const macTop = this.state.macValue.interpolate({
            inputRange: [0, 1],
            outputRange: [-200,150]
        });

        return (
            <View style={styles.mainStyle}>

                {/*// 轉盤*/}
                <Animated.View
                    style={{
                        width: 300,
                        height: 300,
                        marginLeft:marginLeft,
                        transform:[
                            {rotateZ:turnRotateZ}
                        ],
                    }}
                >
                    <Image ref="image" style={{width:300,height:300}}
                           source={{uri:'turntable.jpg'}}>
                    </Image>
                </Animated.View>

                {/*// mac*/}
                <Animated.View
                    style={{
                        width: 300,
                        height: 204,
                        position: 'absolute',
                        top:macTop,
                        left:screenW / 2 - 150,
                    }}
                >
                    <Image ref="image" style={{width:300,height:204}}
                           source={{uri:'macpro.png'}}>
                    </Image>
                </Animated.View>

                <TouchableOpacity style={styles.touchStyle} onPress={this._startAnimated.bind(this)}>
                    <Text style={{width:200,height:100,textAlign:'center',lineHeight:100}}>點擊開始動畫</Text>
                </TouchableOpacity>
            </View>
        );
    }
}

效果圖:

 

AnimatedSequence.gif

這個動畫的流程分爲三個部分:

  • 1.轉盤先由慢到快轉了5秒鐘;
  • 2.在轉盤轉圈停止之後,在短暫的延遲之後,震動了幾下;
  • 3.最後 MacBook Pro 由天而降,驚不驚喜,意不意外,大獎來了!啊哈哈哈哈...

實現步驟就不一一解釋了,對應上面 Animated.parallel() 的例子就可以看明白,主要區別在於動畫的實現思路,這個動畫就很好的利用了 Animated.sequence() 順序執行的特性。

6.Animated.stagger()

一個動畫數組,傳入一個時間參數來設置隊列動畫間的延遲,即在前一個動畫開始之後,隔一段指定時間纔開始執行下一個動畫裏面的動畫,並不關心前一個動畫是否已經完成,所以有可能會出現同時執行(重疊)的情況,其格式如下:
Animated.stagger(delayTime<Number>, Animates<Array>)
其中 delayTime 爲指定的延遲時間(毫秒),第二個和上面兩個一樣傳入一個動畫事件數組。

一個簡單的紅藍塊移動例子幫助理解:

 

export default class AnimatedStagger extends Component {

    constructor(props) {
        super(props);

        this.state = {
            redValue: new Animated.Value(0),
            blueValue : new Animated.Value(0),
        };

        this.staggerAnimated = Animated.stagger(2000,
            [
                Animated.timing(
                    this.state.redValue,
                    {
                        toValue: 1,
                        duration: 5000,
                        easing: Easing.in,
                    }
                ),
                Animated.timing(
                    this.state.blueValue,
                    {
                        toValue: 1,
                        duration: 5000,
                        easing: Easing.in,
                    }
                ),
            ]
        );
    }

    _startAnimated() {
        this.staggerAnimated.start();
    }

    render(){

        const redMarginLeft = this.state.redValue.interpolate({
            inputRange: [0,1],
            outputRange: [0,200]
        });

        const blueMarginLeft = this.state.blueValue.interpolate({
            inputRange: [0,1],
            outputRange: [0,200]
        });

        return (
            <View style={styles.mainStyle}>

                {/*// 紅色*/}
                <Animated.View
                    style={{
                        width: 100,
                        height: 100,
                        backgroundColor:'red',
                        marginLeft:redMarginLeft,
                    }}
                >
                </Animated.View>


                {/*// 藍色*/}
                <Animated.View
                    style={{
                        width: 100,
                        height: 100,
                        backgroundColor:'blue',
                        marginLeft:blueMarginLeft,
                    }}
                >
                </Animated.View>

                <TouchableOpacity style={styles.touchStyle} onPress={this._startAnimated.bind(this)}>
                    <Text style={{width:200,height:100,textAlign:'center',lineHeight:100}}>點擊開始動畫</Text>
                </TouchableOpacity>
            </View>
        );
    }
}

效果圖:

 

AnimatedStagger.gif

這個動畫的流程:
流程就是紅的先移動,然後兩秒後藍的移動!(⊙﹏⊙)

看了這個例子,應該很容易明白 Animated.stagger() 的作用,在此不多做解釋了。

OK,就到這吧,看完這篇文章,一般的動畫效果應該都可以實現了,想了解 Animated 更多的特性可以去Animateddemo在這裏。

參考:
ReactNative入門 —— 動畫篇(下)
【譯】詳解React Native動畫

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章