ReactNative入门 —— 动画篇(下)
在上篇动画入门文章中我们了解了在 React Native 中简单的动画的实现方式,本篇将作为上篇的延续,介绍如何使用 Animated 实现一些比较复杂的动画。
动画组合
在 Animated 中提供了一些有趣的API方法来轻松地按我们的需求实现组合动画,它们分别是 Animated.parallel、Animated.sequence、Animated.stagger、Animated.delay。
我们会分别介绍这些方法,并从中学习到一些其它有用的API。最后我们会得到一个有趣的DOGE动画
1. Animated.parallel
并行执行一系列指定动画的方法,其格式如下:
Animated.parallel(Animates<Array>, [conf<Object>])
第一个参数接受一个元素为动画的数组,通过执行 start() 方法可以并行执行该数组中的所有方法。
如果数组中任意动画被中断的话,该数组内对应的全部动画会一起停止,不过我们可以通过第二个(可选)参数 conf 来取消这种牵连特性:
{stopTogether: false}
我们先看一个简单的、没有使用 Animated.parallel 的例子:
class AwesomeProject extends Component { constructor(props) { super(props); this.state = { grassTransY : new Animated.Value(Dimensions.get('window').height/2), bigDogeTrans : new Animated.ValueXY({ x: 100, y: 298 }) } } componentDidMount() { Animated.timing(this.state.grassTransY, { toValue: 200, duration: 1000, easing: Easing.bezier(0.15, 0.73, 0.37, 1.2) }).start(); Animated.timing(this.state.bigDogeTrans, { toValue: { x : Dimensions.get('window').width/2 - 139, y : -200 }, duration: 2000, delay: 1000 }).start(); } render() { return ( <View style={styles.container}> <Animated.View style={[styles.doges, {transform: this.state.bigDogeTrans.getTranslateTransform()}]} > <Image source={require('./src/img/bdoge.png')}/> </Animated.View> <Animated.View style={[styles.grass, {transform: [{translateY: this.state.grassTransY}]}]}></Animated.View> </View> ); } } var styles = StyleSheet.create({ grass: { position: 'absolute', width: Dimensions.get('window').width, backgroundColor: '#A3D900', height: 240 }, doges: { position: 'absolute' }, container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#73B9FF' } });
执行如下:
更改为 Animated.parallel 形式为(只需要修改 componentDidMount 代码块):
componentDidMount() { var timing = Animated.timing; Animated.parallel([ timing(this.state.grassTransY, { toValue: 200, duration: 1000, easing: Easing.bezier(0.15, 0.73, 0.37, 1.2) }), timing(this.state.bigDogeTrans, { toValue: { x : Dimensions.get('window').width/2 - 139, y : -200 }, duration: 2000, delay: 1000 }) ]).start(); }
执行后效果是一致的。
不过对于上方的代码,这里提一下俩处API:
new Animated.ValueXY({ x: 100, y: 298 })
以及我们给 doge 设置的样式:
{transform: this.state.bigDogeTrans.getTranslateTransform()}
它们是一个语法糖,Animated.ValueXY 方法会生成一个 x 和 y 的映射对象,方便后续使用相关方法将该对象转换为需要的样式对象。
例如这里我们通过 .getTranslateTransform() 方法将该 ValueXY 对象转换为 translate 的样式值应用到组件上,即其初始样式等价于
{transform: [{ translateX: 100 }, { translateY: 298 }]}
注意在 Animated.timing 中设置动画终值时需要以
{ x: XXX,
y: YYY
}
的形式来做修改:
timing(this.state.bigDogeTrans, { toValue: { //注意这里 x : Dimensions.get('window').width/2 - 139, y : -200 }, duration: 2000, delay: 1000 })
另外,除了能将 ValueXY 对象转为 translateX/Y 的 getTranslateTransform() 方法,我们还能通过 getLayout() 方法来将 ValueXY 对象转为 {left, top} 形式的样式对象:
style={{ transform: this.state.anim.getTranslateTransform() }}
不过这里就不举例了。
我们回到 Animated.parallel 方法的话题来。我们对开头的代码做小小的改动来学习一个新的API—— interpolate 插值函数:
class AwesomeProject extends Component { constructor(props) { super(props); this.state = { //注意这里初始化value都为0 grassTransY : new Animated.Value(0), bigDogeTransY : new Animated.Value(0) } } componentDidMount() { var timing = Animated.timing; Animated.parallel(['grassTransY', 'bigDogeTransY'].map((prop, i) => { var _conf = { toValue: 1, //注意这里设置最终value都为1 duration: 1000 + i * 1000 }; i || (_conf.easing = Easing.bezier(0.15, 0.73, 0.37, 1.2)); return timing(this.state[prop], _conf) })).start(); } render() { return ( <View style={styles.container}> <Animated.View style={[styles.doges, {transform: [{ translateX: Dimensions.get('window').width/2 - 139 }, { translateY: this.state.bigDogeTransY.interpolate({ inputRange: [0, 1], //动画value输入范围 outputRange: [298, -200] //对应的输出范围 }) }]}]}> <Image source={require('./src/img/bdoge.png')}/> </Animated.View> <Animated.View style={[styles.grass, {transform: [{ translateY: this.state.grassTransY.interpolate({ inputRange: [0, 1], outputRange: [Dimensions.get('window').height/2, 200] }) }]}]}></Animated.View> </View> ); } }
注意我们这里统一把动画属性初始值都设为0:
this.state = { //注意这里初始化value都为0 grassTransY : new Animated.Value(0), bigDogeTransY : new Animated.Value(0) }
然后又把动画属性的终值设为1:
var _conf = { toValue: 1, //注意这里设置最终value都为1 duration: 1000 }; return timing(this.state[prop], _conf)
然后通过 interpolate 插值函数将 value 映射为正确的值:
translateY: this.state.bigDogeTransY.interpolate({ inputRange: [0, 1], //动画value输入范围 outputRange: [298, -200] //对应的输出范围 })
这意味着当 value 的值为0时,interpolate 会将其转为 298 传给组件;当 value 的值为1时则转为 -200。
因此当value的值从0变化到1时,interpolate 会将其转为 (298 - 498 * value) 的值。
事实上 inputRange 和 outputRange 的取值非常灵活,我们看官网的例子:
value.interpolate({ inputRange: [-300, -100, 0, 100, 101], outputRange: [300, 0, 1, 0, 0], });
其映射为:
Input Output
-400 450
-300 300
-200 150
-100 0
-50 0.5
0 1
50 0.5
100 0
101 0
200 0
2. Animated.sequence
Animated的动画是异步执行的,如果希望它们能以队列的形式一个个逐步执行,那么 Animated.sequence 会是一个最好的实现。其语法如下:
Animated.sequence(Animates<Array>)
事实上了解了开头的 parallel 方法,后面几个方法都是一样套路了。
来个例子,我们依旧直接修改上方代码即可:
componentDidMount() { var timing = Animated.timing; Animated.sequence(['grassTransY', 'bigDogeTransY'].map((prop, i) => { var _conf = { toValue: 1 }; if(i==0){ _conf.easing = Easing.bezier(0.15, 0.73, 0.37, 1.2) } return timing(this.state[prop], _conf) })).start(); }
这样 doge 的动画会在 草地出来的动画结束后才开始执行:
3. Animated.stagger
该方法为 sequence 的变异版,支持传入一个时间参数来设置队列动画间的延迟,即让前一个动画结束后,隔一段指定时间才开始执行下一个动画。其语法如下:
Animated.stagger(delayTime<Number>, Animates<Array>)
其中 delayTime 为指定的延迟时间(毫秒),我们继续拿前面的代码来开刀(为了给力点我们再加入一个running doge的动画事件):
class AwesomeProject extends Component { constructor(props) { super(props); this.state = { grassTransY : new Animated.Value(0), bigDogeTransY : new Animated.Value(0), runningDogeTrans : new Animated.ValueXY({ x: Dimensions.get('window').width, y: Dimensions.get('window').height/2 - 120 }) } } componentDidMount() { var timing = Animated.timing; Animated.stagger(1500, ['grassTransY', 'bigDogeTransY', 'runningDogeTrans'].map((prop, i) => { var _conf = { toValue: 1 }; if(i==0){ _conf.easing = Easing.bezier(0.15, 0.73, 0.37, 1.2) } if(i==2){ //running doge _conf.toValue = { x: Dimensions.get('window').width - 150, y: Dimensions.get('window').height/2 - 120 } } return timing(this.state[prop], _conf) })).start(); } render() { return ( <View style={styles.container}> <Animated.View style={[styles.doges, {transform: [{ translateX: Dimensions.get('window').width/2 - 139 }, { translateY: this.state.bigDogeTransY.interpolate({ inputRange: [0, 1], outputRange: [298, -200] }) }]}]}> <Image source={require('./src/img/bdoge.png')}/> </Animated.View> <Animated.View style={[styles.grass, {transform: [{ translateY: this.state.grassTransY.interpolate({ inputRange: [0, 1], outputRange: [Dimensions.get('window').height/2, 200] }) }]}]}></Animated.View> <Animated.View style={[styles.doges, { transform: this.state.runningDogeTrans.getTranslateTransform() }]}> <Image source={require('./src/img/sdoge.gif')}/> </Animated.View> </View> ); } }
我们把三个动画间隔时间设定为 2000 毫秒,执行效果如下:
4. Animated.delay
噢这个接口实在太简单了,就是设置一段动画的延迟时间,接收一个时间参数(毫秒)作为指定延迟时长:
Animated.delay(delayTime<Number>)
常规还是跟好基友 Animated.sequence 一同使用,我们继续修改前面的代码:
class AwesomeProject extends Component { constructor(props) { super(props); this.state = { grassTransY : new Animated.Value(0), bigDogeTransY : new Animated.Value(0), runningDogeTrans : new Animated.ValueXY({ x: Dimensions.get('window').width, y: Dimensions.get('window').height/2 - 120 }) } } componentDidMount() { var timing = Animated.timing; Animated.sequence([ Animated.delay(1000), //延迟1秒再开始执行动画 timing(this.state.grassTransY, { toValue: 1, duration: 1000, easing: Easing.bezier(0.15, 0.73, 0.37, 1.2) }), timing(this.state.bigDogeTransY, { toValue: 1, duration: 3000 }), Animated.delay(2000), //延迟2秒再执行running doge动画 timing(this.state.runningDogeTrans, { toValue: { x: Dimensions.get('window').width - 150, y: Dimensions.get('window').height/2 - 120 }, duration: 2000 }), Animated.delay(1000), //1秒后跑到中间 timing(this.state.runningDogeTrans, { toValue: { x: Dimensions.get('window').width/2 - 59, y: Dimensions.get('window').height/2 - 180 }, duration: 1000 }) ] ).start(); } render() { return ( <View style={styles.container}> <Animated.View style={[styles.doges, {transform: [{ translateX: Dimensions.get('window').width/2 - 139 }, { translateY: this.state.bigDogeTransY.interpolate({ inputRange: [0, 1], //动画value输入范围 outputRange: [298, -200] //对应的输出范围 }) }]}]}> <Image source={require('./src/img/bdoge.png')}/> </Animated.View> <Animated.View style={[styles.grass, {transform: [{ translateY: this.state.grassTransY.interpolate({ inputRange: [0, 1], outputRange: [Dimensions.get('window').height/2, 200] }) }]}]}></Animated.View> <Animated.View style={[styles.doges, { transform: this.state.runningDogeTrans.getTranslateTransform() }]}> <Image source={require('./src/img/sdoge.gif')}/> </Animated.View> </View> ); } }
执行如下:
到这里我们基本就掌握了 RN 动画的常用API了,对于本章的 Doge 动画我们再搞复杂一点——再加一只running doge,然后在动画结束后,让最大的Doge头一直不断地循环旋转:
class AwesomeProject extends Component { constructor(props) { super(props); this.state = { grassTransY : new Animated.Value(0), bigDogeTransY : new Animated.Value(0), bigDogeRotate : new Animated.Value(0), runningDogeTrans : new Animated.ValueXY({ x: Dimensions.get('window').width, y: Dimensions.get('window').height/2 - 120 }), runningDoge2Trans : new Animated.ValueXY({ x: Dimensions.get('window').width, y: Dimensions.get('window').height/2 - 90 }) } } componentDidMount() { var timing = Animated.timing; Animated.sequence([ Animated.delay(1000), //延迟1秒再开始执行动画 timing(this.state.grassTransY, { toValue: 1, duration: 1000, easing: Easing.bezier(0.15, 0.73, 0.37, 1.2) }), timing(this.state.bigDogeTransY, { toValue: 1, duration: 3000 }), Animated.parallel([ Animated.sequence([ timing(this.state.runningDogeTrans, { toValue: { x: Dimensions.get('window').width - 150, y: Dimensions.get('window').height/2 - 120 }, duration: 2000 }), Animated.delay(1000), //1秒后跑到中间 timing(this.state.runningDogeTrans, { toValue: { x: Dimensions.get('window').width/2 - 99, y: Dimensions.get('window').height/2 - 180 }, duration: 1000 }) ]), Animated.sequence([ timing(this.state.runningDoge2Trans, { toValue: { x: Dimensions.get('window').width/2 + 90, y: Dimensions.get('window').height/2 - 90 }, duration: 2000 }), Animated.delay(1000), timing(this.state.runningDoge2Trans, { toValue: { x: Dimensions.get('window').width/2 + 20, y: Dimensions.get('window').height/2 - 110 }, duration: 1000 }) ]) ]) ] ).start(()=>{ this.bigDogeRotate() }); } //大doge一直不断循环 bigDogeRotate(){ this.state.bigDogeRotate.setValue(0); //重置Rotate动画值为0 Animated.timing(this.state.bigDogeRotate, { toValue: 1, duration: 5000 }).start(() => this.bigDogeRotate()) } render() { return ( <View style={styles.container}> <Animated.View style={[styles.doges, {transform: [{ translateX: Dimensions.get('window').width/2 - 139 }, { translateY: this.state.bigDogeTransY.interpolate({ inputRange: [0, 1], //动画value输入范围 outputRange: [298, -200] //对应的输出范围 }) }, { rotateZ: this.state.bigDogeRotate.interpolate({ inputRange: [0, 1], //动画value输入范围 outputRange: ['0deg', '360deg'] //对应的输出范围 }) }]}]}> <Image source={require('./src/img/bdoge.png')}/> </Animated.View> <Animated.View style={[styles.grass, {transform: [{ translateY: this.state.grassTransY.interpolate({ inputRange: [0, 1], outputRange: [Dimensions.get('window').height/2, 200] }) }]}]}></Animated.View> <Animated.View style={[styles.doges, { transform: this.state.runningDogeTrans.getTranslateTransform() }]}> <Image source={require('./src/img/sdoge.gif')}/> </Animated.View> <Animated.View style={[styles.doges, { transform: this.state.runningDoge2Trans.getTranslateTransform() }]}> <Image source={require('./src/img/sdoge.gif')}/> </Animated.View> </View> ); } }
最终效果如下:
写完这篇文章都凌晨了,我也是蛮拼的
共勉~