在不使用任何RN動畫相關API的時候,咱們會想到一種很是粗暴的方式來實現咱們但願的動畫效果——經過修改state來不斷得改變視圖上的樣式。css
咱們來個簡單的示例:html
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> ); } });
效果以下:react
這種方式實現的動畫存在兩大問題:css3
1. 將頻繁地銷燬、重繪視圖來實現動畫效果,性能體驗很糟糕,常規表現爲內存花銷大且動畫卡頓明顯;git
2. 動畫較爲生硬,畢竟web的css3不適用在RN上,沒法輕易設定動畫方式(好比ease-in、ease-out)。github
所以在RN上設置動畫,仍是得乖乖使用相應的API來實現,它們都能很好地觸達組件底層的動畫特性,以原生的形式來實現動畫效果。web
LayoutAnimationspring
LayoutAnimation 是在佈局發生變化時觸發動畫的接口(咱們在上一篇文章裏已經介紹過),這意味着你須要經過修改佈局(好比修改了組件的style、插入新組件)來觸發動畫。react-native
該接口最經常使用的方法是 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.View、Animated.Text 或 Animated.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', } });
留意這裏咱們用了 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> ); } });
對於最後介紹的兩個動畫效果,可能得熟悉一些物理、數學模型才能更好地來作控制,大部分狀況下,我們直接使用 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> ); } });
而 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> ); } }
動畫的API較多,本章先介紹到這裏,下篇將介紹更復雜的動畫實現~ 共勉~