ReactNative入門 —— 動畫篇(上)


在不使用任何RN動畫相關API的時候,我們會想到一種非常粗暴的方式來實現我們希望的動畫效果——通過修改state來不斷得改變視圖上的樣式。

我們來個簡單的示例:

var AwesomeProject = React.createClass({
    getInitialState() {
        return { w: 200, h: 20 }
    },

    _onPress() {
        //每按一次增加近30寬高
        var count = 0;
        while(++count<30){
            requestAnimationFrame(()=>{
                this.setState({w: this.state.w + 1, h: this.state.h + 1})
            })
        }
    }

    render: function() {
        return (
            <View style={styles.container}>
                <View style={[styles.content, {width: this.state.w, height: this.state.h}]}>
                    <Text style={[{textAlign: 'center'}]}>Hi, here is VaJoy</Text>
                </View>
                <TouchableOpacity onPress={this._onPress}>
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>Press me!</Text>
                    </View>
                </TouchableOpacity>
                <TouchableOpacity>
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>忽略本按鈕</Text>
                    </View>
                </TouchableOpacity>
            </View>
        );
    }
});

效果如下:

這種方式實現的動畫存在兩大問題:

1. 將頻繁地銷毀、重繪視圖來實現動畫效果,性能體驗很糟糕,常規表現為內存花銷大且動畫卡頓明顯;

2. 動畫較為生硬,畢竟web的css3不適用在RN上,無法輕易設定動畫方式(比如ease-in、ease-out)。

因此在RN上設置動畫,還是得乖乖使用相應的API來實現,它們都能很好地觸達組件底層的動畫特性,以原生的形式來實現動畫效果。

LayoutAnimation

LayoutAnimation 是在布局發生變化時觸發動畫的接口(我們在上一篇文章里已經介紹過),這意味着你需要通過修改布局(比如修改了組件的style、插入新組件)來觸發動畫。

該接口最常用的方法是  LayoutAnimation.configureNext(conf<Object>) ,用來設置下次布局變化時的動畫效果,需要在執行 setState 前調用。

其中 conf 參數格式參考:

            {
                duration: 700,   //持續時間
                create: {    //若是新布局的動畫類型
                    type: 'linear',  //線性模式
                    property: 'opacity'  //動畫屬性,除了opacity還有一個scaleXY可以配置
                },
                update: {  //若是布局更新的動畫類型
                    type: 'spring',   //彈跳模式
                    springDamping: 0.4  //彈跳阻尼系數
                }
            }

其中動畫type的類型可枚舉為:

  spring  //彈跳
  linear  //線性
  easeInEaseOut  //緩入緩出
  easeIn  //緩入
  easeOut  //緩出
  keyboard  //鍵入

要注意的是,安卓平台使用 LayoutAnimation 動畫必須加上這么一句代碼(否則動畫會失效):

UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true);

於是我們一開始的動畫可以這么來寫:

var AwesomeProject = React.createClass({
    getInitialState() {
        return { w: 200, h: 20 }
    },

    _onPress() {
        LayoutAnimation.configureNext({
            duration: 700,   //持續時間
            create: {
                type: 'linear',
                property: 'opacity'
            },
            update: {
                type: 'spring',
                springDamping: 0.4
            }
        });
        this.setState({w: this.state.w + 30, h: this.state.h + 30})
    }

    render: function() {
        return (
            <View style={styles.container}>
                <View style={[styles.content, {width: this.state.w, height: this.state.h}]}>
                    <Text style={[{textAlign: 'center'}]}>Hi, here is VaJoy</Text>
                </View>
                <TouchableOpacity onPress={this._onPress}>
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>Press me!</Text>
                    </View>
                </TouchableOpacity>
                <TouchableOpacity>
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>忽略本按鈕</Text>
                    </View>
                </TouchableOpacity>
            </View>
        );
    }
});

這時候動畫靈活和流暢多了:

ok我們上例看到的僅僅是布局更新的情況,我們來看看新布局被創建(有新組件加入到視圖上)的情況如何:

