參考鏈接:
React Native開發(fā)之動畫(Animations)
最近ReactNative(以下簡稱RN)在前端的熱度越來越高,不少同學開始在業(yè)務中嘗試使用RN咐汞,這里著重介紹一下RN中動畫的使用與實現(xiàn)原理。
使用篇
舉個簡單的栗子
var React = require('react-native');
var {
Animated,
Easing,
View,
StyleSheet,
Text
} = React;
var Demo = React.createClass({
getInitialState() {
return {
fadeInOpacity: new Animated.Value(0) // 初始值
};
},
componentDidMount() {
Animated.timing(this.state.fadeInOpacity, {
toValue: 1, // 目標值
duration: 2500, // 動畫時間
easing: Easing.linear // 緩動函數(shù)
}).start();
},
render() {
return (
<Animated.View style={[styles.demo, {
opacity: this.state.fadeInOpacity
}]}>
<Text style={styles.text}>悄悄的儒鹿,我出現(xiàn)了</Text>
</Animated.View>
);
}
});
var styles = StyleSheet.create({
demo: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'white',
},
text: {
fontSize: 30
}
});
是不是很簡單易懂<(?????)> 和JQuery的Animation用法很類似化撕。
步驟拆解
一個RN的動畫,可以按照以下步驟進行挺身。
使用基本的Animated組件侯谁,如Animated.View Animated.Image Animated.Text (重要!不加Animated的后果就是一個看不懂的報錯章钾,然后查半天動畫參數(shù)墙贱,最后懷疑人生)
使用Animated.Value設定一個或多個初始化值(透明度,位置等等)贱傀。
將初始化值綁定到動畫目標的屬性上(如style)
通過Animated.timing等函數(shù)設定動畫參數(shù)
調用start啟動動畫惨撇。
栗子敢再復雜一點嗎?
顯然府寒,一個簡單的漸顯是無法滿足各位觀眾老爺們的好奇心的.我們試一試加上多個動畫
getInitialState() {
return (
fadeInOpacity: new Animated.Value(0),
rotation: new Animated.Value(0),
fontSize: new Animated.Value(0)
);
},
componentDidMount() {
var timing = Animated.timing;
Animated.parallel(['fadeInOpacity', 'rotation', 'fontSize'].map(property => {
return timing(this.state[property], {
toValue: 1,
duration: 1000,
easing: Easing.linear
});
})).start();
}魁衙,
render() {
return (<Animated.View style={[styles.demo, {
opacity: this.state.fadeInOpacity,
transform: [{
rotateZ: this.state.rotation.interpolate({
inputRange: [0,1],
outputRange: ['0deg', '360deg']
})
}]
}]}><Animated.Text style={{
fontSize: this.state.fontSize.interpolate({
inputRange: [0,1],
outputRange: [12,26]
})
}}>我騎著七彩祥云出現(xiàn)了????</Animated.Text>
</Animated.View>
);
}
注意到我們給文字區(qū)域加上了字體增大的動畫效果报腔,相應地,也要修改Text為Animated.Text
強大的interpolate
上面的栗子使用了interpolate函數(shù)剖淀,也就是插值函數(shù)纯蛾。這個函數(shù)很強大,實現(xiàn)了數(shù)值大小纵隔、單位的映射轉換翻诉,比如
{
inputRange: [0,1],
outPutRange: ['0deg','180deg']
}
當setValue(0.5)時,會自動映射成90deg捌刮。 inputRange并不局限于[0,1]區(qū)間碰煌,可以畫出多段。 interpolate一般用于多個動畫共用一個Animated.Value绅作,只需要在每個屬性里面映射好對應的值芦圾,就可以用一個變量控制多個動畫。 事實上俄认,上例中的fadeInOpacityfontSizerotation用一個變量來聲明就可以了个少。(那你寫那么多變量逗我嗎(╯‵□′)╯︵┻━┻) (因為我要強行使用parallel ?┬─┬ ノ( ' – 'ノ))
流程控制
在剛才的栗子中,我們使用了Parallel來實現(xiàn)多個動畫的并行渲染梭依,其它用于流程控制的API還有:
sequence接受一系列動畫數(shù)組為參數(shù)稍算,并依次執(zhí)行
stagger接受一系列動畫數(shù)組和一個延遲時間,按照序列役拴,每隔一個延遲時間后執(zhí)行下一個動畫(其實就是插入了delay的parrllel)
delay生成一個延遲時間(基于timing的delay參數(shù)生成)
例3
getInitialState() {
return (
anim: [1,2,3].map(() => new Animated.Value(0)) // 初始化3個值
);
},
componentDidMount() {
var timing = Animated.timing;
Animated.sequence([
Animated.stagger(200, this.state.anim.map(left => {
return timing(left, {
toValue: 1,
});
}).concat(
this.state.anim.map(left => {
return timing(left, {
toValue: 0,
});
})
)), // 三個view滾到右邊再還原糊探,每個動作間隔200ms
Animated.delay(400), // 延遲400ms,配合sequence使用
timing(this.state.anim[0], {
toValue: 1
}),
timing(this.state.anim[1], {
toValue: -1
}),
timing(this.state.anim[2], {
toValue: 0.5
}),
Animated.delay(400),
Animated.parallel(this.state.anim.map((anim) => timing(anim, {
toValue: 0
}))) // 同時回到原位置
]
).start();
},
render() {
var views = this.state.anim.map(function(value, i) {
return (
<Animated.View
key={i}
style={[styles.demo, styles['demo' + i], {
left: value.interpolate({
inputRange: [0,1],
outputRange: [0,200]
})
}]}>
<Text style={styles.text}>我是第{i + 1}個View</Text>
</Animated.View>
);
});
return <View style={styles.container}>
<Text>sequence/delay/stagger/parallel演示</Text>
{views}
</View>;
}
Spring/Decay/Timing
前面的幾個動畫都是基于時間實現(xiàn)的河闰,事實上科平,在日常的手勢操作中,基于時間的動畫往往難以滿足復雜的交互動畫姜性。對此瞪慧,RN還提供了另外兩種動畫模式。
Spring 彈簧效果
friction 摩擦系數(shù)部念,默認40
tension 張力系數(shù)弃酌,默認7
bounciness
speed
Decay 衰變效果
velocity 初速率
deceleration 衰減系數(shù) 默認0.997
Spring支持 friction與tension 或者 bounciness與speed 兩種組合模式,這兩種模式不能并存儡炼。 其中friction與tension模型來源于origami,一款F家自制的動畫原型設計工具妓湘,而bounciness與speed則是傳統(tǒng)的彈簧模型參數(shù)。
Track && Event
RN動畫支持跟蹤功能乌询,這也是日常交互中很常見的需求炮沐,比如跟蹤用戶的手勢變化桨螺,跟蹤另一個動畫姆泻。而跟蹤的用法也很簡單,只需要指定toValue到另一個Animated.Value就可以了冲泥。 交互動畫需要跟蹤用戶的手勢操作,Animated也很貼心地提供了事件接口的封裝,示例:
// Animated.event 封裝手勢事件等值映射到對應的Animated.Value
onPanResponderMove: Animated.event(
[null, {dx: this.state.x, dy: this.state.y}] // map gesture to leader
)
在官方的demo上改了一下,加了一張費玉污的圖霜浴,效果圖如下 代碼太長,就不貼出來了蓝纲,可以參考官方Github代碼
[圖片上傳中坷随。。驻龟。(4)]
動畫循環(huán)
Animated的start方法是支持回調函數(shù)的,在動畫或某個流程結束的時候執(zhí)行缸匪,這樣子就可以很簡單地實現(xiàn)循環(huán)動畫了翁狐。
startAnimation() {
this.state.rotateValue.setValue(0);
Animated.timing(this.state.rotateValue, {
toValue: 1,
duration: 800,
easing: Easing.linear
}).start(() => this.startAnimation());
}
是不是很魔性?[doge]
原理篇
首先感謝能看到這里的小伙伴們:)
在上面的文章中,我們已經基本掌握了RN Animated的各種常用API凌蔬,接下來我們來了解一下這些API是如何設計出來的露懒。
聲明: 以下內容參考自Animated原作者的分享視頻
首先,從React的生命周期來編程的話砂心,一個動畫大概是這樣子寫:
getInitialState() {
return {left: 0};
}
render(){
return (
<div style={{left: this.state.left}}>
<Child />
</div>
);
}
onChange(value) {
this.setState({left: value});
}
只需要通過requestAnimationFrame調用onChange懈词,輸入對應的value,動畫就簡單粗暴地跑起來了????辩诞,全劇終坎弯。
然而事實總是沒那么簡單,問題在哪译暂?
我們看到抠忘,上述動畫基本是以毫秒級的頻率在調用setState,而React的每次setState都會重新調用render方法外永,并切遍歷子元素進行渲染崎脉,即使有Dom Diff也可能扛不住這么大的計算量和UI渲染。
那么該如何優(yōu)化呢伯顶?
關鍵詞:
ShouldComponentUpdate
<StaticContainer>(靜態(tài)容器)
Element Caching (元素緩存)
Raw DOM Mutation (原生DOM操作)
↑↑↓↓←→←→BA (秘籍)
ShouldComponentUpdate
學過React的都知道囚灼,ShouldComponentUpdate是性能優(yōu)化利器,只需要在子組件的shouldComponentUpdate返回false祭衩,分分鐘渲染性能爆表灶体。
[圖片上傳中。汪厨。赃春。(7)]
然而并非所有的子元素都是一成不變的,粗暴地返回false的話子元素就變成一灘死水了劫乱。而且組件間應該是獨立的织中,子組件很可能是其他人寫的锥涕,父元素不能依賴于子元素的實現(xiàn)。
<StaticContainer>(靜態(tài)容器)
這時候可以考慮封裝一個容器狭吼,管理ShouldCompontUpdate层坠,如圖示:
[圖片上傳中。刁笙。破花。(8)]
小明和老王再也不用關心父元素的動畫實現(xiàn)啦。
一個簡單的<StaticContainer>實現(xiàn)如下:
class StaticContainer extends React.Component {
render(){
return this.props.children;
}
shouldComponentUpdate(nextProps){
return nextProps.shouldUpdate; // 父元素控制是否更新
}
}
// 父元素嵌入StaticContainer
render() {
return (
<div style={{left: this.state.left}}>
<StaticContainer
shouldUpdate={!this.state.isAnimating}>
<ExpensiveChild />
</StaticContainer>
</div>
);
}
Element Caching 緩存元素
還有另一種思路優(yōu)化子元素的渲染疲吸,那就是緩存子元素的渲染結果到局地變量座每。
render(){
this._child = this._child || <ExpensiveChild />;
return (
<div style={{left:this.state.left}}>
{this._child}
</div>
);
}
緩存之后,每次setState時摘悴,React通過DOM Diff就不再渲染子元素了峭梳。
上面的方法都有弊端,就是條件競爭蹂喻。當動畫在進行的時候葱椭,子元素恰好獲得了新的state,而這時候動畫無視了這個更新口四,最后就會導致狀態(tài)不一致孵运,或者動畫結束的時候子元素發(fā)生了閃動,這些都是影響用戶操作的問題蔓彩。
Raw DOM Mutation 原生DOM操作
剛剛都是在React的生命周期里實現(xiàn)動畫治笨,事實上,我們只想要變更這個元素的left值粪小,并不希望各種重新渲染大磺、DOM DIFF等等發(fā)生。
“React探膊,我知道自己要干啥杠愧,你一邊涼快去“
如果我們跳出這個生命周期,直接找到元素進行變更逞壁,是不是更簡單呢流济?
簡單易懂,性能彪悍腌闯,有木有绳瘟?!
然而弊端也很明顯姿骏,比如這個組件unmount之后糖声,動畫就報錯了。
Uncaught Exception: Cannot call ‘style’ of null
而且這種方法照樣避不開條件競爭——動畫值改變的時候,有可能發(fā)生setState之后蘸泻,left又回到初始值之類的情況琉苇。
再者,我們使用React悦施,就是因為不想去關心dom元素的操作并扇,而是交給React管理,直接使用Dom操作顯然違背了初衷抡诞。
↑↑↓↓←→←→BA (秘籍)
嘮叨了這么多穷蛹,這也不行,那也不行昼汗,什么才是真理肴熏?
我們既想要原生DOM操作的高性能,又想要React完善的生命周期管理顷窒,如何把兩者優(yōu)勢結合到一起呢扮超?答案就是Data Binding(數(shù)據(jù)綁定)
render(){
return(
<Animated.div style={{left: this.state.left}}>
<ExpensiveChild />
</Animated.div>
);
}
getInitialState(){
return {left: new Animated.Value(0)}; // 實現(xiàn)了數(shù)據(jù)綁定的類
}
onUpdate(value){
this.state.left.setValue(value); // 不是setState
}
首先,需要實現(xiàn)一個具有數(shù)據(jù)綁定功能的類Animated.Value蹋肮,提供setValueonChange等接口。 其次璧疗,由于原生的組件并不能識別Value坯辩,需要將動畫元素用Animated包裹起來,在內部處理數(shù)據(jù)變更與DOM操作崩侠。
一個簡單的動畫組件實現(xiàn)如下:
Animated.div = class extends React.Component{
componentWillUnmount() {
nextProps.style.left.removeAllListeners();
},
// componentWillMount需要完成與componentWillReceiveProps同樣的操作漆魔,此處略
componentWillReceiveProps(nextProps) {
nextProps.style.left.removeAllListeners();
nextProps.style.left.onChange(value => {
React.findDOMNode(this).style.left = value + 'px';
});
// 將動畫值解析為普通數(shù)值傳給原生div
this._props = React.addons.update(
nextProps,
{style:{left:{$set: nextProps.style.left.getValue()}}}
);
},
render() {
return <div ...{this._props} />;
}
}
代碼很簡短,做的事情有:
遍歷傳入的props却音,查找是否有Animated.Value的實例改抡,并綁定相應的DOM操作。
每次props變更或者組件unmount的時候系瓢,停止監(jiān)聽數(shù)據(jù)綁定事件阿纤,避免了條件競爭和內存泄露問題。
將初始傳入的Animated.Value值逐個轉化為普通數(shù)值夷陋,再交給原生的React組件進行渲染欠拾。
綜上,通過封裝一個Animated的元素骗绕,內部通過數(shù)據(jù)綁定和DOM操作變更元素藐窄,結合React的生命周期完善內存管理,解決條件競爭問題酬土,對外表現(xiàn)則與原生組件相同荆忍,實現(xiàn)了高效流暢的動畫效果。
讀到這里,應該知道為什么ImageText等做動畫一定要使用Animated加持過的元素了吧刹枉?
參考資料
React Addons Update
React Component Lifecycle
Christopher Chedeau – Animated
好書推薦 《React Native開發(fā)指南》
原創(chuàng)文章轉載請注明:
轉載自AlloyTeam:http://www.alloyteam.com/2016/01/reactnative-animated/