ReactNative入门 —— 动画篇(上)
在不使用任何RN动画相关API的时候,我们会想到一种非常粗暴的方式来实现我们希望的动画效果——通过修改state来不断得改变视图上的样式。
我们来个简单的示例:
var AwesomeProject = React.createClass({ getInitialState() { return { w: 200, h: 20 } }, _onPress() { //每按一次增加近30宽高 var count = 0; while(++count<30){ requestAnimationFrame(()=>{ this.setState({w: this.state.w + 1, h: this.state.h + 1}) }) } } render: function() { return ( <View style={styles.container}> <View style={[styles.content, {width: this.state.w, height: this.state.h}]}> <Text style={[{textAlign: 'center'}]}>Hi, here is VaJoy</Text> </View> <TouchableOpacity onPress={this._onPress}> <View style={styles.button}> <Text style={styles.buttonText}>Press me!</Text> </View> </TouchableOpacity> <TouchableOpacity> <View style={styles.button}> <Text style={styles.buttonText}>忽略本按钮</Text> </View> </TouchableOpacity> </View> ); } });
效果如下:
这种方式实现的动画存在两大问题:
1. 将频繁地销毁、重绘视图来实现动画效果,性能体验很糟糕,常规表现为内存花销大且动画卡顿明显;
2. 动画较为生硬,毕竟web的css3不适用在RN上,无法轻易设定动画方式(比如ease-in、ease-out)。
因此在RN上设置动画,还是得乖乖使用相应的API来实现,它们都能很好地触达组件底层的动画特性,以原生的形式来实现动画效果。
LayoutAnimation
LayoutAnimation 是在布局发生变化时触发动画的接口(我们在上一篇文章里已经介绍过),这意味着你需要通过修改布局(比如修改了组件的style、插入新组件)来触发动画。
该接口最常用的方法是 LayoutAnimation.configureNext(conf<Object>) ,用来设置下次布局变化时的动画效果,需要在执行 setState 前调用。
其中 conf 参数格式参考:
{ duration: 700, //持续时间 create: { //若是新布局的动画类型 type: 'linear', //线性模式 property: 'opacity' //动画属性,除了opacity还有一个scaleXY可以配置 }, update: { //若是布局更新的动画类型 type: 'spring', //弹跳模式 springDamping: 0.4 //弹跳阻尼系数 } }
其中动画type的类型可枚举为:
spring //弹跳 linear //线性 easeInEaseOut //缓入缓出 easeIn //缓入 easeOut //缓出 keyboard //键入
要注意的是,安卓平台使用 LayoutAnimation 动画必须加上这么一句代码(否则动画会失效):
UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true);
于是我们一开始的动画可以这么来写:
var AwesomeProject = React.createClass({ getInitialState() { return { w: 200, h: 20 } }, _onPress() { LayoutAnimation.configureNext({ duration: 700, //持续时间 create: { type: 'linear', property: 'opacity' }, update: { type: 'spring', springDamping: 0.4 } }); this.setState({w: this.state.w + 30, h: this.state.h + 30}) } render: function() { return ( <View style={styles.container}> <View style={[styles.content, {width: this.state.w, height: this.state.h}]}> <Text style={[{textAlign: 'center'}]}>Hi, here is VaJoy</Text> </View> <TouchableOpacity onPress={this._onPress}> <View style={styles.button}> <Text style={styles.buttonText}>Press me!</Text> </View> </TouchableOpacity> <TouchableOpacity> <View style={styles.button}> <Text style={styles.buttonText}>忽略本按钮</Text> </View> </TouchableOpacity> </View> ); } });
这时候动画灵活和流畅多了:
ok我们上例看到的仅仅是布局更新的情况,我们来看看新布局被创建(有新组件加入到视图上)的情况如何:
var AwesomeProject = React.createClass({ getInitialState() { return { showNewOne : false, w: 200, h: 20 } }, _onPress() { LayoutAnimation.configureNext({ duration: 1200, create: { type: 'linear', property: 'opacity' //注意这里,我们设置新布局被创建时的动画特性为透明度 }, update: { type: 'spring', springDamping: 0.4 } }); this.setState({w: this.state.w + 30, h: this.state.h + 30, showNewOne : true}) }, render: function() { var newOne = this.state.showNewOne ? ( <View style={[styles.content, {width: this.state.w, height: this.state.h}]}> <Text style={[{textAlign: 'center'}]}>new one</Text> </View> ) : null; return ( <View style={styles.container}> <View style={[styles.content, {width: this.state.w, height: this.state.h}]}> <Text style={[{textAlign: 'center'}]}>Hi, here is VaJoy</Text> </View> {newOne} <TouchableOpacity onPress={this._onPress}> <View style={styles.button}> <Text style={styles.buttonText}>Press me!</Text> </View> </TouchableOpacity> <TouchableOpacity> <View style={styles.button}> <Text style={styles.buttonText}>忽略本按钮</Text> </View> </TouchableOpacity> </View> ); } });
效果如下:
setNativeProps
如果我们执意使用开篇的修改state的方式,觉得这种方式更符合当前需求对动画的控制,那么则应当使用原生组件的 setNativeProps 方法来做对应实现,它会直接修改组件底层特性,不会重绘组件,因此性能也远胜动态修改组件内联style的方法。
该接口属原生组件(比如View,比如TouchableOpacity)才拥有的原生特性接口,调用格式参考如下:
Component.setNativeProps({ style: {transform: [{rotate:'50deg'}]} });
对于开篇的动画示例,我们可以做如下修改:
var _s = { w: 200, h: 20 }; var AwesomeProject = React.createClass({ _onPress() { var count = 0; while(count++<30){ requestAnimationFrame(()=>{ this.refs.view.setNativeProps({ style: { width : ++_s.w, height : ++_s.h } }); }) } }, render: function() { return ( <View style={styles.container}> <View ref="view" style={[styles.content, {width: 200, height: 20}]}> <Text style={[{textAlign: 'center'}]}>Hi, here is VaJoy</Text> </View> <TouchableOpacity onPress={this._onPress}> <View style={styles.button}> <Text style={styles.buttonText}>Press me!</Text> </View> </TouchableOpacity> <TouchableOpacity> <View style={styles.button}> <Text style={styles.buttonText}>忽略本按钮</Text> </View> </TouchableOpacity> </View> ); } });
效果如下(比开篇的示例流畅多了):
Animated
本文的重点介绍对象,通过 Animated 我们可以在确保性能良好的前提下创造更为灵活丰富且易维护的动画。
不同于上述的动画实现方案,我们得在 Animated.View、Animated.Text 或 Animated.Image 动画组件上运用 Animate 模块的动画能力(如果有在其它组件上的需求,可以使用 Animated.createAnimatedComponent
方法来对其它类型的组件创建动画)。
我们先来个简单的例子:
var React = require('react-native'); var { AppRegistry, StyleSheet, Text, View, Easing, Animated, TouchableOpacity, } = React; var _animateHandler; var AwesomeProject = React.createClass({ componentDidMount() { _animateHandler = Animated.timing(this.state.opacityAnmValue, { toValue: 1, //透明度动画最终值 duration: 3000, //动画时长3000毫秒 easing: Easing.bezier(0.15, 0.73, 0.37, 1.2) //缓动函数 }) }, getInitialState() { return { opacityAnmValue : new Animated.Value(0) //设置透明度动画初始值 } }, _onPress() { _animateHandler.start && _animateHandler.start() } render: function() { return ( <View style={styles.container}> <Animated.View ref="view" style={[styles.content, {width: 200, height: 20, opacity: this.state.opacityAnmValue}]}> <Text style={[{textAlign: 'center'}]}>Hi, here is VaJoy</Text> </Animated.View> <TouchableOpacity onPress={this._onPress}> <View style={styles.button}> <Text style={styles.buttonText}>Press me!</Text> </View> </TouchableOpacity> <TouchableOpacity > <View style={styles.button}> <Text style={styles.buttonText}>忽略本按钮</Text> </View> </TouchableOpacity> </View> ); } }); var styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center' }, content: { justifyContent: 'center', backgroundColor: 'yellow', }, button: { marginTop: 10, paddingVertical: 10, paddingHorizontal: 20, backgroundColor: 'black' }, buttonText: { color: 'white', fontSize: 16, fontWeight: 'bold', } });
点击按钮后,Animated.View 会以bezier曲线形式执行时长3秒的透明度动画(由0到1):
so 我们做了这些事情:
1. 以 new Animated.Value(0) 实例化动画的初始值给state:
getInitialState() { return { opacityAnmValue : new Animated.Value(0) //设置透明度动画初始值 } }
2. 通过 Animated.timing 我们定义了一个动画事件,在后续可以以 .start() 或 .stop() 方法来开始/停止该动画:
componentDidMount() { _animateHandler = Animated.timing(this.state.opacityAnmValue, { toValue: 1, //透明度动画最终值 duration: 3000, //动画时长3000毫秒 easing: Easing.bezier(0.15, 0.73, 0.37, 1.2) }) },
我们在按钮点击事件中触发了动画的 .start 方法让它跑起来:
_onPress() { _animateHandler.start && _animateHandler.start() },
start 方法接受一个回调函数,会在动画结束时触发,并传入一个参数 {finished: true/false},若动画是正常结束的,finished 字段值为true,若动画是因为被调用 .stop() 方法而提前结束的,则 finished 字段值为false。
3. 动画的绑定是在 <Animate.View /> 上的,我们把实例化的动画初始值传入 style 中:
<Animated.View ref="view" style={[styles.content, {width: 200, height: 20, opacity: this.state.opacityAnmValue}]}>
<Text style={[{textAlign: 'center'}]}>Hi, here is VaJoy</Text>
</Animated.View>
然后。。。没有然后了,这实在太简单了
这里需要讲一下的应该是定义动画事件的 Animated.timing(animateValue, conf<Object>) 方法,其中设置参数格式为:
{
duration: 动画持续的时间(单位是毫秒),默认为500。
easing:一个用于定义曲线的渐变函数。阅读Easing模块可以找到许多预定义的函数。iOS默认为Easing.inOut(Easing.ease)。
delay: 在一段时间之后开始动画(单位是毫秒),默认为0。
}
这里提及的 Easing 动画函数模块在 react-native/Libraries/Animated/src/ 目录下,该模块预置了 linear、ease、elastic、bezier 等诸多缓动特性,有兴趣可以去了解。
另外除了 Animated.timing,Animated 还提供了另外两个动画事件创建接口:
1. Animated.spring(animateValue, conf<Object>) —— 基础的单次弹跳物理模型,支持origami标准,conf参考:
{
friction: 控制“弹跳系数”、夸张系数,默认为7。
tension: 控制速度,默认40。
}
代码参考:
var React = require('react-native'); var { AppRegistry, StyleSheet, Text, View, Easing, Animated, TouchableOpacity, } = React; var _animateHandler; var AwesomeProject = React.createClass({ componentDidMount() { this.state.bounceValue.setValue(1.5); // 设置一个较大的初始值 _animateHandler = Animated.spring(this.state.bounceValue, { toValue: 1, friction: 8, tension: 35 }) }, getInitialState() { return { bounceValue : new Animated.Value(0) //设置缩放动画初始值 } }, _onPress() { _animateHandler.start && _animateHandler.start() }, _reload() { AppRegistry.reload() }, render: function() { return ( <View style={styles.container}> <Animated.View ref="view" style={[styles.content, {width: 200, height: 20, transform: [{scale: this.state.bounceValue}]}]}> <Text style={[{textAlign: 'center'}]}>Hi, here is VaJoy</Text> </Animated.View> <TouchableOpacity onPress={this._onPress}> <View style={styles.button}> <Text style={styles.buttonText}>Press me!</Text> </View> </TouchableOpacity> <TouchableOpacity onPress={this._reload}> <View style={styles.button}> <Text style={styles.buttonText}>忽略本按钮</Text> </View> </TouchableOpacity> </View> ); } }); var styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center' }, content: { justifyContent: 'center', backgroundColor: 'yellow', }, button: { marginTop: 10, paddingVertical: 10, paddingHorizontal: 20, backgroundColor: 'black' }, buttonText: { color: 'white', fontSize: 16, fontWeight: 'bold', } });
留意这里我们用了 animateValue.setValue(1.5) 方法来修改动画属性值。效果如下:
2. Animated.decay(animateValue, conf<Object>) —— 以一个初始速度开始并且逐渐减慢停止,conf参考:
{ velocity: 起始速度,必填参数。 deceleration: 速度衰减比例,默认为0.997。 }
代码参考:
var _animateHandler; var AwesomeProject = React.createClass({ componentDidMount() { _animateHandler = Animated.decay(this.state.bounceValue, { toValue: 0.2, velocity: 0.1 }) }, getInitialState() { return { bounceValue : new Animated.Value(0.1) } }, _onPress() { _animateHandler.start && _animateHandler.start() }, render: function() { return ( <View style={styles.container}> <Animated.View ref="view" style={[styles.content, {width: 200, height: 30, transform: [{scale: this.state.bounceValue}]}]}> <Text style={[{textAlign: 'center'}]}>Hi, here is VaJoy</Text> </Animated.View> <TouchableOpacity onPress={this._onPress}> <View style={styles.button}> <Text style={styles.buttonText}>Press me!</Text> </View> </TouchableOpacity> <TouchableOpacity> <View style={styles.button}> <Text style={styles.buttonText}>忽略本按钮</Text> </View> </TouchableOpacity> </View> ); } });
对于最后介绍的两个动画效果,可能得熟悉一些物理、数学模型才能更好地来做控制,大部分情况下,咱们直接使用 Animated.timing 就足够满足需求了。
监听动画
1. 有时候我们需要在动画的过程中监听到某动画时刻的属性值,可以通过 animateValue.stopAnimation(callback<Function>) 或 animateValue.addListener(callback<Function>) 来实现
其中 stopAnimation 会停止当前动画并在回调函数中返回一个 {value : number} 对象,value对应最后一刻的动画属性值:
var _animateHandler, _isFirsPress = 0; var AwesomeProject = React.createClass({ componentDidMount() { _animateHandler = Animated.timing(this.state.opacityAnmValue, { toValue: 1, duration: 6000, easing: Easing.linear }) }, getInitialState() { return { opacityAnmValue : new Animated.Value(0) //设置透明度动画初始值 } }, _onPress() { if(_isFirsPress == 0){ _animateHandler.start && _animateHandler.start(); _isFirsPress = 1 } else { this.state.opacityAnmValue.stopAnimation(value => { Alert.alert( '动画结束,最终值:', JSON.stringify(value), [ {text: 'OK', onPress: () => {}} ] ) }) } }, render: function() { return ( <View style={styles.container}> <Animated.View style={[styles.content, {width: 200, height: 20, opacity: this.state.opacityAnmValue}]}> <Text style={[{textAlign: 'center'}]}>Hi, here is VaJoy</Text> </Animated.View> <TouchableOpacity onPress={this._onPress}> <View style={styles.button}> <Text style={styles.buttonText}>Press me!</Text> </View> </TouchableOpacity> <TouchableOpacity > <View style={styles.button}> <Text style={styles.buttonText}>忽略本按钮</Text> </View> </TouchableOpacity> </View> ); } });
而 addListener 方法会在动画的执行过程中持续异步调用callback回调函数,提供一个最近的值作为参数。
2. 有时候我们希望在某个交互事件(特别是手势)中更灵活地捕获某个事件对象属性值,并动态赋予某个变量,对于这种需求可以通过 Animated.event 来实现。
它接受一个数组为参数,数组中的层次对应绑定事件参数的相应映射,听着有点绕,看例子就很好理解了:
var scrollX = 0, pan = { x: 0, y: 0 }; //... onScroll : Animated.event( [{nativeEvent: {contentOffset: {x: scrollX}}}] // scrollX = e.nativeEvent.contentOffset.x ) onPanResponderMove : Animated.event([ null, // 忽略原生事件 {dx: pan.x, dy: pan.y} // 从gestureState中解析出dx和dy的值 ]);
onScroll 是绑定给某个组件的滚动事件,而 onPanResponderMove 是 PanResponder 模块下的响应事件。
拿上方 onPanResponderMove 的例子来讲,该事件方法接收两个参数 e<event Object> 和 gestureState<Object>,其中 gestureState 的属性有:
stateID - 触摸状态的ID。在屏幕上有至少一个触摸点的情况下,这个ID会一直有效。 moveX - 最近一次移动时的屏幕横坐标 moveY - 最近一次移动时的屏幕纵坐标 x0 - 当响应器产生时的屏幕坐标 y0 - 当响应器产生时的屏幕坐标 dx - 从触摸操作开始时的累计横向路程 dy - 从触摸操作开始时的累计纵向路程 vx - 当前的横向移动速度 vy - 当前的纵向移动速度 numberActiveTouches - 当前在屏幕上的有效触摸点的数量
此处不了解的可以去看我上一篇RN入门文章的相关介绍。
而上方例子中,我们动态地将 gestureState.dx 和 gestureState.dy 的值赋予 pan.x 和 pan.y。
来个有简单的例子:
class AwesomeProject extends Component { constructor(props) { super(props); this.state = { transY : new Animated.Value(0) }; this._panResponder = {} } componentWillMount处预先创建手势响应器 componentWillMount() { this._panResponder = PanResponder.create({ onStartShouldSetPanResponder: this._returnTrue.bind(this), onMoveShouldSetPanResponder: this._returnTrue.bind(this), //手势开始处理 //手势移动时的处理 onPanResponderMove: Animated.event([null, { dy : this.state.transY }]) }); } _returnTrue(e, gestureState) { return true; } render() { return ( <View style={styles.container}> <Animated.View style={[styles.content, {width: this.state.w, height: this.state.h, transform: [{ translateY : this.state.transY }] }]}> <Text style={[{textAlign: 'center'}]}>Hi, here is VaJoy</Text> </Animated.View> <TouchableOpacity> <View style={styles.button} {...this._panResponder.panHandlers}> <Text style={styles.buttonText}>control</Text> </View> </TouchableOpacity> <TouchableOpacity> <View style={styles.button}> <Text style={styles.buttonText}>忽略此按钮</Text> </View> </TouchableOpacity> </View> ); } }
动画的API较多,本章先介绍到这里,下篇将介绍更复杂的动画实现~ 共勉~