这一小节我们来看一下 RN 中和动画相关的 API,主要包括:
- LayoutAnimation
- Animated
LayoutAnimation#
LayoutAnimation 是 RN 提供的一套全局布局动画 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 个可选值:
- duration:动画的时长
- create:组件创建时的动画
- update:组件更新时的动画
- delete:组件销毁时的动画
以 customSpring 为例,对应的 duration 为 400 毫秒,而 create 和 update 包括 delete 对应的又是一个对象,其类型定义如下:
type Anim = { duration? : number, // 动画时常 delay? : number, // 动画延迟 springDamping? : number, // 弹跳动画阻尼系数 initialV elocity? : number, // 初始速度 type? : $Enum<typeof TypesEnum> // 动画类型 property? : $Enum<typeof PropertiesEnum> // 动画属性}其中 type 定义在 LayoutAnimation.Types 中,常见的动画类型有:
- spring:弹跳动画
- linear:线性动画
- easeInEaseOut:缓入缓出动画
- easeIn:缓入动画
- easeOut:缓出动画
动画属性 property 定义在 LayoutAnimation.Properties 中,支持的动画属性有:
- opacity:透明度
- scaleXY:缩放
因此,上面我们所定义的 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。
Animated 是 RN 提供的另一种动画方式,相较于 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 对应的参数属性如下:
- duration: 动画的持续时间,默认为 500
- easing: 缓动动画,默认为 Easing.inOut
- delay: 开始动画前的延迟时间,默认为 0
- isInteraction: 指定本动画是否在 InteractionManager 的队列中注册以影响任务调度,默认值为 true
- useNativeDriver: 是否启用原生动画驱动,默认为 false
除了 timing 动画,Animated 还支持 decay 和 spring。每种动画类型都提供了特定的函数曲线,用于控制动画值从初始值到最终值的变化过程。
- decay:衰减动画,以一个初始速度开始并且逐渐减慢停止
- spring:弹跳动画,基于阻尼谐振动的弹性动画
- timing:渐变动画,按照线性函数执行的动画
在 Animated 动画 API 中,decay、spring 和 timing 是动画的核心,其他复杂动画都可以使用这三种动画类型来实现。
除了上面介绍的动画 API 之外,Animated 还支持复杂的组合动画,如常见的串行动画和并行动画。Animated 可以通过以下的方法将多个动画组合起来。
- parallel:并行执行
- sequence:顺序执行
- stagger:错峰执行,其实就是插入 delay 的 parallel 动画
来看一个示例:
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.Image
- Animated.ScrollView
- Animated.Text
- Animated.View
- Animated.FlatList
- Animated.SectionList
它们非常强大,基本可以满足大部分动画需求,在实际应用场景中,可以应用于透明度渐变、位移、缩放、颜色的变化等。
除了上面介绍的一些常见的动画场景,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。
作为提升用户体验的重要手段,动画对于移动应用程序来说是非常重要的,因此合理地使用动画是必须掌握的一项技能。