var AwesomeProject = React.createClass({
    getInitialState() {
        return {
            showNewOne : false,
            w: 200,
            h: 20
        }
    },

    _onPress() {
        LayoutAnimation.configureNext({
            duration: 1200,
            create: {
                type: 'linear',
                property: 'opacity'  //注意這里,我們設置新布局被創建時的動畫特性為透明度
            },
            update: {
                type: 'spring',
                springDamping: 0.4
            }
        });
        this.setState({w: this.state.w + 30, h: this.state.h + 30, showNewOne : true})
    },
    render: function() {
        var newOne = this.state.showNewOne ? (
            <View style={[styles.content, {width: this.state.w, height: this.state.h}]}>
                <Text style={[{textAlign: 'center'}]}>new one</Text>
            </View>
        ) : null;
        return (
            <View style={styles.container}>
                <View style={[styles.content, {width: this.state.w, height: this.state.h}]}>
                    <Text style={[{textAlign: 'center'}]}>Hi, here is VaJoy</Text>
                </View>
                {newOne}
                <TouchableOpacity onPress={this._onPress}>
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>Press me!</Text>
                    </View>
                </TouchableOpacity>
                <TouchableOpacity>
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>忽略本按鈕</Text>
                    </View>
                </TouchableOpacity>
            </View>
        );
    }
});

效果如下:

setNativeProps

如果我們執意使用開篇的修改state的方式,覺得這種方式更符合當前需求對動畫的控制,那么則應當使用原生組件的 setNativeProps 方法來做對應實現,它會直接修改組件底層特性,不會重繪組件,因此性能也遠勝動態修改組件內聯style的方法。

該接口屬原生組件(比如View,比如TouchableOpacity)才擁有的原生特性接口,調用格式參考如下:

            Component.setNativeProps({
                style: {transform: [{rotate:'50deg'}]}
            });

對於開篇的動畫示例,我們可以做如下修改:

var _s = {
    w: 200,
    h: 20
};
var AwesomeProject = React.createClass({
    _onPress() {
        var count = 0;
        while(count++<30){
            requestAnimationFrame(()=>{
                this.refs.view.setNativeProps({
                    style: {
                        width : ++_s.w,
                        height : ++_s.h
                    }
                });
            })
        }
    },

    render: function() {
        return (
            <View style={styles.container}>
                <View ref="view" style={[styles.content, {width: 200, height: 20}]}>
                    <Text style={[{textAlign: 'center'}]}>Hi, here is VaJoy</Text>
                </View>
                <TouchableOpacity onPress={this._onPress}>
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>Press me!</Text>
                    </View>
                </TouchableOpacity>
                <TouchableOpacity>
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>忽略本按鈕</Text>
                    </View>
                </TouchableOpacity>
            </View>
        );
    }
});

效果如下(比開篇的示例流暢多了):

Animated

本文的重點介紹對象,通過 Animated 我們可以在確保性能良好的前提下創造更為靈活豐富且易維護的動畫。

不同於上述的動畫實現方案,我們得在 Animated.ViewAnimated.TextAnimated.Image 動畫組件上運用 Animate 模塊的動畫能力(如果有在其它組件上的需求,可以使用 Animated.createAnimatedComponent 方法來對其它類型的組件創建動畫)。

我們先來個簡單的例子:

var React = require('react-native');
var {
    AppRegistry,
    StyleSheet,
    Text,
    View,
    Easing,
    Animated,
    TouchableOpacity,
    } = React;

