学习和实践react已经有一段时间了,在经历了从最初的彷徨到解决痛点时的兴奋,再到不断实践后遭遇问题时的苦闷,确实被这一种新的思维方式和开发模式所折服,react不是万能的,在很多场景下滥用反而会适得其反,这里不展开讨论。
有了react的实践经验,结合之前自己的一点ios开发经验,决定继续冒险,开始react-native学习和实践,目前主要是从常规的native功能入手,逐步用react-native实现,基础知识如开发环境搭建、调试工具等官方文档有很清楚的指引,不再赘述,这里主要是想把实际学习实践中遇到的坑或者有意思的经历记录下来,为广大react-native初学者提供一点参考。O(∩_∩)O~
话不多说,进入正题,今天要实现的是一个加载动画,效果如下:
很简单一个动画,不是么?用native实现实在是小菜一碟,现在我们试着用RN来实现它!
先将动画的视图结构搭建出来,这个比较简单,就是4个会变形的View顺序排列:
<View style={styles.square}>
<Animated.View style={[styles.line,{height:this.state.fV}]}></Animated.View>
<Animated.View style={[styles.line,{height:this.state.sV}]}></Animated.View>
<Animated.View style={[styles.line,{height:this.state.tV}]}></Animated.View>
<Animated.View style={[styles.line,{height:this.state.foV}]}></Animated.View>
</View>
这里的视图结构很普通,只不过在RN中,需要施加动画的视图,都不能是普通的View,而是Animated.View,包括施加动画的图片,也应该是Animated.Image,需要注意。
RN继承了react的核心思想,基于虚拟DOM和数据驱动的模式,用state来管理视图层,所以RN的动画和react的动画类似,都是通过改变state从而执行render进行视图重绘,展现动画。
毫无疑问,先从Animated库下手,这是facebook官方提供的专门用于实现动画的库,它比较强大,集成了多种常见的动画形式,正如官方文档写道:
Animated focuses on declarative relationships between inputs and outputs, with configurable transforms in between, and simple start/stop methods to control time-based animation execution.
它专注于输入和输出之间的对应关系,其间是可以配置的各种变形,通过简单的开始和停止方法来控制基于时间的动画。
所以使用这个库的时候,需要清楚知道动画的输入值,不过这并不代表需要知道每一个时刻动画的精确属性值,因为这是一种插值动画,Animated只需要知道初始值和结束值,它会将所有中间值动态计算出来运用到动画中,这有点类似于CSS3中的关键帧动画。它提供了spring、decay、timing三种动画方式,其实这也就是三种不同的差值方式,指定相同的初始值和结束值,它们会以不同的函数计算中间值并运用到动画中,最终输出的就是三种不同的动画,比如官方给出的示例:
class Playground extends React.Component {
constructor(props: any) {
super(props);
this.state = {
bounceValue: new Animated.Value(0),//这里设定了动画的输入初始值,注意不是数字0
};
}
render(): ReactElement {
return (
<Animated.Image //这里不是普通Image组件
source={{uri: 'http://i.imgur.com/XMKOH81.jpg'}}
style={{
flex: 1,
transform: [ //添加变换,transform的值是数组,包含一系列施加到对象上的变换
{scale: this.state.bounceValue}, // 变换是缩放,缩放值state里的bounceValue,这个值是一个动态值,也是动画的根源
]
}}
/>
);
}
componentDidMount() {
this.state.bounceValue.setValue(1.5); // 组件加载的时候设定bounceValue,因此图片会被放大1.5倍
Animated.spring( //这里运用的spring方法,它的差值方式不是线性的,会呈现弹性的效果
this.state.bounceValue, //spring方法的第一个参数,表示被动态插值的变量
{
toValue: 0.8, //这里就是输入值的结束值
friction: 1, //这里是spring方法接受的特定参数,表示弹性系数
}
).start();// 开始spring动画
}
}
可以想象该动画效果大致为:图片首先被放大1.5倍呈现出来,然后以弹性方式缩小到0.8倍。这里的start方法还可以接收一个参数,参数是一个回调函数,在动画正常执行完毕之后,会调用这个回调函数。
Animated库不仅有spring/decay/timing三个方法提供三种动画,还有sequence/decay/parallel等方法来控制动画队列的执行方式,比如多个动画顺序执行或者同时进行等。
介绍完了基础知识,我们开始探索这个实际动画的开发,这个动画需要动态插值的属性其实很简单,只有四个视图的高度值,其次,也不需要特殊的弹性或者缓动效果。所以我们只需要将每个视图的高度依次变化,就可以了,so easy!
开始尝试:
Animated.timing(
this.state.fV,
{
toValue: 100,
duration:500,
delay:500,
}
).start();
Animated.timing(
this.state.sV,
{
toValue: 100,
duration:1000,
delay:1000,
}
).start();
Animated.timing(
this.state.tV,
{
toValue: 100,
duration:1000,
delay:1500,
}
).start();
Animated.timing(
this.state.foV,
{
toValue: 100,
duration:1000,
delay:2000,
}
).start();
WTF!
虽然动画动起来了,但是这根本就是四根火柴在做广播体操。。。
并且一个更严重的问题是,动画运行完,就停止了。。。,而loading动画应该是循环的,在查阅了文档及Animated源码之后,没有找到类似loop这种控制循环的属性,无奈之下,只能另辟蹊径了。
上文提到过,Animated动画的start方法可以在动画完成之后执行回调函数,如果动画执行完毕之后再执行自己,就实现了循环,因此,将动画封装成函数,然后循环调用本身就可以了,不过目前动画还只把高度变矮了,没有重新变高的部分,因此即使循环也不会有效果,动画部分也需要修正:
...//其他部分代码
loopAnimation(){
Animated.parallel([//最外层是一个并行动画,四个视图的动画以不同延迟并行运行
Animated.sequence([//这里是一个顺序动画,针对每个视图有两个动画:缩小和还原,他们依次进行
Animated.timing(//这里是缩小动画
this.state.fV,
{
toValue: Utils.getRealSize(100),
duration:500,
delay:0,
}
),
Animated.timing(//这里是还原动画
this.state.fV,
{
toValue: Utils.getRealSize(200),
duration:500,
delay:500,//注意这里的delay刚好等于duration,也就是缩小之后,就开始还原
}
)
]),
...//后面三个数值的动画类似,依次加大delay就可以
]).start(this.loopAnimation2.bind(this));
}
...
效果粗来了!
怎么说呢,动画是粗来了,基本实现了循环动画,但是总觉得缺少那么点灵(sao)动(qi),仔细分析会发现,这是因为我们的循环的实现是通过执行回调来实现的,当parallel执行完毕之后,会执行回调进行第二次动画,也就是说parallel不执行完毕,第二遍是不会开始的,这就是为什么动画会略显僵硬,因此仔细观察,第一个条块在执行完自己的缩小放大动画后,只有在等到第四个条也完成缩小放大动画,整个并行队列才算执行完,回调才会被执行,第二遍动画才开始。
So,回调能被提前执行吗?
Nooooooooooooooooooooop!
多么感人,眼角貌似有翔滑过。。。。。
但是,不哭站撸的程序猿是不会轻易折服的,在多次查阅Animated文档之后,无果,累觉不爱(或许我们并不合适)~~~
好在facebook还提供了另一个更基础的requestAnimationFrame函数,熟悉canvas动画的同学对它应该不陌生,这是一个动画重绘中经常遇到的方法,动画的最基本原理就是重绘,通过在每次绘制的时候改变元素的位置或者其他属性使得元素在肉眼看起来动起来了,因此,在碰壁之后,我们尝试用它来实现我们的动画。
其实,用requestAnimationFrame来实现动画,就相当于需要我们自己来做插值,通过特定方式动态计算出中间值,将这些中间值赋值给元素的高度,就实现了动画。
这四个动画是完全相同的,只是以一定延迟顺序进行的,因此分解之后只要实现一个就可以了,每个动画就是条块的高度随时间呈现规律变化:
大概就介么个意思。这是一个分段函数,弄起来比较复杂,我们可以将其近似成相当接近的连续函数--余弦函数,这样就相当轻松了:
let animationT=0;//定义一个全局变量来标示动画时间
let animationN=50,//余弦函数的极值倍数,即最大偏移值范围为正负50
animationM=150;//余弦函数偏移值,使得极值在100-200之间
componentDidMount(){
animationT=0;
requestAnimationFrame(this.loopAnimation.bind(this));//组件加载之后就执行loopAnimation动画
}
loopAnimation(){
var t0=animationT,t1=t0+0.5,t2=t1+0.5,t3=t2+timeDelay,t4=t3+0.5;//这里分别是四个动画的当前时间,依次加上了0.5的延迟
var v1=Number(Math.cos(t0).toFixed(2))*animationN+animationM;//将cos函数的小数值只精确到小数点2位,提高运算效率
var v2=Number(Math.cos(t1).toFixed(2))*animationN+animationM;
var v3=Number(Math.cos(t2).toFixed(2))*animationN+animationM;
var v4=Number(Math.cos(t3).toFixed(2))*animationN+animationM;
this.setState({
fV:v1,
sV:v2,
tV:v3,
foV:v4
});
animationT+=0.35;//增加时间值,每次增值越大动画越快
requestAnimationFrame(this.loopAnimation.bind(this));
}
最终效果:
可以看出,相当灵(sao)动(qi),由此也可以一窥RN的性能,我们知道,RN中的JS是运行在JavaScriptCore环境中的,对大多数React Native应用来说,业务逻辑是运行在JavaScript线程上的。这是React应用所在的线程,也是发生API调用,以及处理触摸事件等操作的线程。更新数据到原生支持的视图是批量进行的,并且在事件循环每进行一次的时候被发送到原生端,这一步通常会在一帧时间结束之前处理完(如果一切顺利的话)。可以看出,我们在每一帧都进行了运算并改变了state,这是在JavaScript线程上进行的,然后通过RN推送到native端实时渲染每一帧,说实话,最开始对动画的性能还是比较担忧的,现在看来还算不错,不过这只是一个很简单的动画,需要绘制的东西很少,在实际app应用中,还是需要结合实际情况不断优化。
这个动画应该还有更好更便捷的实现方式,这里抛砖引玉,希望大家能够在此基础上探索出性能更好的实现方式并分享出来。
好了,这次动画初探就到这里,随着学习和实践的深入,还会陆续推出一系列分享,敬请关注。