學習和實踐react已經有一段時間了,在經歷了從最初的彷徨到解決痛點時的興奮,再到不斷實踐后遭遇問題時的苦悶,確實被這一種新的思維方式和開發模式所折服,react不是萬能的,在很多場景下濫用反而會適得其反,這里不展開討論。
有了react的實踐經驗,結合之前自己的一點ios開發經驗,決定繼續冒險,開始react-native學習和實踐,目前主要是從常規的native功能入手,逐步用react-native實現,基礎知識如開發環境搭建、調試工具等官方文檔有很清楚的指引,不再贅述,這里主要是想把實際學習實踐中遇到的坑或者有意思的經歷記錄下來,為廣大react-native初學者提供一點參考。O(∩_∩)O~
話不多說,進入正題,今天要實現的是一個加載動畫,效果如下:
很簡單一個動畫,不是么?用native實現實在是小菜一碟,現在我們試着用RN來實現它!
先將動畫的視圖結構搭建出來,這個比較簡單,就是4個會變形的View順序排列:
<View style={styles.square}>
<Animated.View style={[styles.line,{height:this.state.fV}]}></Animated.View>
<Animated.View style={[styles.line,{height:this.state.sV}]}></Animated.View>
<Animated.View style={[styles.line,{height:this.state.tV}]}></Animated.View>
<Animated.View style={[styles.line,{height:this.state.foV}]}></Animated.View>
</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三種動畫方式,其實這也就是三種不同的差值方式,指定相同的初始值和結束值,它們會以不同的函數計算中間值並運用到動畫中,最終輸出的就是三種不同的動畫,比如官方給出的示例:
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!
開始嘗試:
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方法可以在動畫完成之后執行回調函數,如果動畫執行完畢之后再執行自己,就實現了循環,因此,將動畫封裝成函數,然后循環調用本身就可以了,不過目前動畫還只把高度變矮了,沒有重新變高的部分,因此即使循環也不會有效果,動畫部分也需要修正:
...//其他部分代碼
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來實現動畫,就相當於需要我們自己來做插值,通過特定方式動態計算出中間值,將這些中間值賦值給元素的高度,就實現了動畫。
這四個動畫是完全相同的,只是以一定延遲順序進行的,因此分解之后只要實現一個就可以了,每個動畫就是條塊的高度隨時間呈現規律變化:
大概就介么個意思。這是一個分段函數,弄起來比較復雜,我們可以將其近似成相當接近的連續函數--余弦函數,這樣就相當輕松了:
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應用中,還是需要結合實際情況不斷優化。
這個動畫應該還有更好更便捷的實現方式,這里拋磚引玉,希望大家能夠在此基礎上探索出性能更好的實現方式並分享出來。
好了,這次動畫初探就到這里,隨着學習和實踐的深入,還會陸續推出一系列分享,敬請關注。