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较多,本章先介绍到这里,下篇将介绍更复杂的动画实现~ 共勉~

ReactNative入门 —— 动画篇(上)的更多相关文章
- ReactNative入门 —— 动画篇(下)
在上篇动画入门文章中我们了解了在 React Native 中简单的动画的实现方式,本篇将作为上篇的延续,介绍如何使用 Animated 实现一些比较复杂的动画. 动画组合 在 Animated 中提 ...
- jQuery-4.动画篇---上卷下拉效果
jQuery中下拉动画slideDown 对于隐藏的元素,在将其显示出来的过程中,可以对其进行一些变化的动画效果.之前学过了show方法,show方法在显示的过程中也可以有动画,但是.show()方法 ...
- ReactNative入门(安卓)——API(上)
Alert - 弹窗 通过 Alert.alert() 方法调用唤起原生弹窗,点击会触发 onPress 回调(参考下方代码)并清除弹窗. import React, { AppRegistry, C ...
- Apache Maven 入门篇 ( 上 )
作者:George Ma 写这个 maven 的入门篇是因为之前在一个开发者会的动手实验中发现挺多人对于 maven 不是那么了解,所以就有了这个想法. 这个入门篇分上下两篇.本文着重动手,用 mav ...
- React-Native入门指导之iOS篇 —— 一、准备工作
React-Native 入门指导系列教程目录 一.准备工作 (已完成) 二.项目介绍与调试 三.CSS样式与Flex布局 四.常用UI控件的使用 五.JSX在React-Native中的应用 六.事 ...
- [转]Apache Maven 入门篇 ( 上 )
原文地址:Apache Maven 入门篇 ( 上 ) 作者:George Ma 写这个 maven 的入门篇是因为之前在一个开发者会的动手实验中发现挺多人对于 maven 不是那么了解,所以就有了这 ...
- React-Native入门指导之iOS篇
React-Native 入门指导系列教程目录 一.准备工作 (已完成) 二.项目介绍与调试 三.CSS样式与Flex布局 四.常用UI控件的使用 五.JSX在React-Native中的应用 六.事 ...
- iOS React-Native入门指南之HelloWorld
React-native 作为facebook开源项目,最近是火的一塌糊涂,它采用node.js能够写ios和android的native界面代码,简直是太酷了.支持动态更新,而且appstore 提 ...
- [转] ReactNative Animated动画详解
http://web.jobbole.com/84962/ 首页 所有文章 JavaScript HTML5 CSS 基础技术 前端职场 工具资源 更多频道▼ - 导航条 - 首页 所有文章 ...
随机推荐
- Hangfire项目实践分享
Hangfire项目实践分享 目录 Hangfire项目实践分享 目录 什么是Hangfire Hangfire基础 基于队列的任务处理(Fire-and-forget jobs) 延迟任务执行(De ...
- Mybatis批量删除
<delete id="deleteByStandardIds"> delete from t_standard_catalog where standard_id i ...
- android 事件分发机制详解(OnTouchListener,OnClick)
昨天做东西做到触摸事件冲突,以前也经常碰到事件冲突,想到要研究一下Android的事件冲突机制,于是从昨天开始到今天整整一天时间都要了解这方面的知识,这才懂了安卓的触摸和点击事件的机制.探究如下: 首 ...
- 使用HTML5的cavas实现的一个画板
<!DOCTYPE html><html><head> <meta charset="utf-8"> <meta http-e ...
- 微信小程序的机会在于重新理解群组与二维码
历时一年,唯一一个尚未发布就获得Pony Ma与Allen Zhang站台的产品:微信小程序,将于2017年1月9日正式上线了.我很期待.唯一要警惕的是:防止长考出臭棋. 在上线前夕,我对于如何借助小 ...
- Android AndroidRuntime类
AndroidRuntime类是安卓底层很重要的一个类,它负责启动虚拟机以及Java线程,AndroidRuntime类在一个进程中只有一个实例对象保存在全局变量,gCurRuntime中.
- Linux命令【第三篇】
执行下面命令时发现提示需要输入密码,请问提示输入的密码是哪个用户的密码. [test@oldboy ~]$ sudo su - oldboy 解答: 输入当前执行命令test账户的密码. 相关说明: ...
- phpexcel读取输出操作
//读取 <?php header("Content-Type:text/html;charset=utf-8"); include 'Classes/PHPExcel.ph ...
- linux字符串url编码与解码
编码的两种方式 echo '手机' | tr -d '\n' | xxd -plain | sed 's/\(..\)/%\1/g' echo '手机' |tr -d '\n' |od -An -tx ...
- 【腾讯bugly干货分享】微信Android热补丁实践演进之路
本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://bugly.qq.com/bbs/forum.php?mod=viewthread&tid=1264& ...