Skip to content

动画API

这一小节我们来看一下 RN 中和动画相关的 API,主要包括:

LayoutAnimation#

LayoutAnimationRN 提供的一套全局布局动画 API,只需要配置好动画的相关属性(例如大小、位置、透明度),然后调用组件的状态更新方法引起重绘,这些布局变化就会在下一次渲染时以动画的形式呈现。

Android 设备上使用 LayoutAnimation,需要通过 UIManager 手动启用,并且需要放在任何动画代码之前,比如可以放在入口文件 App.js 中。

if (Platform.OS === 'android') {
if (UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true)
}
}

下面我们来看一个示例:

const customAnim = {
customSpring: {
duration: 400,
create: {
type: LayoutAnimation.Types.spring,
property: LayoutAnimation.Properties.scaleXY,
springDamping: 0.6,
},
update: {
type: LayoutAnimation.Types.spring,
springDamping: 0.6,
},
},
customLinear: {
duration: 200,
create: {
type: LayoutAnimation.Types.linear,
property: LayoutAnimation.Properties.opacity,
},
update: {
type: LayoutAnimation.Types.easeInEaseOut,
},
},
}

在上面的代码中,我们定义了 customAnim 是一个对象,该对象包含了两种动画方式,一种是 customSpring,另一种是 customLinear

每一种动画都用对象来描述,包含 4 个可选值:

customSpring 为例,对应的 duration400 毫秒,而 createupdate 包括 delete 对应的又是一个对象,其类型定义如下:

type Anim = {
duration? : number, // 动画时常
delay? : number, // 动画延迟
springDamping? : number, // 弹跳动画阻尼系数
initialV elocity? : number, // 初始速度
type? : $Enum<typeof TypesEnum> // 动画类型
property? : $Enum<typeof PropertiesEnum> // 动画属性
}

其中 type 定义在 LayoutAnimation.Types 中,常见的动画类型有:

动画属性 property 定义在 LayoutAnimation.Properties 中,支持的动画属性有:

因此,上面我们所定义的 customSpring 动画的不同属性值也就非常清晰了。

customSpring: {
duration: 400,
create: {
type: LayoutAnimation.Types.spring,
property: LayoutAnimation.Properties.scaleXY,
springDamping: 0.6,
},
update: {
type: LayoutAnimation.Types.spring,
springDamping: 0.6,
},
},

下面附上该示例的完整代码:

import React, { useState } from 'react'
import {
View,
StyleSheet,
Text,
LayoutAnimation,
TouchableOpacity,
UIManager,
} from 'react-native'
if (
Platform.OS === 'android' &&
UIManager.setLayoutAnimationEnabledExperimental
) {
UIManager.setLayoutAnimationEnabledExperimental(true)
}
const customAnim = {
customSpring: {
duration: 400,
create: {
type: LayoutAnimation.Types.spring,
property: LayoutAnimation.Properties.scaleXY,
springDamping: 0.6,
},
update: {
type: LayoutAnimation.Types.spring,
springDamping: 0.6,
},
},
customLinear: {
duration: 200,
create: {
type: LayoutAnimation.Types.linear,
property: LayoutAnimation.Properties.opacity,
},
update: {
type: LayoutAnimation.Types.easeInEaseOut,
},
},
}
const App = () => {
const [width, setWidth] = useState(200)
const [height, setHeight] = useState(200)
const [whichAni, setWhichAni] = useState(true)
function largePress() {
whichAni
? LayoutAnimation.configureNext(customAnim.customSpring)
: LayoutAnimation.configureNext(customAnim.customLinear)
setWhichAni(!whichAni)
setWidth(width + 20)
setHeight(height + 20)
}
return (
<View style={styles.container}>
<View style={[styles.content, { width, height }]} />
<TouchableOpacity style={styles.btnContainer} onPress={largePress}>
<Text style={styles.textStyle}>点击增大</Text>
</TouchableOpacity>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#F5FCFF',
},
content: {
backgroundColor: '#FF0000',
marginBottom: 10,
},
btnContainer: {
marginTop: 30,
marginLeft: 10,
marginRight: 10,
backgroundColor: '#EE7942',
height: 38,
width: 320,
borderRadius: 5,
justifyContent: 'center',
alignItems: 'center',
},
textStyle: {
fontSize: 18,
color: '#ffffff',
},
})
export default App

