近日,被安排作一個開場動畫的任務。雖然RN提供了Animated來自定義動畫,可是本次動畫中的元素頗多,交互甚煩。。。在完成任務的同時,發現不少步驟實際上是重複的,因而封裝了一個小組件記錄一下,分享給你們。javascript
分析一下:雖然此次的動畫需求步驟挺多的,可是把每一步動畫拆解成step1, step2, step3, step4... 講道理應該仍是可以實現的吧?嗯,用Animated.Value()建立值,而後再配上Animated.timing應該就行了。java
想到這,反手就是建立一個demo.js,先作個往上飄的氣球試試先吧。git
export class Demo1 extends PureComponent { constructor(props) { super(props); } componentWillMount() { this._initAnimation(); } componentDidMount() { this._playAnimation(); } _initAnimation() { this.topAnimatedValue = new Animated.Value(400); this.balloonStyle = { position: 'absolute', left: 137.5, top: this.topAnimatedValue.interpolate({ inputRange: [-999999, 999999], outputRange: [-999999, 999999] }) }; } _playAnimation() { Animated.timing(this.topAnimatedValue, { toValue: 200, duration: 1500 }).start(); } render() { return ( <View style={styles.demoContainer}> <Animated.Image style={[styles.balloonImage, this.balloonStyle]} source={require('../../pic/demo1/balloon.png')} /> </View> ); } }
固然,這是再簡單不過的基礎動畫了。。。若是咱們讓這裏的氣球一開始最好先是從底部的一個點放大,而且有一個漸入的效果,完了以後再往上飄,這該怎麼實現呢?因而代碼變成了這樣:github
export class Demo1 extends PureComponent { ... _interpolateAnimation(animatedValue, inputRange, outputRange) { return animatedValue.interpolate({inputRange, outputRange}); } _initAnimation() { this.opacityAnimatedValue = new Animated.Value(0); this.scaleAnimatedValue = new Animated.Value(0); this.topAnimatedValue = new Animated.Value(400); this.balloonStyle = { position: 'absolute', left: 137.5, opacity: this._interpolateAnimation(this.opacityAnimatedValue, [0, 1], [0, 1]), top: this._interpolateAnimation(this.topAnimatedValue, [-999999, 999999], [-999999, 999999]), transform:[{scale: this._interpolateAnimation(this.scaleAnimatedValue, [0, 1], [0, 1])}] }; } _playAnimation() { Animated.sequence([ this.step1(), this.step2() ]).start(); } step1() { return Animated.parallel([ Animated.timing(this.opacityAnimatedValue, { toValue: 1, duration: 500 }), Animated.timing(this.scaleAnimatedValue, { toValue: 1, duration: 500 }) ]); } step2() { return Animated.timing(this.topAnimatedValue, { toValue: 200, duration: 1500 }); } ... }
插句話:在動畫銜接的時候,仍是糾結了一下。由於Animated提供的方法仍是比較多的,這裏用到了sequence、parallel,分別可讓動畫順序執行和並行。除此以外,animtaion的start方法是支持傳入一個回調函數的,表示在當前動畫運行結束的時候會觸發這個回調。因此咱們還能夠這麼寫:promise
_playAnimation() { this.step1(() => this.step2()); // 不一樣之處1:step2做爲step1動畫結束以後的回調傳入 } step1(callback) { Animated.parallel([ Animated.timing(this.opacityAnimatedValue, { toValue: 1, duration: 500 }), Animated.timing(this.scaleAnimatedValue, { toValue: 1, duration: 500 }) ]).start(() => { callback && callback(); // 不一樣之處2:調用傳入的回調 }); } step2() { Animated.timing(this.topAnimatedValue, { toValue: 200, duration: 1500 }).start(); }
雖然一樣可以實現效果,可是仍是以爲這種方式不是很舒服,因此棄之。。。dom
到這裏,咱們已經對這個氣球作了漸變、放大、平移等3項操做。可是,若是有5個氣球,還有其餘各類元素又該怎麼辦呢?這才一個氣球咱們就已經用了opacityAnimatedValue,scaleAnimatedValue,topAnimatedValue三個變量來控制,更多的動畫元素那直就gg,不用下班了。。。ide
說實話,要作這麼個東西,怎麼就那麼像在作一個PPT呢。。。函數
「屏幕就比如是一張PPT背景圖;每個氣球就是PPT上的元素;你能夠經過拖動鼠標來擺放各個氣球,我能夠用絕對定位來肯定每一個氣球的位置;至於動畫嘛,剛纔的demo已經證實並不難實現,無非就是控制透明度、xy座標、縮放比例罷了。」工具
想到這,心中難免一陣竊喜。哈哈,有路子了,能夠對PPT上的這些元素封裝一個通用的組件,而後提供經常使用的一些動畫方法,剩下的事情就是調用這些動畫方法組裝成更復雜的動畫了。新建一個PPT:「出現、飛躍、淡化、浮入、百葉窗、棋盤。。。」看着這使人眼花繚亂的各類動畫,我想了下:嗯,我仍是從最簡單的作起吧。。。佈局
首先,咱們能夠將動畫分紅兩種:一次性動畫和循環動畫。
其次,做爲一個元素,它能夠用做動畫的屬性主要包括有:opacity, x, y, scale, angle等(這裏先只考慮了二維平面的,其實還能夠延伸擴展成三維立體的)。
最後,基本動畫均可以拆解爲這幾種行爲:出現/消失、移動、縮放、旋轉。
想到這,反手就是建立一個新文件,代碼以下:
// Comstants.js export const INF = 999999999; // Helper.js export const Helper = { sleep(millSeconds) { return new Promise(resolve => { setTimeout(() => resolve(), millSeconds); }); }, animateInterpolate(animatedValue, inputRange, outputRange) { if(animatedValue && animatedValue.interpolate) { return animatedValue.interpolate({inputRange, outputRange}); } } }; // AnimatedContainer.js import {INF} from "./Constants"; import {Helper} from "./Helper"; export class AnimatedContainer extends PureComponent { constructor(props) { super(props); } componentWillMount() { this._initAnimationConfig(); } _initAnimationConfig() { const {initialConfig} = this.props; const {opacity = 1, scale = 1, x = 0, y = 0, rotate = 0} = initialConfig; // create animated values: opacity, scale, x, y, rotate this.opacityAnimatedValue = new Animated.Value(opacity); this.scaleAnimatedValue = new Animated.Value(scale); this.rotateAnimatedValue = new Animated.Value(rotate); this.xAnimatedValue = new Animated.Value(x); this.yAnimatedValue = new Animated.Value(y); this.style = { position: 'absolute', left: this.xAnimatedValue, top: this.yAnimatedValue, opacity: Helper.animateInterpolate(this.opacityAnimatedValue, [0, 1], [0, 1]), transform: [ {scale: this.scaleAnimatedValue}, {rotate: Helper.animateInterpolate(this.rotateAnimatedValue, [-INF, INF], [`-${INF}rad`, `${INF}rad`])} ] }; } show() {} hide() {} scaleTo() {} rotateTo() {} moveTo() {} render() { return ( <Animated.View style={[this.style, this.props.style]}> {this.props.children} </Animated.View> ); } } AnimatedContainer.defaultProps = { initialConfig: { opacity: 1, scale: 1, x: 0, y: 0, rotate: 0 } };
第一步的骨架這就搭好了,簡單到本身都難以置信。。。接下來就是具體實現每個動畫的方法了,先拿show/hide開刀。
show(config = {opacity: 1, duration: 500}) { Animated.timing(this.opacityAnimatedValue, { toValue: config.opacity, duration: config.duration }).start(); } hide(config = {opacity: 0, duration: 500}) { Animated.timing(this.opacityAnimatedValue, { toValue: config.opacity, duration: config.duration }).start(); }
試了一下,簡直是文美~
可是!仔細一想,卻有個很嚴重的問題,這裏的動畫銜接該怎處理?要想作一個先show,而後過1s以後再hide的動畫該怎麼實現?貌似又回到了一開始考慮過的問題。不過此次,我倒是用Promise來解決這個問題。因而代碼又變成了這樣:
sleep(millSeconds) { return new Promise(resolve => setTimeout(() => resolve(), millSeconds)); } show(config = {opacity: 1, duration: 500}) { return new Promise(resolve => { Animated.timing(this.opacityAnimatedValue, { toValue: config.opacity, duration: config.duration }).start(() => resolve()); }); } hide(config = {opacity: 0, duration: 500}) { return new Promise(resolve => { Animated.timing(this.opacityAnimatedValue, { toValue: config.opacity, duration: config.duration }).start(() => resolve()); }); }
如今咱們再來看剛纔的動畫,只需這樣就能實現:
playAnimation() { this.animationRef .show() // 先出現 .sleep(1000) // 等待1s .then(() => this.animationRef.hide()); // 消失 }
甚至還能夠對createPromise這個過程再封裝一波:
_createAnimation(animationConfig = []) { const len = animationConfig.length; if (len === 1) { const {animatedValue, toValue, duration} = animationConfig[0]; return Animated.timing(animatedValue, {toValue, duration}); } else if (len >= 2) { return Animated.parallel(animationConfig.map(config => { return this._createAnimation([config]); })); } } _createAnimationPromise(animationConfig = []) { return new Promise(resolve => { const len = animationConfig.length; if(len <= 0) { resolve(); } else { this._createAnimation(animationConfig).start(() => resolve()); } }); } opacityTo(config = {opacity: .5, duration: 500}) { return this._createAnimationPromise([{ toValue: config.opacity, duration: config.duration, animatedValue: this.opacityAnimatedValue }]); } show(config = {opacity: 1, duration: 500}) { this.opacityTo(config); } hide(config = {opacity: 0, duration: 500}) { this.opacityTo(config); }
而後,咱們再把其餘的幾種基礎動畫(scale, rotate, move)實現也加上:
scaleTo(config = {scale: 1, duration: 1000}) { return this._createAnimationPromise([{ toValue: config.scale, duration: config.duration, animatedValue: this.scaleAnimatedValue }]); } rotateTo(config = {rotate: 0, duration: 500}) { return this._createAnimationPromise([{ toValue: config.rotate, duration: config.duration, animatedValue: this.rotateAnimatedValue }]); } moveTo(config = {x: 0, y: 0, duration: 1000}) { return this._createAnimationPromise([{ toValue: config.x, duration: config.duration, animatedValue: this.xAnimatedValue }, { toValue: config.y, duration: config.duration, animatedValue: this.yAnimatedValue }]); }
一次性動畫問題就這樣解決了,再來看看循環動畫怎麼辦。根據平時的經驗,一個循環播放的動畫通常都會這麼寫:
roll() { this.rollAnimation = Animated.timing(this.rotateAnimatedValue, { toValue: Math.PI * 2, duration: 2000 }); this.rollAnimation.start(() => { this.rotateAnimatedValue.setValue(0); this.roll(); }); } play() { this.roll(); } stop() { this.rollAnimation.stop(); }
沒錯,就是在一個動畫的start中傳入回調,而這個回調就是遞歸地調用播放動畫自己這個函數。那要是對應到咱們要封裝的這個組件,又該怎麼實現呢?
思考良久,爲了保持和一次性動畫API的一致性,咱們能夠給animatedContainer新增瞭如下幾個函數:
export class AnimatedContainer extends PureComponent { ... constructor(props) { super(props); this.cyclicAnimations = {}; } _createCyclicAnimation(name, animations) { this.cyclicAnimations[name] = Animated.sequence(animations); } _createCyclicAnimationPromise(name, animations) { return new Promise(resolve => { this._createCyclicAnimation(name, animations); this._playCyclicAnimation(name); resolve(); }); } _playCyclicAnimation(name) { const animation = this.cyclicAnimations[name]; animation.start(() => { animation.reset(); this._playCyclicAnimation(name); }); } _stopCyclicAnimation(name) { this.cyclicAnimations[name].stop(); } ... }
其中,_createCyclicAnimation,_createCyclicAnimationPromise是和一次性動畫的API對應的。可是,不一樣點在於傳入的參數發生了很大的變化:animationConfg -> (name, animations)
到這裏,循環動畫基本也已經封裝完畢。再來封裝兩個循環動畫roll(旋轉),blink(閃爍)試試:
blink(config = {period: 2000}) { return this._createCyclicAnimationPromise('blink', [ this._createAnimation([{ toValue: 1, duration: config.period / 2, animatedValue: this.opacityAnimatedValue }]), this._createAnimation([{ toValue: 0, duration: config.period / 2, animatedValue: this.opacityAnimatedValue }]) ]); } stopBlink() { this._stopCyclicAnimation('blink'); } roll(config = {period: 1000}) { return this._createCyclicAnimationPromise('roll', [ this._createAnimation([{ toValue: Math.PI * 2, duration: config.period, animatedValue: this.rotateAnimatedValue }]) ]); } stopRoll() { this._stopCyclicAnimation('roll'); }
忙活了大半天,總算是把AnimatedContainer封裝好了。先找個素材練練手吧~但是,找個啥呢?「叮」,只見手機上挖財的一個提醒亮了起來。嘿嘿,就你了,挖財的簽到頁面真的很適合(沒有作廣告。。。)效果圖以下:
渲染元素的render代碼就不貼了,可是咱們來看看動畫播放的代碼:
startOpeningAnimation() { // 簽到(一次性動畫) Promise .all([ this._header.show(), this._header.scaleTo({scale: 1}), this._header.rotateTo({rotate: Math.PI * 2}) ]) .then(() => this._header.sleep(100)) .then(() => this._header.moveTo({x: 64, y: 150})) .then(() => Promise.all([ this._tips.show(), this._ladder.sleep(150).then(() => this._ladder.show()) ])) .then(() => Promise.all([ this._today.show(), this._today.moveTo({x: 105, y: 365}) ])); // 星星閃爍(循環動畫) this._stars.forEach(item => item .sleep(Math.random() * 2000) .then(() => item.blink({period: 1000})) ); }
光看代碼,是否是就已經腦補整個動畫了~ 肥腸地一目瞭然,真的是美滋滋。
老規矩,本文代碼地址:https://github.com/SmallStoneSK/AnimatedContainer