近日,被安排作一个开场动画的任务。虽然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