当然,如果不想那么麻烦的进行配置,LayoutAnimation 也提供了一些 linear、spring 的替代方法,这些替代方法会直接使用默认值。

例如:

function largePress() {
whichAni ? LayoutAnimation.spring() : LayoutAnimation.linear()
setWhichAni(!whichAni)
setWidth(width + 20)
setHeight(height + 20)
}

Animated#

前面所学习的 LayoutAnimation 称为布局动画,这种方法使用起来非常便捷,它会在如透明度渐变、缩放这类变化时触发动画效果,动画会在下一次渲染或布局周期运行。布局动画还有个优点就是无需使用动画化组件,如 Animated.View

AnimatedRN 提供的另一种动画方式,相较于 LayoutAnimation,它更为精细,可以只作为单个组件的单个属性,也可以更加手势的响应来设定动画(例如通过手势放大图片等行为),甚至可以将多个动画变化组合到一起,并可以根据条件中断或者修改。

下面我们先来看一个快速入门示例:

import React, { useState } from 'react'
import { Animated, Text, View, StyleSheet, Button, Easing } from 'react-native'
const App = () => {
// fadeAnim will be used as the value for opacity. Initial Value: 0
const [fadeInValue, setFadeInValue] = useState(new Animated.Value(0))
const fadeIn = () => {
// Will change fadeAnim value to 1 in 5 seconds
Animated.timing(fadeInValue, {
toValue: 1,
duration: 2000,
easing: Easing.linear,
useNativeDriver: true,
}).start()
}
const fadeOut = () => {
// Will change fadeAnim value to 0 in 3 seconds
Animated.timing(fadeInValue, {
toValue: 0,
duration: 3000,
useNativeDriver: true,
}).start()
}
return (
<View style={styles.container}>
<Animated.View
style={[
styles.fadingContainer,
{
// Bind opacity to animated value
opacity: fadeInValue,
},
]}
>
<Text style={styles.fadingText}>Fading View!</Text>
</Animated.View>
<View style={styles.buttonRow}>
<Button title="Fade In View" onPress={fadeIn} />
<Button title="Fade Out View" onPress={fadeOut} />
</View>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
fadingContainer: {
padding: 20,
backgroundColor: 'powderblue',
},
fadingText: {
fontSize: 28,
},
buttonRow: {
flexBasis: 100,
justifyContent: 'space-evenly',
marginVertical: 16,
},
})
export default App

在上面的代码中,我们书写了一个淡入淡出的效果。下面我们来分析其中关键的代码。

const [fadeInValue, setFadeInValue] = useState(new Animated.Value(0))

App 组件中,我们定义了一个状态 fadeInValue,该状态的初始值为 new Animated.Value(0),这就是设置动画的初始值。

<Animated.View
style={[
styles.fadingContainer,
{
// Bind opacity to animated value
opacity: fadeInValue,
},
]}
>
<Text style={styles.fadingText}>Fading View!</Text>
</Animated.View>

接下来,我们将要应用动画的组件包裹在 Animated.View 组件中,然后将 Animated.Value 绑定到组件的 style 属性上。

之后点击按钮的时候,我们要控制 Text 的显隐效果,按钮各自绑定事件,对应的代码:

const fadeIn = () => {
// Will change fadeAnim value to 1 in 5 seconds
Animated.timing(fadeInValue, {
toValue: 1,
duration: 2000,
easing: Easing.linear,
useNativeDriver: true,
}).start()
}
const fadeOut = () => {
// Will change fadeAnim value to 0 in 3 seconds
Animated.timing(fadeInValue, {
toValue: 0,
duration: 3000,
useNativeDriver: true,
}).start()
}

在事件处理函数中,使用 Animated.timing 方法并设置动画参数,最后调用 start 方法启动动画。

timing 对应的参数属性如下:

除了 timing 动画,Animated 还支持 decayspring。每种动画类型都提供了特定的函数曲线,用于控制动画值从初始值到最终值的变化过程。

Animated 动画 API 中,decay、springtiming 是动画的核心,其他复杂动画都可以使用这三种动画类型来实现。

除了上面介绍的动画 API 之外,Animated 还支持复杂的组合动画,如常见的串行动画和并行动画。Animated 可以通过以下的方法将多个动画组合起来。

