學習和實踐react已經有一段時間了,在經歷了從最初的彷徨到解決痛點時的興奮,再到不斷實踐後遭遇問題時的苦悶,確實被這一種新的思維方式和開發模式所折服,但react也不是萬能的,在很多場景下濫用反而會適得其反,這裡不展開討論。
有了react的實踐經驗,結合之前自己的一點ios開發經驗,決定繼續冒險,開始react-native學習和實踐,目前主要是從常規的native功能入手,逐步用react-native實現,基礎知識如開發環境搭建、除錯工具等官方文件有很清楚的指引,不再贅述,這裡主要是想把實際學習實踐中遇到的坑或者有意思的經歷記錄下來,為廣大react-native初學者提供一點參考。O(∩_∩)O~
話不多說,進入正題,今天要實現的是一個載入動畫,效果如下:
很簡單一個動畫,不是麼?用native實現實在是小菜一碟,現在我們試著用RN來實現它!
先將動畫的檢視結構搭建出來,這個比較簡單,就是4個會變形的View順序排列:
1 2 3 4 5 6 |
<View style={styles.square}> <Animated.View style={[styles.line,{height:this.state.fV}]}> <Animated.View style={[styles.line,{height:this.state.sV}]}> <Animated.View style={[styles.line,{height:this.state.tV}]}> <Animated.View style={[styles.line,{height:this.state.foV}]}> </View> |
這裡的檢視結構很普通,只不過在RN中,需要施加動畫的檢視,都不能是普通的View,而是Animated.View,包括施加動畫的圖片,也應該是Animated.Image,需要注意。
RN繼承了react的核心思想,基於虛擬DOM和資料驅動的模式,用state來管理檢視層,所以RN的動畫和react的動畫類似,都是通過改變state從而執行render進行檢視重繪,展現動畫。
毫無疑問,先從Animated庫下手,這是facebook官方提供的專門用於實現動畫的庫,它比較強大,整合了多種常見的動畫形式,正如官方文件寫道:
Animated focuses on declarative relationships between inputs and outputs, with configurable transforms in between, and simple start/stop methods to control time-based animation execution.
它專注於輸入和輸出之間的對應關係,其間是可以配置的各種變形,通過簡單的開始和停止方法來控制基於時間的動畫。
所以使用這個庫的時候,需要清楚知道動畫的輸入值,不過這並不代表需要知道每一個時刻動畫的精確屬性值,因為這是一種插值動畫,Animated只需要知道初始值和結束值,它會將所有中間值動態計算出來運用到動畫中,這有點類似於CSS3中的關鍵幀動畫。它提供了spring、decay、timing三種動畫方式,其實這也就是三種不同的差值方式,指定相同的初始值和結束值,它們會以不同的函式計算中間值並運用到動畫中,最終輸出的就是三種不同的動畫,比如官方給出的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
class Playground extends React.Component { constructor(props: any) { super(props); this.state = { bounceValue: new Animated.Value(0),//這裡設定了動畫的輸入初始值,注意不是數字0 }; } render(): ReactElement { return ( Animated.Image //這裡不是普通Image元件 source={{uri: 'http://i.imgur.com/XMKOH81.jpg'}} style={{ flex: 1, transform: [ //新增變換,transform的值是陣列,包含一系列施加到物件上的變換 {scale: this.state.bounceValue}, // 變換是縮放,縮放值state裡的bounceValue,這個值是一個動態值,也是動畫的根源 ] }} /> ); } componentDidMount() { this.state.bounceValue.setValue(1.5); // 元件載入的時候設定bounceValue,因此圖片會被放大1.5倍 Animated.spring( //這裡運用的spring方法,它的差值方式不是線性的,會呈現彈性的效果 this.state.bounceValue, //spring方法的第一個引數,表示被動態插值的變數 { toValue: 0.8, //這裡就是輸入值的結束值 friction: 1, //這裡是spring方法接受的特定引數,表示彈性係數 } ).start();// 開始spring動畫 } } |
可以想象該動畫效果大致為:圖片首先被放大1.5倍呈現出來,然後以彈性方式縮小到0.8倍。這裡的start方法還可以接收一個引數,引數是一個回撥函式,在動畫正常執行完畢之後,會呼叫這個回撥函式。
Animated庫不僅有spring/decay/timing三個方法提供三種動畫,還有sequence/decay/parallel等方法來控制動畫佇列的執行方式,比如多個動畫順序執行或者同時進行等。
介紹完了基礎知識,我們開始探索這個實際動畫的開發,這個動畫需要動態插值的屬性其實很簡單,只有四個檢視的高度值,其次,也不需要特殊的彈性或者緩動效果。所以我們只需要將每個檢視的高度依次變化,就可以了,so easy!
開始嘗試:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
Animated.timing( this.state.fV, { toValue: 100, duration:500, delay:500, } ).start(); Animated.timing( this.state.sV, { toValue: 100, duration:1000, delay:1000, } ).start(); Animated.timing( this.state.tV, { toValue: 100, duration:1000, delay:1500, } ).start(); Animated.timing( this.state.foV, { toValue: 100, duration:1000, delay:2000, } ).start(); |
WTF!
雖然動畫動起來了,但是這根本就是四根火柴在做廣播體操。。。
並且一個更嚴重的問題是,動畫執行完,就停止了。。。,而loading動畫應該是迴圈的,在查閱了文件及Animated原始碼之後,沒有找到類似loop這種控制迴圈的屬性,無奈之下,只能另闢蹊徑了。
上文提到過,Animated動畫的start方法可以在動畫完成之後執行回撥函式,如果動畫執行完畢之後再執行自己,就實現了迴圈,因此,將動畫封裝成函式,然後迴圈呼叫本身就可以了,不過目前動畫還只把高度變矮了,沒有重新變高的部分,因此即使迴圈也不會有效果,動畫部分也需要修正:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
...//其他部分程式碼 loopAnimation(){ Animated.parallel([//最外層是一個並行動畫,四個檢視的動畫以不同延遲並行執行 Animated.sequence([//這裡是一個順序動畫,針對每個檢視有兩個動畫:縮小和還原,他們依次進行 Animated.timing(//這裡是縮小動畫 this.state.fV, { toValue: Utils.getRealSize(100), duration:500, delay:0, } ), Animated.timing(//這裡是還原動畫 this.state.fV, { toValue: Utils.getRealSize(200), duration:500, delay:500,//注意這裡的delay剛好等於duration,也就是縮小之後,就開始還原 } ) ]), ...//後面三個數值的動畫類似,依次加大delay就可以 ]).start(this.loopAnimation2.bind(this)); } ... |
效果粗來了!
怎麼說呢,
動畫是粗來了,基本實現了迴圈動畫,但是總覺得缺少那麼點靈(sao)動(qi),仔細分析會發現,這是因為我們的迴圈的實現是通過執行回撥來實現的,當parallel執行完畢之後,會執行回撥進行第二次動畫,也就是說parallel不執行完畢,第二遍是不會開始的,這就是為什麼動畫會略顯僵硬,因此仔細觀察,第一個條塊在執行完自己的縮小放大動畫後,只有在等到第四個條也完成縮小放大動畫,整個並行佇列才算執行完,回撥才會被執行,第二遍動畫才開始。
So,回撥能被提前執行嗎?
Nooooooooooooooooooooop!
多麼感人,眼角貌似有翔滑過。。。。。
但是,不哭站擼的程式猿是不會輕易折服的,在多次查閱Animated文件之後,無果,累覺不愛(或許我們並不合適)~~~
好在facebook還提供了另一個更基礎的requestAnimationFrame函式,熟悉canvas動畫的同學對它應該不陌生,這是一個動畫重繪中經常遇到的方法,動畫的最基本原理就是重繪,通過在每次繪製的時候改變元素的位置或者其他屬性使得元素在肉眼看起來動起來了,因此,在碰壁之後,我們嘗試用它來實現我們的動畫。
其實,用requestAnimationFrame來實現動畫,就相當於需要我們自己來做插值,通過特定方式動態計算出中間值,將這些中間值賦值給元素的高度,就實現了動畫。
這四個動畫是完全相同的,只是以一定延遲順序進行的,因此分解之後只要實現一個就可以了,每個動畫就是條塊的高度隨時間呈現規律變化:
大概就介麼個意思。這是一個分段函式,弄起來比較複雜,我們可以將其近似成相當接近的連續函式–餘弦函式,這樣就相當輕鬆了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
let animationT=0;//定義一個全域性變數來標示動畫時間 let animationN=50,//餘弦函式的極值倍數,即最大偏移值範圍為正負50 animationM=150;//餘弦函式偏移值,使得極值在100-200之間 componentDidMount(){ animationT=0; requestAnimationFrame(this.loopAnimation.bind(this));//元件載入之後就執行loopAnimation動畫 } loopAnimation(){ var t0=animationT,t1=t0+0.5,t2=t1+0.5,t3=t2+timeDelay,t4=t3+0.5;//這裡分別是四個動畫的當前時間,依次加上了0.5的延遲 var v1=Number(Math.cos(t0).toFixed(2))*animationN+animationM;//將cos函式的小數值只精確到小數點2位,提高運算效率 var v2=Number(Math.cos(t1).toFixed(2))*animationN+animationM; var v3=Number(Math.cos(t2).toFixed(2))*animationN+animationM; var v4=Number(Math.cos(t3).toFixed(2))*animationN+animationM; this.setState({ fV:v1, sV:v2, tV:v3, foV:v4 }); animationT+=0.35;//增加時間值,每次增值越大動畫越快 requestAnimationFrame(this.loopAnimation.bind(this)); } |
最終效果:
可以看出,相當靈(sao)動(qi),由此也可以一窺RN的效能,我們知道,RN中的JS是執行在JavaScriptCore環境中的,對大多數React Native應用來說,業務邏輯是執行在JavaScript執行緒上的。這是React應用所在的執行緒,也是發生API呼叫,以及處理觸控事件等操作的執行緒。更新資料到原生支援的檢視是批量進行的,並且在事件迴圈每進行一次的時候被髮送到原生端,這一步通常會在一幀時間結束之前處理完(如果一切順利的話)。可以看出,我們在每一幀都進行了運算並改變了state,這是在JavaScript執行緒上進行的,然後通過RN推送到native端實時渲染每一幀,說實話,最開始對動畫的效能還是比較擔憂的,現在看來還算不錯,不過這只是一個很簡單的動畫,需要繪製的東西很少,在實際app應用中,還是需要結合實際情況不斷優化。
這個動畫應該還有更好更便捷的實現方式,這裡拋磚引玉,希望大家能夠在此基礎上探索出效能更好的實現方式並分享出來。
好了,這次動畫初探就到這裡,隨著學習和實踐的深入,還會陸續推出一系列分享,敬請關注。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式