大多数情况下,在 React Native 中创建动画是推荐使用 Animated API 的,其提供了三个主要的方法用于创建动画:
译者注:React Native(0.37) 目前只支持Animated.Text/Animated.View/Animated.Image
以我的经验来看, Animated.timing() 和 Animated.spring() 在创建动画方面是非常有效的。
除了这三个创建动画的方法,对于每个独立的方法都有三种调用该动画的方式:
第一个要创建的动画是使用 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交互的载体。
Image用于在UI中显示图片。
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.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>
)
}
transform: [{rotate: spin}]
最后,在 container 样式中,使所有元素都居中:
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
})
这个示例动画的最终代码在 这里 。
这是 Easing 模块的源码链接,从源码中可以看到每一个 easing 方法。
我创建了另外一个示例项目,里面包含了大部分 easing 动画的实现,可以供你参考,链接在 这里 。(项目的运行截图)依据在下面:
上文已经说过了 Animated.timing 的基础知识,这一节会例举更多使用 Animated.timing 与 interpolate 结合实现的动画示例。
下一个示例中,会声明一个单一的动画值, this.animatedValue ,然后将该值和 interpolate 一起使用来驱动下列属性值的变化来创建复杂动画:
在开始之前,可以创建一个新分支或者清除上一个项目的旧代码。
第一件事是在构造函数中初始化一个需要用到的动画属性值:
constructor () {
super()
this.animatedValue = new Animated.Value(0)
}
接下来,创建一个名为 animate 的方法,并在 componentDidMount() 中调用该方法:
componentDidMount () {
this.animate()
}
animate () {
this.animatedValue.setValue(0)
Animated.timing(
this.animatedValue,
{
toValue: 1,
duration: 2000,
easing: Easing.linear
}
).start(() => this.animate())
}
在 render 方法中,我们创建 5 个不同的插值变量:
render () {
const marginLeft = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [0, 300]
})
const opacity = this.animatedValue.interpolate({
inputRange: [0, 0.5, 1],
outputRange: [0, 1, 0]
})
const movingMargin = this.animatedValue.interpolate({
inputRange: [0, 0.5, 1],
outputRange: [0, 300, 0]
})
const textSize = this.animatedValue.interpolate({
inputRange: [0, 0.5, 1],
outputRange: [18, 32, 18]
})
const rotateX = this.animatedValue.interpolate({
inputRange: [0, 0.5, 1],
outputRange: ['0deg', '180deg', '0deg']
})
...
}
interpolate是一个很强大的方法,允许我们用多种方式来使用单一的动画属性值: this.animatedValue 。因为 this.animatedValue 只是简单的从0变到1,因而我们能将这个值插入到 opacity、margins、text sizes 和 rotation 等样式属性中。
最后,返回实现了上述变量的 Animated.View 和 Animated.Text 组件:
return (
Animated Text!
Hello from TransformX
)
当然,也需要更新下 container 样式:
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 150
}
})
接下来,我们将会使用 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,
{
toValue: 1,
friction: 1
}
).start()
}
动画已经设置好了,我们将其放在 View 的click事件中,动画元素依然是之前使用过的 React logo 图片:
<Text
style={{marginBottom: 100}}
onPress={this.spring.bind(this)}>SpringText>
227, height: 200, transform: [{scale: this.springValue}] }}
source={{uri: 'https://s3.amazonaws.com/media-p.slid.es/uploads/alexanderfarennikov/images/1198519/reactjs.png'}}/>
View>
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,
duration,
easing,
delay
}
)
}
Animated.parallel([
createAnimation(this.animatedValue1, 2000, Easing.ease),
createAnimation(this.animatedValue2, 1000, Easing.ease, 1000),
createAnimation(this.animatedValue3, 1000, Easing.ease, 2000)
]).start()
}
在 animate 方法中,我们将三个动画属性值重置为0。此外,还创建了一个 createAnimation 方法,该方法接受四个参数:value, duration, easing, delay(默认值是0),返回一个新的动画。
然后,调用 Animated.parallel() ,并将三个使用 createAnimation 创建的动画作为参数传递给它。
在 render 方法中,我们需要设置插值:
render () {
const scaleText = this.animatedValue1.interpolate({
inputRange: [0, 1],
outputRange: [0.5, 2]
})
const spinText = this.animatedValue2.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '720deg']
})
const introButton = this.animatedValue3.interpolate({
inputRange: [0, 1],
outputRange: [-100, 400]
})
...
}
最后,我们用一个主 View 包裹三个 Animated.Views:
Welcome
to the App!
Click Here To Start
当 animate() 被调用时,三个动画会同时执行。
先看一下这个api是怎么调用的:
// API
Animated.sequence(arrayOfAnimations)
// In use
Animated.sequence([
Animated.timing(
animatedValue,
{
//config options
}
),
Animated.spring(
animatedValue2,
{
//config options
}
)
])
和 Animated.parallel() 一样, Animated.sequence() 接受一个动画数组。但不同的是, Animated.sequence() 是按顺序执行一个动画数组里的动画,等待一个完成后再执行下一个。
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View,
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。
先看一下这个api是怎么调用的:
// API
Animated.stagger(delay, arrayOfAnimations)
// In use:
Animated.stagger(1000, [
Animated.timing(
animatedValue,
{
//config options
}
),
Animated.spring(
animatedValue2,
{
//config options
}
)
])
和 Animated.parallel() 和 Animated.sequence() 一样, Animated.Stagger 接受一个动画数组。但不同的是,Animated.Stagger 里面的动画有可能会同时执行(重叠),不过会以指定的延迟来开始。
与上述两个动画主要的不同点是 Animated.Stagger 的第一个参数, delay 会被应用到每一个动画:
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View,
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: 4000
}
)
})
Animated.stagger(10, 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('SampleApp', () => animations);
React Native Animations Using the Animated API