来看一个示例:

import React, { Component } from 'react'
import {
View,
StyleSheet,
Text,
Animated,
Easing,
TouchableOpacity,
} from 'react-native'
/**
* 串行动画
*/
export default class AnimatedTiming extends Component {
constructor(props) {
super(props)
this.state = {
bounceValue: new Animated.Value(0),
rotateValue: new Animated.Value(0),
}
}
onPress() {
Animated.sequence([
//串行动画函数
Animated.spring(this.state.bounceValue, {
toValue: 1,
useNativeDriver: true,
}), //弹性动画
Animated.delay(500),
Animated.timing(this.state.rotateValue, {
//渐变动画
toValue: 1,
duration: 800,
easing: Easing.out(Easing.quad),
useNativeDriver: true,
}),
]).start(() => this.onPress()) //开始执行动画
}
render() {
return (
<View style={styles.container}>
<Animated.View
style={[
styles.content,
{
transform: [
{
rotate: this.state.rotateValue.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
}),
},
{
scale: this.state.bounceValue,
},
],
},
]}
>
<Text style={styles.content}>Hello World!</Text>
</Animated.View>
<TouchableOpacity
style={styles.btnContainer}
onPress={this.onPress.bind(this)}
>
<Text style={styles.textStyle}>串行动画</Text>
</TouchableOpacity>
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#F5FCFF',
},
content: {
backgroundColor: '#FF0000',
marginBottom: 10,
padding: 10,
},
btnContainer: {
marginTop: 30,
marginLeft: 10,
marginRight: 10,
backgroundColor: '#EE7942',
height: 38,
width: 320,
borderRadius: 5,
justifyContent: 'center',
alignItems: 'center',
},
textStyle: {
fontSize: 18,
color: '#ffffff',
},
})

在上面我们就是使用的 Animated.sequence 顺序执行,如果想要并行执行,可以将上面 Animated.sequence 部分代码修改为:

Animated.parallel([
//串行动画函数
Animated.spring(this.state.bounceValue, {
toValue: 1,
useNativeDriver: true,
}), //弹性动画
Animated.timing(this.state.rotateValue, {
//渐变动画
toValue: 1,
duration: 800,
easing: Easing.out(Easing.quad),
useNativeDriver: true,
}),
]).start(() => this.onPress()) //开始执行动画

关于动画化组件,前面我们使用的是 Animated.View,目前官方提供的动画化组件有 6 种:

它们非常强大,基本可以满足大部分动画需求,在实际应用场景中,可以应用于透明度渐变、位移、缩放、颜色的变化等。

除了上面介绍的一些常见的动画场景,Animated 还支持手势控制动画。手势控制动画使用的是 Animated.event,它支持将手势或其他事件直接绑定到动态值上。

来看一个示例,下面是使用 Animated.event 实现图片水平滚动时的图片背景渐变效果。

import React, { useState } from 'react'
import {
ScrollView,
Animated,
Image,
View,
StyleSheet,
Dimensions,
} from 'react-native'
const { width } = Dimensions.get('window')
const App = () => {
const [xOffset, setXOffset] = useState(new Animated.Value(1.0))
return (
<View style={styles.container}>
<ScrollView
horizontal={true}
showsHorizontalScrollIndicator={false}
style={styles.imageStyle}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { x: xOffset } } }],
{ useNativeDriver: false }
)}
scrollEventThrottle={100}
>
<Animated.Image
source={{ uri: 'http://doc.zwwill.com/yanxuan/imgs/banner-1.jpg' }}
style={[
styles.imageStyle,
{
opacity: xOffset.interpolate({
inputRange: [0, 375],
outputRange: [1.0, 0.0],
}),
},
]}
resizeMode="cover"
/>
<Image
source={{ uri: 'http://doc.zwwill.com/yanxuan/imgs/banner-2.jpg' }}
style={styles.imageStyle}
resizeMode="cover"
/>
</ScrollView>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
marginTop: 44,
justifyContent: 'center',
backgroundColor: '#F5FCFF',
},
imageStyle: {
height: 200,
width: width,
},
})
export default App

ScrollView 逐渐向左滑动时,左边的图片的透明度会逐渐降为 0

作为提升用户体验的重要手段,动画对于移动应用程序来说是非常重要的,因此合理地使用动画是必须掌握的一项技能。