学习和实践 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 style={[styles.line,{height:this.state.sV}]}>
- <Animated.View style={[styles.line,{height:this.state.tV}]}>
- <Animated.View style={[styles.line,{height:this.state.foV}]}>
- </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 中的关键帧动画它提供了 springdecaytiming 三种动画方式, 其实这也就是三种不同的差值方式, 指定相同的初始值和结束值, 它们会以不同的函数计算中间值并运用到动画中, 最终输出的就是三种不同的动画, 比如官方给出的示例:
- classPlayground extendsReact.Component{
- constructor(props: any){
- super(props);
- this.state = {
- bounceValue: newAnimated.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(){
- vart0=animationT,t1=t0+0.5,t2=t1+0.5,t3=t2+timeDelay,t4=t3+0.5;// 这里分别是四个动画的当前时间, 依次加上了 0.5 的延迟
- varv1=Number(Math.cos(t0).toFixed(2))*animationN+animationM;// 将 cos 函数的小数值只精确到小数点 2 位, 提高运算效率
- varv2=Number(Math.cos(t1).toFixed(2))*animationN+animationM;
- varv3=Number(Math.cos(t2).toFixed(2))*animationN+animationM;
- varv4=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 应用中, 还是需要结合实际情况不断优化
这个动画应该还有更好更便捷的实现方式, 这里抛砖引玉, 希望大家能够在此基础上探索出性能更好的实现方式并分享出来
来源: http://lib.csdn.net/article/react/36996