var _animateHandler;
var AwesomeProject = React.createClass({
    componentDidMount() {
        _animateHandler = Animated.timing(this.state.opacityAnmValue, {
            toValue: 1,  //透明度動畫最終值
            duration: 3000,   //動畫時長3000毫秒
            easing: Easing.bezier(0.15, 0.73, 0.37, 1.2)  //緩動函數
        })
    },

    getInitialState() {
        return {
            opacityAnmValue : new Animated.Value(0)   //設置透明度動畫初始值
        }
    },

    _onPress() {
        _animateHandler.start && _animateHandler.start()
    }

    render: function() {
        return (
            <View style={styles.container}>
                <Animated.View ref="view" style={[styles.content, {width: 200, height: 20, opacity: this.state.opacityAnmValue}]}>
                    <Text style={[{textAlign: 'center'}]}>Hi, here is VaJoy</Text>
                </Animated.View>
                <TouchableOpacity onPress={this._onPress}>
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>Press me!</Text>
                    </View>
                </TouchableOpacity>
                <TouchableOpacity >
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>忽略本按鈕</Text>
                    </View>
                </TouchableOpacity>
            </View>
        );
    }
});

var styles = StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center'
    },
    content: {
        justifyContent: 'center',
        backgroundColor: 'yellow',
    },
    button: {
        marginTop: 10,
        paddingVertical: 10,
        paddingHorizontal: 20,
        backgroundColor: 'black'
    },
    buttonText: {
        color: 'white',
        fontSize: 16,
        fontWeight: 'bold',
    }
});

點擊按鈕后,Animated.View 會以bezier曲線形式執行時長3秒的透明度動畫(由0到1):

so 我們做了這些事情:

1. 以 new Animated.Value(0) 實例化動畫的初始值給state:

    getInitialState() {
        return {
            opacityAnmValue : new Animated.Value(0)   //設置透明度動畫初始值
        }
    }

2. 通過 Animated.timing 我們定義了一個動畫事件,在后續可以以 .start().stop() 方法來開始/停止該動畫:

    componentDidMount() {
        _animateHandler = Animated.timing(this.state.opacityAnmValue, {
            toValue: 1,  //透明度動畫最終值
            duration: 3000,   //動畫時長3000毫秒
            easing: Easing.bezier(0.15, 0.73, 0.37, 1.2)
        })
    },

我們在按鈕點擊事件中觸發了動畫的 .start 方法讓它跑起來:

    _onPress() {
        _animateHandler.start && _animateHandler.start()
    },

start 方法接受一個回調函數,會在動畫結束時觸發,並傳入一個參數 {finished: true/false},若動畫是正常結束的,finished 字段值為true,若動畫是因為被調用 .stop() 方法而提前結束的,則 finished 字段值為false。

3. 動畫的綁定是在 <Animate.View /> 上的,我們把實例化的動畫初始值傳入 style 中:

                <Animated.View ref="view" style={[styles.content, {width: 200, height: 20, opacity: this.state.opacityAnmValue}]}>
                    <Text style={[{textAlign: 'center'}]}>Hi, here is VaJoy</Text>
                </Animated.View>

然后。。。沒有然后了,這實在太簡單了

這里需要講一下的應該是定義動畫事件的 Animated.timing(animateValue, conf<Object>) 方法,其中設置參數格式為:

{
  duration: 動畫持續的時間(單位是毫秒),默認為500。
  easing:一個用於定義曲線的漸變函數。閱讀Easing模塊可以找到許多預定義的函數。iOS默認為Easing.inOut(Easing.ease)。
  delay: 在一段時間之后開始動畫(單位是毫秒),默認為0。
}

這里提及的 Easing 動畫函數模塊在 react-native/Libraries/Animated/src/ 目錄下,該模塊預置了 linear、ease、elastic、bezier 等諸多緩動特性,有興趣可以去了解。

另外除了 Animated.timing,Animated 還提供了另外兩個動畫事件創建接口:

1. Animated.spring(animateValue, conf<Object>) —— 基礎的單次彈跳物理模型,支持origami標准,conf參考:

{
  friction: 控制“彈跳系數”、誇張系數,默認為7。
  tension: 控制速度,默認40。
}

代碼參考:

var React = require('react-native');
var {
    AppRegistry,
    StyleSheet,
    Text,
    View,
    Easing,
    Animated,
    TouchableOpacity,
    } = React;

