大多数情况下,在 React Native 中创建动画是推荐使用 Animated API 的,其提供了三个主要的方法用于创建动画:
Animated.timing() -- 推动一个值按照一个过渡曲线而随时间变化。
Easing
模块定义了很多缓冲曲线函数。Animated.decay() -- 推动一个值以一个初始的速度和一个衰减系数逐渐变为0。
Animated.spring() -- 产生一个基于 Rebound 和 Origami 实现的Spring动画。它会在
toValue
值更新的同时跟踪当前的速度状态,以确保动画连贯。
译者注:React Native(0.37) 目前只支持Animated.Text/Animated.View/Animated.Image
以我的经验来看,Animated.timing() 和 Animated.spring() 在创建动画方面是非常有效的。
除了这三个创建动画的方法,对于每个独立的方法都有三种调用该动画的方式:
Animated.parallel() -- 同时开始一个动画数组里的全部动画。默认情况下,如果有任何一个动画停止了,其余的也会被停止。你可以通过
stopTogether
选项来改变这个效果。Animated.sequence() -- 按顺序执行一个动画数组里的动画,等待一个完成后再执行下一个。如果当前的动画被中止,后面的动画则不会继续执行。
Animated.stagger() -- 一个动画数组,里面的动画有可能会同时执行(重叠),不过会以指定的延迟来开始。
1. Animated.timing()
第一个要创建的动画是使用 Animated.timing
创建的旋转动画。
// Example implementation: Animated.timing( someValue,{ toValue: number,duration: number,easing: easingFunction,delay: number } )
这种方式常用于创建需要loading指示的动画,在我使用React Native的项目中,这也是创建动画最有效的方式。这个理念也可以用于其它诸如按比例放大和缩小类型的指示动画。
开始之前,我们需要创建一个新的React Native 项目或者一个空的React Native项目。创建新项目之前,需要输入 react-native init
来初始化一个项目,并切换到该项目目录:
react-native init animations cd animations
然后打开 index.android.js
和 index.ios.js
。
现在已经创建了一个新项目,则第一件事是在已经引入的 View 之后从 react native
中引入 Animated,Image 和 Easing:
import { AppRegistry,StyleSheet,Text,View,Animated,Image,Easing } from 'react-native'
Animated 是我们将用于创建动画的库,和React Native交互的载体。
Easing 也是用React Native创建动画的载体,它允许我们使用已经定义好的各种缓冲函数,例如:linear,ease,quad,cubic,sin,elastic,bounce,back,bezier,in,out,inout 。由于有直线运动,我们将使用 linear。在这节(阅读)完成之后,对于实现直线运动的动画,你或许会有更好的实现方式。
接下来,需要在构造函数中初始化一个带动画属性的值用于旋转动画的初始值:
constructor () { super() this.spinValue = new Animated.Value(0) }
我们使用 Animated.Value 声明了一个 spinValue 变量,并传了一个 0 作为初始值。
然后创建了一个名为 spin
的方法,并在 componentDidMount
中调用它,目的是在 app 加载之后运行动画:
componentDidMount () { this.spin() } spin () { this.spinValue.setValue(0) Animated.timing( this.spinValue,{ toValue: 1,duration: 4000,easing: Easing.linear } ).start(() => this.spin()) }
spin()
方法的作用如下:
将 this.spinValue 重置成 0
调用 Animated.timing ,并驱动
this.spinValue
的值以Easing.linear
的动画方式在 4000 毫秒从 0 变成 1。Animated.timing 需要两个参数,一个要变化的值(本文中是 this.spinValue) 和一个可配置对象。这个配置对象有四个属性:toValue(终值)、duration(一次动画的持续时间)、easing(缓存函数)和delay(延迟执行的时间)调用 start(),并将 this.spin 作为回调传递给
start
,它将在(一次)动画完成之后调用,这也是创建无穷动画的一种基本方式。start()
需要一个完成回调,该回调在动画正常的运行完成之后会被调用,并有一个参数是{finished: true}
,但如果动画是在它正常运行完成之前而被停止了(如:被手势动作或者其它动画中断),则回调函数的参数变为{finished: false}
。
译者注:如果在回调中将动画的初始值设置成其终值,该动画就不会再执行。如将 this.spinValue.setValue(0) 改为 this.spinValue.setValue(1),spin动画不会执行了
现在方法已经创建好了,接下来就是在UI中渲染动画了。为了渲染动画,需要更新 render
方法:
render () { const spin = this.spinValue.interpolate({ inputRange: [0,1],outputRange: ['0deg','360deg'] }) return ( <View style={styles.container}> <Animated.Image style={{ width: 227,height: 200,transform: [{rotate: spin}] }} source={{uri: 'https://s3.amazonaws.com/media-p.slid.es/uploads/alexanderfarennikov/images/1198519/reactjs.png'}} /> </View> ) }
在
render
方法中,创建了一个 spin 变量,并调用了 this.spinValue 的 interpolate 方法。interpolate 方法可以在任何一个 Animated.Value 返回的实例上调用,该方法会在属性更新之前插入一个新值,如将 0~1 映射到 1~10。在我们的demo中,利用 interpolate 方法将数值 0~1 映射到了 0deg~360deg。我们传递了inputRange
和outputRange
参数给interpolate 方法,并分别赋值为 [0,1] 和 &[‘0deg’,‘360deg’]。我们返回了一个带
container
样式值的 View和 带 height,width和 transform 属性的Animated.Image,并将 spin 的值赋给transform
的 rotate 属性,这也是动画发生的地方:
transform: [{rotate: spin}]
最后,在 container
样式中,使所有元素都居中:
const styles = StyleSheet.create({ container: { flex: 1,justifyContent: 'center',alignItems: 'center' } })
关于Easing
这是 Easing
模块的源码链接,从源码中可以看到每一个 easing 方法。
我创建了另外一个示例项目,里面包含了大部分 easing 动画的实现,可以供你参考,链接在这里。(项目的运行截图)依据在下面:
该项目实现的 easing 动画在 RNPlay 的地址在这里。
2. Animated.timing 示例
上文已经说过了 Animated.timing 的基础知识,这一节会例举更多使用 Animated.timing 与 interpolate 结合实现的动画示例。
下一个示例中,会声明一个单一的动画值, this.animatedValue
,然后将该值和 interpolate
一起使用来驱动下列属性值的变化来创建复杂动画:
marginLeft
opacity
fontSize
rotateX
在开始之前,可以创建一个新分支或者清除上一个项目的旧代码。
constructor () { super() this.animatedValue = new Animated.Value(0) }
接下来,创建一个名为animate
的方法,并在 componentDidMount() 中调用该方法:
componentDidMount () { this.animate() } animate () { this.animatedValue.setValue(0) Animated.timing( this.animatedValue,duration: 2000,easing: Easing.linear } ).start(() => this.animate()) }
在 render
方法中,我们创建 5 个不同的插值变量:
render () { const marginLeft = this.animatedValue.interpolate({ inputRange: [0,outputRange: [0,300] }) const opacity = this.animatedValue.interpolate({ inputRange: [0,0.5,1,0] }) const movingMargin = this.animatedValue.interpolate({ inputRange: [0,300,0] }) const textSize = this.animatedValue.interpolate({ inputRange: [0,outputRange: [18,32,18] }) const rotateX = this.animatedValue.interpolate({ inputRange: [0,'180deg','0deg'] }) ... }
interpolate 是一个很强大的方法,允许我们用多种方式来使用单一的动画属性值:this.animatedValue。因为 this.animatedValue
只是简单的从0变到1,因而我们能将这个值插入到 opacity、margins、text sizes 和 rotation 等样式属性中。
最后,返回实现了上述变量的 Animated.View 和 Animated.Text 组件:
return ( <View style={styles.container}> <Animated.View style={{ marginLeft,height: 30,width: 40,backgroundColor: 'red'}} /> <Animated.View style={{ opacity,marginTop: 10,backgroundColor: 'blue'}} /> <Animated.View style={{ marginLeft: movingMargin,backgroundColor: 'orange'}} /> <Animated.Text style={{ fontSize: textSize,color: 'green'}} > Animated Text! </Animated.Text> <Animated.View style={{ transform: [{rotateX}],marginTop: 50,backgroundColor: 'black'}}> <Text style={{color: 'white'}}>Hello from TransformX</Text> </Animated.View> </View> )
当然,也需要更新下 container
样式:
const styles = StyleSheet.create({ container: { flex: 1,paddingTop: 150 } })
3. Animated.spring()
接下来,我们将会使用 Animated.spring() 方法创建动画。
// Example implementation: Animated.spring( someValue,{ toValue: number,friction: number } )
我们继续使用上一个项目,并只需要更新少量代码就行。在构造函数中,创建一个 springValue 变量,初始化其值为0.3:
constructor () { super() this.springValue = new Animated.Value(0.3) }
然后,删除 animated
方法和componentDidMount
方法,创建一个新的 spring
方法:
spring () { this.springValue.setValue(0.3) Animated.spring( this.springValue,friction: 1 } ).start() }
将
springValue
的值重置为 0.3调用
Animated.spring
方法,并传递两个参数:一个要变化的值和一个可配置对象。可配置对象的属性可以是下列的任何值:toValue (number),overshootClamping (boolean),restDisplacementThreshold (number),restSpeedThreshold (number),velocity (number),bounciness (number),speed (number),tension(number),和 friction (number)。除了 toValue 是必须的,其他值都是可选的,但 friction 和 tension 能帮你更好地控制 spring 动画。调用
start()
启动动画
动画已经设置好了,我们将其放在 View 的click事件中,动画元素依然是之前使用过的 React logo 图片:
<View style={styles.container}> <Text style={{marginBottom: 100}} onPress={this.spring.bind(this)}>Spring</Text> <Animated.Image style={{ width: 227,transform: [{scale: this.springValue}] }} source={{uri: 'https://s3.amazonaws.com/media-p.slid.es/uploads/alexanderfarennikov/images/1198519/reactjs.png'}}/> </View>
4. Animated.parallel()
Animated.parallel() 会同时开始一个动画数组里的全部动画。
先看一下这个api是怎么调用的:
// API Animated.parallel(arrayOfAnimations) // In use: Animated.parallel([ Animated.spring( animatedValue,{ //config options } ),Animated.timing( animatedValue2,{ //config options } ) ])
开始之前,我们先直接创建三个我们需要的动画属性值:
constructor () { super() this.animatedValue1 = new Animated.Value(0) this.animatedValue2 = new Animated.Value(0) this.animatedValue3 = new Animated.Value(0) }
然后,创建一个 animate
方法并在 componendDidMount() 中调用它:
componentDidMount () { this.animate() } animate () { this.animatedValue1.setValue(0) this.animatedValue2.setValue(0) this.animatedValue3.setValue(0) const createAnimation = function (value,duration,easing,delay = 0) { return Animated.timing( value,{ toValue: 1,delay } ) } Animated.parallel([ createAnimation(this.animatedValue1,2000,Easing.ease),createAnimation(this.animatedValue2,1000,Easing.ease,1000),createAnimation(this.animatedValue3,2000) ]).start() }
在 animate
方法中,我们将三个动画属性值重置为0。此外,还创建了一个 createAnimation 方法,该方法接受四个参数:value,delay(默认值是0),返回一个新的动画。
然后,调用 Animated.parallel()
,并将三个使用 createAnimation
创建的动画作为参数传递给它。
在 render
方法中,我们需要设置插值:
render () { const scaleText = this.animatedValue1.interpolate({ inputRange: [0,outputRange: [0.5,2] }) const spinText = this.animatedValue2.interpolate({ inputRange: [0,'720deg'] }) const introButton = this.animatedValue3.interpolate({ inputRange: [0,outputRange: [-100,400] }) ... }
scaleText -- 插值的输出范围是从0.5到2,我们会用这个值对文本按0.5到2的比例进行缩放
spinText -- 插值的输出范围是 0 degrees 到 720 degrees,即将元素旋转两周
最后,我们用一个主 View 包裹三个 Animated.Views:
<View style={[styles.container]}> <Animated.View style={{ transform: [{scale: scaleText}] }}> <Text>Welcome</Text> </Animated.View> <Animated.View style={{ marginTop: 20,transform: [{rotate: spinText}] }}> <Text style={{fontSize: 20}}> to the App! </Text> </Animated.View> <Animated.View style={{top: introButton,position: 'absolute'}}> <TouchableHighlight onPress={this.animate.bind(this)} style={styles.button}> <Text style={{color: 'white',fontSize: 20}}> Click Here To Start </Text> </TouchableHighlight> </Animated.View> </View>
当 animate()
被调用时,三个动画会同时执行。
5. Animated.Sequence()
先看一下这个api是怎么调用的:
// API Animated.sequence(arrayOfAnimations) // In use Animated.sequence([ Animated.timing( animatedValue,Animated.spring( animatedValue2,{ //config options } ) ])
和 Animated.parallel() 一样, Animated.sequence() 接受一个动画数组。但不同的是,Animated.sequence() 是按顺序执行一个动画数组里的动画,等待一个完成后再执行下一个。
import React,{ Component } from 'react'; import { AppRegistry,Animated } from 'react-native' const arr = [] for (var i = 0; i < 500; i++) { arr.push(i) } class animations extends Component { constructor () { super() this.animatedValue = [] arr.forEach((value) => { this.animatedValue[value] = new Animated.Value(0) }) } componentDidMount () { this.animate() } animate () { const animations = arr.map((item) => { return Animated.timing( this.animatedValue[item],{ toValue: 1,duration: 50 } ) }) Animated.sequence(animations).start() } render () { const animations = arr.map((a,i) => { return <Animated.View key={i} style={{opacity: this.animatedValue[a],height: 20,width: 20,backgroundColor: 'red',marginLeft: 3,marginTop: 3}} /> }) return ( <View style={styles.container}> {animations} </View> ) } } const styles = StyleSheet.create({ container: { flex: 1,flexDirection: 'row',flexWrap: 'wrap' } }) AppRegistry.registerComponent('animations',() => animations);
由于 Animated.sequence()
和 Animated.parallel()
很相似,因而对 Animated.sequence()
就不多作介绍了。主要不同的一点是我们是使用循环创建 Animated.Values。
6. Animated.Stagger()
先看一下这个api是怎么调用的:
// API Animated.stagger(delay,arrayOfAnimations) // In use: Animated.stagger(1000,[ Animated.timing( animatedValue,{ //config options } ) ])
和 Animated.parallel() 和 Animated.sequence() 一样, Animated.Stagger 接受一个动画数组。但不同的是,Animated.Stagger 里面的动画有可能会同时执行(重叠),不过会以指定的延迟来开始。
与上述两个动画主要的不同点是 Animated.Stagger 的第一个参数,delay
会被应用到每一个动画:
import React,Animated } from 'react-native' const arr = [] for (var i = 0; i < 500; i++) { arr.push(i) } class animations extends Component { constructor () { super() this.animatedValue = [] arr.forEach((value) => { this.animatedValue[value] = new Animated.Value(0) }) } componentDidMount () { this.animate() } animate () { const animations = arr.map((item) => { return Animated.timing( this.animatedValue[item],duration: 4000 } ) }) Animated.stagger(10,animations).start() } render () { const animations = arr.map((a,flexWrap: 'wrap' } }) AppRegistry.registerComponent('SampleApp',() => animations);
文中使用的demo repo: react native animations