var _animateHandler;
var AwesomeProject = React.createClass({
    componentDidMount() {
        this.state.bounceValue.setValue(1.5);     // 設置一個較大的初始值
        _animateHandler = Animated.spring(this.state.bounceValue, {
            toValue: 1,
            friction: 8,
            tension: 35
        })
    },

    getInitialState() {
        return {
            bounceValue : new Animated.Value(0)   //設置縮放動畫初始值
        }
    },

    _onPress() {
        _animateHandler.start && _animateHandler.start()
    },
    _reload() {
        AppRegistry.reload()
    },

    render: function() {
        return (
            <View style={styles.container}>
                <Animated.View ref="view" style={[styles.content, {width: 200, height: 20, transform: [{scale: this.state.bounceValue}]}]}>
                    <Text style={[{textAlign: 'center'}]}>Hi, here is VaJoy</Text>
                </Animated.View>
                <TouchableOpacity onPress={this._onPress}>
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>Press me!</Text>
                    </View>
                </TouchableOpacity>
                <TouchableOpacity onPress={this._reload}>
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>忽略本按鈕</Text>
                    </View>
                </TouchableOpacity>
            </View>
        );
    }
});

var styles = StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center'
    },
    content: {
        justifyContent: 'center',
        backgroundColor: 'yellow',
    },
    button: {
        marginTop: 10,
        paddingVertical: 10,
        paddingHorizontal: 20,
        backgroundColor: 'black'
    },
    buttonText: {
        color: 'white',
        fontSize: 16,
        fontWeight: 'bold',
    }
});
View Code

留意這里我們用了 animateValue.setValue(1.5) 方法來修改動畫屬性值。效果如下:

2. Animated.decay(animateValue, conf<Object>) —— 以一個初始速度開始並且逐漸減慢停止,conf參考:

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

 代碼參考:

var _animateHandler;
var AwesomeProject = React.createClass({
    componentDidMount() {
        _animateHandler = Animated.decay(this.state.bounceValue, {
            toValue: 0.2,
            velocity: 0.1
        })
    },

    getInitialState() {
        return {
            bounceValue : new Animated.Value(0.1)
        }
    },

    _onPress() {
        _animateHandler.start && _animateHandler.start()
    },

    render: function() {
        return (
            <View style={styles.container}>
                <Animated.View ref="view" style={[styles.content, {width: 200, height: 30, transform: [{scale: this.state.bounceValue}]}]}>
                    <Text style={[{textAlign: 'center'}]}>Hi, here is VaJoy</Text>
                </Animated.View>
                <TouchableOpacity onPress={this._onPress}>
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>Press me!</Text>
                    </View>
                </TouchableOpacity>
                <TouchableOpacity>
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>忽略本按鈕</Text>
                    </View>
                </TouchableOpacity>
            </View>
        );
    }
});
View Code

對於最后介紹的兩個動畫效果,可能得熟悉一些物理、數學模型才能更好地來做控制,大部分情況下,咱們直接使用 Animated.timing 就足夠滿足需求了。

監聽動畫

1. 有時候我們需要在動畫的過程中監聽到某動畫時刻的屬性值,可以通過 animateValue.stopAnimation(callback<Function>)animateValue.addListener(callback<Function>) 來實現

其中 stopAnimation 會停止當前動畫並在回調函數中返回一個 {value : number} 對象,value對應最后一刻的動畫屬性值:

var _animateHandler,
    _isFirsPress = 0;
var AwesomeProject = React.createClass({
    componentDidMount() {
        _animateHandler = Animated.timing(this.state.opacityAnmValue, {
            toValue: 1,
            duration: 6000,
            easing: Easing.linear
        })
    },

    getInitialState() {
        return {
            opacityAnmValue : new Animated.Value(0)   //設置透明度動畫初始值
        }
    },

    _onPress() {

        if(_isFirsPress == 0){
            _animateHandler.start && _animateHandler.start();
            _isFirsPress = 1
        }
        else {
            this.state.opacityAnmValue.stopAnimation(value => {
                Alert.alert(
                    '動畫結束,最終值:',
                    JSON.stringify(value),
                    [
                        {text: 'OK', onPress: () => {}}
                    ]
                )
            })
        }
    },

    render: function() {
        return (
            <View style={styles.container}>
                <Animated.View style={[styles.content, {width: 200, height: 20, opacity: this.state.opacityAnmValue}]}>
                    <Text style={[{textAlign: 'center'}]}>Hi, here is VaJoy</Text>
                </Animated.View>
                <TouchableOpacity onPress={this._onPress}>
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>Press me!</Text>
                    </View>
                </TouchableOpacity>
                <TouchableOpacity >
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>忽略本按鈕</Text>
                    </View>
                </TouchableOpacity>
            </View>
        );
    }
});
View Code

而 addListener 方法會在動畫的執行過程中持續異步調用callback回調函數,提供一個最近的值作為參數。

 

2. 有時候我們希望在某個交互事件(特別是手勢)中更靈活地捕獲某個事件對象屬性值,並動態賦予某個變量,對於這種需求可以通過 Animated.event 來實現。

它接受一個數組為參數,數組中的層次對應綁定事件參數的相應映射,聽着有點繞,看例子就很好理解了:

var scrollX = 0,
      pan = {
            x: 0,
            y: 0
      };
//...
onScroll : Animated.event(
  [{nativeEvent: {contentOffset: {x: scrollX}}}]   // scrollX = e.nativeEvent.contentOffset.x
)
onPanResponderMove : Animated.event([
  null,          // 忽略原生事件
  {dx: pan.x, dy: pan.y}     // 從gestureState中解析出dx和dy的值
]);

onScroll 是綁定給某個組件的滾動事件,而 onPanResponderMove 是 PanResponder 模塊下的響應事件。

拿上方 onPanResponderMove 的例子來講,該事件方法接收兩個參數 e<event Object> 和 gestureState<Object>,其中 gestureState 的屬性有:

stateID - 觸摸狀態的ID。在屏幕上有至少一個觸摸點的情況下,這個ID會一直有效。
moveX - 最近一次移動時的屏幕橫坐標
moveY - 最近一次移動時的屏幕縱坐標
x0 - 當響應器產生時的屏幕坐標
y0 - 當響應器產生時的屏幕坐標
dx - 從觸摸操作開始時的累計橫向路程
dy - 從觸摸操作開始時的累計縱向路程
vx - 當前的橫向移動速度
vy - 當前的縱向移動速度
numberActiveTouches - 當前在屏幕上的有效觸摸點的數量

此處不了解的可以去看我上一篇RN入門文章的相關介紹

而上方例子中,我們動態地將 gestureState.dx 和 gestureState.dy 的值賦予 pan.x 和 pan.y。

來個有簡單的例子:

class AwesomeProject extends Component {
    constructor(props) {
        super(props);
        this.state = {
            transY : new Animated.Value(0)
        };
        this._panResponder = {}
    }
componentWillMount處預先創建手勢響應器
    componentWillMount() {
        this._panResponder = PanResponder.create({
            onStartShouldSetPanResponder: this._returnTrue.bind(this),
            onMoveShouldSetPanResponder: this._returnTrue.bind(this),
            //手勢開始處理
            //手勢移動時的處理
            onPanResponderMove: Animated.event([null, {
                dy : this.state.transY
            }])
        });
    }

    _returnTrue(e, gestureState) {
        return true;
    }

    render() {
        return (
            <View style={styles.container}>
                <Animated.View style={[styles.content, {width: this.state.w, height: this.state.h,
                    transform: [{
                      translateY : this.state.transY
                    }]
                }]}>
                    <Text style={[{textAlign: 'center'}]}>Hi, here is VaJoy</Text>
                </Animated.View>
                <TouchableOpacity>
                    <View style={styles.button} {...this._panResponder.panHandlers}>
                        <Text style={styles.buttonText}>control</Text>
                    </View>
                </TouchableOpacity>

                <TouchableOpacity>
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>忽略此按鈕</Text>
                    </View>
                </TouchableOpacity>
            </View>
        );
    }
}
View Code

 

動畫的API較多,本章先介紹到這里,下篇將介紹更復雜的動畫實現~ 共勉~

donate


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM