xml地图|网站地图|网站标签 [设为首页] [加入收藏]

Native学习实践,ReactNative学习实践

React Native学习实践:动画初探之加载动画

2016/04/29 · JavaScript · 1 评论 · React, React Native, 动画

本文作者: 伯乐在线 - D.son 。未经作者许可,禁止转载!
欢迎加入伯乐在线 专栏作者。

学习和实践react已经有一段时间了,在经历了从最初的彷徨到解决痛点时的兴奋,再到不断实践后遭遇问题时的苦闷,确实被这一种新的思维方式和开发模式所折服,但react也不是万能的,在很多场景下滥用反而会适得其反,这里不展开讨论。

有了react的实践经验,结合之前自己的一点ios开发经验,决定继续冒险,开始react-native学习和实践,目前主要是从常规的native功能入手,逐步用react-native实现,基础知识如开发环境搭建、调试工具等官方文档有很清楚的指引,不再赘述,这里主要是想把实际学习实践中遇到的坑或者有意思的经历记录下来,为广大react-native初学者提供一点参考。O(∩_∩)O~

话不多说,进入正题,今天要实现的是一个加载动画,效果如下:

betway 1

很简单一个动画,不是么?用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>

1
2
3
4
5
6
<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中的关键帧动画。它提供了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: ' 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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();

betway 2
WTF!
betway 3
虽然动画动起来了,但是这根本就是四根火柴在做广播体操。。。

并且一个更严重的问题是,动画运行完,就停止了。。。,而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)); } ...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
...//其他部分代码
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));
}
...

效果粗来了!

betway 4
怎么说呢betway 5,

动画是粗来了,基本实现了循环动画,但是总觉得缺少那么点灵(sao)动(qi),仔细分析会发现,这是因为我们的循环的实现是通过执行回调来实现的,当parallel执行完毕之后,会执行回调进行第二次动画,也就是说parallel不执行完毕,第二遍是不会开始的,这就是为什么动画会略显僵硬,因此仔细观察,第一个条块在执行完自己的缩小放大动画后,只有在等到第四个条也完成缩小放大动画,整个并行队列才算执行完,回调才会被执行,第二遍动画才开始。

So,回调能被提前执行吗?
Nooooooooooooooooooooop!

多么感人,眼角貌似有翔滑过。。。。。

但是,不哭站撸的程序猿是不会轻易折服的,在多次查阅Animated文档之后,无果,累觉不爱(或许我们并不合适)~~~
好在facebook还提供了另一个更基础的requestAnimationFrame函数,熟悉canvas动画的同学对它应该不陌生,这是一个动画重绘中经常遇到的方法,动画的最基本原理就是重绘,通过在每次绘制的时候改变元素的位置或者其他属性使得元素在肉眼看起来动起来了,因此,在碰壁之后,我们尝试用它来实现我们的动画。

其实,用requestAnimationFrame来实现动画,就相当于需要我们自己来做插值,通过特定方式动态计算出中间值,将这些中间值赋值给元素的高度,就实现了动画。

这四个动画是完全相同的,只是以一定延迟顺序进行的,因此分解之后只要实现一个就可以了,每个动画就是条块的高度随时间呈现规律变化:
betway 6

大概就介么个意思。这是一个分段函数,弄起来比较复杂,我们可以将其近似成相当接近的连续函数–余弦函数,这样就相当轻松了:

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)); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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));
  }

最终效果:
betway 7

可以看出,相当灵(sao)动(qi),由此也可以一窥RN的性能,我们知道,RN中的JS是运行在JavaScriptCore环境中的,对大多数React Native应用来说,业务逻辑是运行在JavaScript线程上的。这是React应用所在的线程,也是发生API调用,以及处理触摸事件等操作的线程。更新数据到原生支持的视图是批量进行的,并且在事件循环每进行一次的时候被发送到原生端,这一步通常会在一帧时间结束之前处理完(如果一切顺利的话)。可以看出,我们在每一帧都进行了运算并改变了state,这是在JavaScript线程上进行的,然后通过RN推送到native端实时渲染每一帧,说实话,最开始对动画的性能还是比较担忧的,现在看来还算不错,不过这只是一个很简单的动画,需要绘制的东西很少,在实际app应用中,还是需要结合实际情况不断优化。

这个动画应该还有更好更便捷的实现方式,这里抛砖引玉,希望大家能够在此基础上探索出性能更好的实现方式并分享出来。

好了,这次动画初探就到这里,随着学习和实践的深入,还会陆续推出一系列分享,敬请关注。

打赏支持我写出更多好文章,谢谢!

打赏作者

原文链接:
摘要: 在移动开发中,动画是提高用户体验不可缺少的一个元素。在React Native中,动画API提供了一些现成的组件:Animated.View,Animated.Text和Animated.Image默认支持动画。

在我看来,无论是用 OC还是 RN 动画无疑都是我的痛点。但是在开发3.13.0版本的时候,有一个界面必须要使用动画,并且这个界面还要用RN开发,一开始我的拒绝的,但是为了产品交互,给用户带来更好的体验,只能硬着头皮上了。其实该动画说难也不难,如果用OC就是分分钟钟的事,但是关于RN动画方面之前根本没有接触过,所以就没怎么研究。因此我花了两天才把这个动画搞出来,也是给自己点个赞。

ReactNative学习实践:Navigator实践

2016/06/17 · JavaScript · HTML5, Javascript, React, ReactNative

本文作者: 伯乐在线 - D.son 。未经作者许可,禁止转载!
欢迎加入伯乐在线 专栏作者。

离上次写RN笔记有一段时间了,期间参与了一个新项目,只在最近的空余时间继续学习实践,因此进度比较缓慢,不过这并不代表没有新进展,其实这个小东西离上次发文时已经有了相当大的变化了,其中影响最大的变化就是引入了Redux,后面会系统介绍一下。
在开始主题之前,先补充一点上回说到的动画初探(像我这么靠谱严谨的攻城狮,必须精益求精,┗|`O′|┛ 嗷~~)。

上回文说到,经过我们自己定义了余弦动画函数之后,动态设定state的4个参数,实现了比较流畅的加载动画,这里可能有朋友已经注意到了,我们非常频繁的调用了setState方法,这在React和RN中都是相当忌讳的,每一次setState都会触发render方法,也就意味着更频繁的虚拟DOM对比,特别是在RN中,这还意味着更频繁的JSCore<==>iOS通信,尽管框架本身对多次setState做了优化,比如会合并同时调用的多个setState,但这对性能和体验还是会有较大影响。

上回我们只是单独实现了一个loading动画,所以还比较流畅,当视图中元素较多并且有各自的动画的时候,就会看到比较严重的卡顿,这些其实是可以避免的,因为在loading动画的实现部分,我们清楚地知道只需要loading动画的特定组成部分更新而不是组件的所有部分以及继承链上的所有组件都需要更新,并且确信这个节点一定发生了变化,因此不需要经过虚拟DOM对比,那么如果我们能绕开setState,动画就应该会更流畅,即使在复杂的视图里边。这就是Animations文档最后提到的setNativeProps方法。

As mentioned in the Direction Manipulation section, setNativeProps allows us to modify properties of native-backed components (components that are actually backed by native views, unlike composite components) directly, without having to setState and re-render the component hierarchy.

setNativeProps允许我们直接操纵原生组件的属性,而不需要用到setState,也不会重绘继承链上的其他组件。这正是我们想要的效果,加上我们明确知道正在操纵的组件以及它与视图其他组件的关系,因此,这里我们可以放心地使用它,而且相当简单。
更新前:

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)); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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));
  }

更新后:

loopAnimation(){ var t0=··· var v1=··· var v2=··· var v3=··· var v4=··· this.refs.line1.setNativeProps({ style:{width:w1,height:v1} }); this.refs.line2.setNativeProps({ style:{width:w2,height:v2} }); this.refs.line3.setNativeProps({ style:{width:w3,height:v3} }); this.refs.line4.setNativeProps({ style:{width:w4,height:v4} }); animationT+=0.35;//增加时间值,每次增值越大动画越快 requestAnimationFrame(this.loopAnimation.bind(this)); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
loopAnimation(){
    var t0=···
    var v1=···
    var v2=···
    var v3=···
    var v4=···
    this.refs.line1.setNativeProps({
      style:{width:w1,height:v1}
    });
    this.refs.line2.setNativeProps({
      style:{width:w2,height:v2}
    });
    this.refs.line3.setNativeProps({
      style:{width:w3,height:v3}
    });
    this.refs.line4.setNativeProps({
      style:{width:w4,height:v4}
    });
    animationT+=0.35;//增加时间值,每次增值越大动画越快
    requestAnimationFrame(this.loopAnimation.bind(this));
  }

效果如下:
betway 8
这里有意在注册请求完毕之后没有隐藏loading动画,因此同时执行了视图切换和loading两个动画,效果还行~

好了,该进入今天的正题了。先整体看一下这一阶段实现的效果(哒哒哒~):
betway 9

主要是模拟了一个新用户注册流程,实现起来也并不复杂,整体结构是用一个RN组件Navigator来做导航,虽然有另一个NavigatorIOS组件在iOS系统上表现更加优异,但是考虑到RN本身希望能够同时在安卓和iOS上运行的初衷,我选择了可以兼容两个平台的Navigator来尝试,目前来看效果还能接受。
在最后的详细信息视图里边,尝试了各种组件,比如调用相机,Switch,Slider等,主要是尝鲜,哈哈~ 也自己实现了比较简单的check按钮。
首先最外层的结构是一个Navigator,它控制整个用户注册的视图切换:

<Navigator style={styles.navWrap} initialRoute={{name: 'login', component:LoginView}} configureScene={(route) => { return Navigator.SceneConfigs.FloatFromRight; }} renderScene={(route, navigator) => { let Component = route.component; return <Component {...route.params} navigator={navigator} /> }} />

1
2
3
4
5
6
7
8
9
<Navigator style={styles.navWrap}
          initialRoute={{name: 'login', component:LoginView}}
          configureScene={(route) => {
            return Navigator.SceneConfigs.FloatFromRight;
          }}
          renderScene={(route, navigator) => {
            let Component = route.component;
            return <Component {...route.params} navigator={navigator} />
          }} />

其中,initialRoute配置了Navigator的初始组件,这里就是LoginView组件,它本身既可以直接登录,也可以点击【我要注册】进入注册流程。configureScene属性则是用来配置Navigator中视图切换的动画类型,这里可以灵活配置切换方式:

Navigator.SceneConfigs.PushFromRight (default) Navigator.SceneConfigs.FloatFromRight Navigator.SceneConfigs.FloatFromLeft Navigator.SceneConfigs.FloatFromBottom Navigator.SceneConfigs.FloatFromBottomAndroid Navigator.SceneConfigs.FadeAndroid Navigator.SceneConfigs.HorizontalSwipeJump Navigator.SceneConfigs.HorizontalSwipeJumpFromRight Navigator.SceneConfigs.VerticalUpSwipeJump Navigator.SceneConfigs.VerticalDownSwipeJump

1
2
3
4
5
6
7
8
9
10
Navigator.SceneConfigs.PushFromRight (default)
Navigator.SceneConfigs.FloatFromRight
Navigator.SceneConfigs.FloatFromLeft
Navigator.SceneConfigs.FloatFromBottom
Navigator.SceneConfigs.FloatFromBottomAndroid
Navigator.SceneConfigs.FadeAndroid
Navigator.SceneConfigs.HorizontalSwipeJump
Navigator.SceneConfigs.HorizontalSwipeJumpFromRight
Navigator.SceneConfigs.VerticalUpSwipeJump
Navigator.SceneConfigs.VerticalDownSwipeJump

renderScene属性则是必须配置的一个属性,它负责渲染给定路由对应的组件,也就是向Navigator所有路由对应的组件传递了”navigator”属性以及route本身携带的参数,如果不使用类似Flux或者Redux来全局存储或控制state的话,那么Navigator里数据的传递就全靠”route.params”了,比如用户注册流程中,首先是选择角色视图,然后进入注册视图填写账号密码短信码等,此时点击注册才会将所有数据发送给服务器,因此从角色选择视图到注册视图,需要将用户选择的角色传递下去,在注册视图发送给服务器。因此,角色选择视图的跳转事件需要把参数传递下去:

class CharacterView extends Component { constructor(props){ super(props); this.state={ character:"type_one" } } handleNavBack(){ this.props.navigator.pop(); } ··· handleConfirm(){ this.props.navigator.push({ name:"registerNav", component:RegisterNavView, params:{character:this.state.character} }); } render(){ return ( <View style={styles.container}> <TopBarView title="注册" hasBackArr={true} onBackPress={this.handleNavBack.bind(this)}/> ··· (this)}> 确认> </TouchableOpacity> > </View> ); } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class CharacterView extends Component {
  constructor(props){
    super(props);
    this.state={
        character:"type_one"
    }
  }
 
  handleNavBack(){
    this.props.navigator.pop();
  }
  
  ···
  
  handleConfirm(){
    this.props.navigator.push({
      name:"registerNav",
      component:RegisterNavView,
      params:{character:this.state.character}
    });
  }
 
  render(){
    return (
      <View style={styles.container}>
        <TopBarView title="注册" hasBackArr={true} onBackPress={this.handleNavBack.bind(this)}/>
        
          
          ···
          
          (this)}>
            确认>
          </TouchableOpacity>
        >
      </View>
    );
  }
}

这是角色选择视图CharacterView的部分代码,由于Navigator并没有像NavigatorIOS那样提供可配置的顶栏、返回按钮,所以我把顶栏做成了一个克配置的公共组件TopBarView,Navigator里边的所有视图直接使用就可以了,点击TopBarView的返回按钮时,TopBarView会调用给它配置的onBackPress回调函数,这里onBackPress回调函数是CharacterView的handleNavBack方法,即执行了:

this.props.navigator.pop();

1
this.props.navigator.pop();

关于this.props.navigator,这里我们并没有在导航链上的每个组件显式地传递navigator属性,而是在Navigator初始化的时候就在renderScene属性方法里统一配置了,导航链上所有组件的this.props.navigator其实都指向了一个统一的navigator对象,它有两个方法:push和pop,用来向导航链压入和推出组件,视觉上就是进入下一视图和返回上一视图,因此这里当点击顶栏返回按钮时,直接调用pop方法就返回上一视图了。其实也可以把navigator对象传递到TopBarView里,在TopBarView内部调用navigator的pop方法,原理是一样的。而在CharacterView的确认按钮事件里,需要保存用户选择的角色然后跳转到下一个视图,就是通过props传递的:

this.props.navigator.push({ name:"registerNav", component:RegisterNavView, params:{character:this.state.character} });

1
2
3
4
5
this.props.navigator.push({
      name:"registerNav",
      component:RegisterNavView,
      params:{character:this.state.character}
    });

这里就是调用的navigator属性的push方法向导航链压入新的组件,即进入下一视图。push方法接收的参数是一个包含三个属性的对象,name只是用来标识组件名称,而commponent和params则是标识组件以及传递给该组件的参数对象,这里的”commponent”和”params”两个key名称是和前面renderScene是对应的,在renderScene回调里边,用到的route.commponent和route.params,就是这里push传递的参数对应的值。
在用户注册视图中,有一个用户协议需要用户确认,这里用户协议视图的切换方式与主流程不太一样,而一个Navigator只能在最初配置一种切换方式,因此,这里在Navigator里嵌套了Navigator,效果如下:
betway 10
CharacterView的跳转事件中,向navigator的push传递的组件并不是RegisterView组件,而是传递的RegisterNavView组件,它是被嵌套的一个Navigator,这个子导航链上包含了用户注册视图及用户协议视图。

class RegisterNavView extends Component { constructor(props){ super(props); } handleConfirm(){ //send data to server ··· // this.props.navigator.push({ component:nextView, name:'userInfo' }); } render(){ return ( <View style={styles.container}> <Navigator style={styles.navWrap} initialRoute={{name: 'register', component:RegisterView,params:{navigator:this.props.navigator,onConfirm:this.handleConfirm.bind(this)}}} configureScene={(route) => { return Navigator.SceneConfigs.FloatFromBottom; }} renderScene={(route, navigator) => { let Component = route.component; return <Component {...route.params} innerNavigator={navigator} /> }} /> </View> ); } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class RegisterNavView extends Component {
  constructor(props){
    super(props);
  }
 
  handleConfirm(){
    //send data to server
    ···
    //
    this.props.navigator.push({
        component:nextView,
        name:'userInfo'
      });
  }
 
  render(){
    return (
      <View style={styles.container}>
        <Navigator style={styles.navWrap}
          initialRoute={{name: 'register', component:RegisterView,params:{navigator:this.props.navigator,onConfirm:this.handleConfirm.bind(this)}}}
          configureScene={(route) => {
            return Navigator.SceneConfigs.FloatFromBottom;
          }}
          renderScene={(route, navigator) => {
            let Component = route.component;
            return <Component {...route.params} innerNavigator={navigator} />
          }} />
      </View>
    );
  }
}

这个被嵌套的导航我们暂且称为InnerNav,它的初始路由组件就是RegisterView,展示了输入账号密码等信息的视图,它的configureScene设置为“FloatFromBottom”,即从底部浮上来,renderScene也略微不一样,在InnerNav导航链组件上传递的navigator对象名称改成了innerNavigator,以区别主流程Navigator,在RegisterView中有一个【用户协议】的文字按钮,在这个按钮上我们调用了向InnerNav压入协议视图的方法:

handleShowUserdoc(){ this.props.innerNavigator.push({ name:"usrdoc", component:RegisterUsrDocView }); }

1
2
3
4
5
6
handleShowUserdoc(){
    this.props.innerNavigator.push({
      name:"usrdoc",
      component:RegisterUsrDocView
    });
  }

而在RegisterUsrDocView即用户协议视图组件中,点击确定按钮时我们调用了从InnerNav推出视图的方法:

handleHideUserdoc(){ this.props.innerNavigator.pop(); }

1
2
3
handleHideUserdoc(){
    this.props.innerNavigator.pop();
}

这样内嵌的导航链上的视图就完成了压入和推出的完整功能,如果有需要,还可以添加更多组件。
在RegisterNavView组件的handleConfirm方法中,也就是点击注册之后调用的方法,此时向服务器发送数据并且需要进入注册的下一环节了,因此需要主流程的Navigator压入新的视图,所以调用的是this.props.navigator.push,而不是innderNavigator的方法。

好了,大概结构和流程就介绍到这里了,相对比较简单,实际开发中还是会遇到很多细节问题,比如整个注册流程中,数据都需要存储在本地,最后统一提交到服务器,如果导航链上有很多组件,那么数据就要一级一级以props的方式传递,非常蛋疼,因此才引入了Redux来统一存储和管理,下一篇文章会系统介绍Redux以及在这个小项目里引入Redux的过程。

打赏支持我写出更多好文章,谢谢!

打赏作者

打赏支持我写出更多好文章,谢谢!

任选一种支付方式

betway 11 betway 12

1 赞 2 收藏 1 评论

在移动开发中,动画是提高用户体验不可缺少的一个元素。在React Native中,动画API提供了一些现成的组件:Animated.View,Animated.Text和Animated.Image默认支持动画。动画API会调用iOS或者Android的本地代码来完成这些组件的位移、大小等动画。

如果你不也不太了解react-native中的动画,不妨先看看官方文档

打赏支持我写出更多好文章,谢谢!

任选一种支付方式

betway 13 betway 14

1 赞 3 收藏 评论

关于作者:D.son

betway 15

80后码农兼伪文青一枚,闷骚而不木讷,猥琐不流浪荡 个人主页 · 我的文章 · 1

betway 16

在React Native中,Animated创建过程如下:

下面我对用到的属性及方法做一个简要概述:

关于作者:D.son

betway 17

80后码农兼伪文青一枚,闷骚而不木讷,猥琐不流浪荡 个人主页 · 我的文章 · 1

betway 18

创建Animated.Value,设置初始值,比如一个视图的opacity属性,最开始设置Animated.Value(0),来表示动画的开始时候,视图是全透明的。
AnimatedValue绑定到Style的可动画属性,比如透明度,{opacity: this.state.fadeAnim}
使用Animated.timing来创建自动的动画,或者使用Animated.event来根据手势,触摸,Scroll的动态更新动画的状态(本文会侧重讲解Animated.timing)
调用Animated.timeing.start()开始动画
Animated简介
大多数情况下,在 React Native 中创建动画是推荐使用 Animated API 的,其提供了三个主要的方法用于创建动画:

Animation

Animated.timing() -- 推动一个值按照一个过渡曲线而随时间变化。Easing 模块定义了很多缓冲曲线函数。
Animated.decay() -- 推动一个值以一个初始的速度和一个衰减系数逐渐变为0。
Animated.spring() -- 产生一个基于 Rebound 和 Origami 实现的Spring动画。它会在 toValue 值更新的同时跟踪当前的速度状态,以确保动画连贯。
除了这三个创建动画的方法,对于每个独立的方法都有三种调用该动画的方式:

使用范围:

在react-native中有且仅有三个组件支持animation,它们分别是:Image,View,Text,用的最多的可能是View。

Animated.parallel() --同时开始一个动画数组里的全部动画。默认情况下,如果有任何一个动画停止了,其余的也会被停止。你可以通过stopTogether 选项来改变这个效果。
Animated.sequence() --按顺序执行一个动画数组里的动画,等待一个完成后再执行下一个。如果当前的动画被中止,后面的动画则不会继续执行。
Animated.stagger() -- 一个动画数组,里面的动画有可能会同时执行(重叠),不过会以指定的延迟来开始。
Animated.timing()
使用 Animated.timing 创建的旋转动画。Animated.timing()的基本使用方法如下:

执行动画函数:

  • start():开始动画

  • stop(): 结束动画

Animated.timing(
  someValue,
  {
    toValue: number,
    duration: number,
    easing: easingFunction,
    delay: number
  }
)
Easing 也是用React Native创建动画的载体,它允许我们使用已经定义好的各种缓冲函数,例如:linear, ease, quad, cubic, sin, elastic, bounce, back, bezier, in, out, inout 。由于有直线运动,我们将使用 linear。
接下来,需要在构造函数中初始化一个带动画属性的值用于旋转动画的初始值:

Value

在Animation中,设置一种类型的动画后,也要声明Value,就是动画的变化值。一般会将Value在this.state中声明,通过改变改value来实现动效,官网上给的例子就是给spring类型的动画设置bounceValue,有兴趣的小伙伴可以去官网上看,这里不做赘述。

constructor () {
  super()
  this.spinValue = new Animated.Value(0)
}
我们使用 Animated.Value声明了一个 spinValue 变量,并传了一个 0 作为初始值。然后创建了一个名为 spin 的方法,并在 componentDidMount 中调用它,目的是在 app 加载之后运行动画。

动画类型:

  • spring: 弹跳动画,它包括两个参数

    friction:摩擦力默认值为7

    tension:张力,默认值为40

  • decay: 以一个初始值开始逐渐减慢至停止,它亦包括两个参数

    velocity:起始速度,不可缺省哦!

    deceleration:速度递减比例,默认值为0.997。

  • timing: 渐变动画,它有三个可配置参数

    duration:动画持续的时间,默认值为500毫秒

    easing: 定义曲线渐变函数。iOS中默认为Easing.inOut(Easing.ease)

    delay: 延迟多少毫秒后执行,默认为0

componentDidMount () {
  this.spin()
}
spin () {
  this.spinValue.setValue(0)
原文链接:
  Animated.timing(
    this.spinValue,
    {
      toValue: 1,
      duration: 4000,
      easing: Easing.linear
    }
  ).start(() => this.spin())
}
现在方法已经创建好了,接下来就是在UI中渲染动画了。

组合动画

就像在OC中有组动画一样,react-native也提供了类似组动画的函数,即组合动画。你可以将多个动画通过,parallel, sequence, stagger和delay组合使用,三种方式来组织多个动画。它们所接受的参数都是一个动画数组。

render () {
  const spin = this.spinValue.interpolate({
    inputRange: [0, 1],
    outputRange: ['0deg', '360deg']
  })
  return (
    <View style={styles.container}>
      <Animated.Image
        style={{
          width: 227,
          height: 200,
          transform: [{rotate: spin}] }}
          source={{uri: '
      />
    </View>
  )
}
实现效果:
这里写图片描述

插值 interpolate

插值函数是 Animation 中相对比较重要且强大的函数,如果你想实现比较流畅炫酷的动画,那么插值函数是非用不可的。在接下来我给大家展示的例子中就多次用到interpolate
它主要通过接受一个输入区间inputRange ,然后将其映射到一个输出区间outputRange,通过这种方法来改变不同区间值上的不同动效。

以上介绍的都是Animation中比较常用的API,还有诸如跟踪动态值,输入事件,响应当前动画值,LayoutAnimation 等灯,这里先不做总结,以后再做讲解。

OK,下面切入正题,到底如何实现像下图一样流畅的上拉动画呢?

betway 19

动效.gif

思路:
1.先将View1布局好,将View2布局到View1下方

2.点击FlipButton时,改变View2的top坐标,并改变 this.state.pullUp,标记FlipButton的状态

3.改变View2的top坐标时改变View1的透明度

4.将FlipButton旋转180度

5.一定要将FlipButton提至Z轴的最顶端,也就是说要高于 View1 和 View2,在它们的上层,这样才能保证,无论是View1面向于用户面还是View2面向于用户,FlipButton都还是那个最初的FlipButton,并永远面向用户,不会被任何视图覆盖。

如图:
未点击Button时 View1 面向于用户,view2在view1下面

betway 20

上拉前.png

点击Button,View2置于View1上层,并且Button位置变化

betway 21

上拉后.png

核心代码如下:

在constructor方法中声明我们需要的 pullUpAnim & fadeAnim 并为其赋予初始Value
其中pullUpAnim是当点击FlipButton按钮时上滑View2,在这个动画中将插入改变透明度的插值器,来改变View1的透明度,后面会看到相应代码

export default class Voice extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
              pullUp:false,
            pullUpAnim: {
                pullUp: new Animated.Value(0),
            },
            fadeAnim: {
                descriptionAlpha: new Animated.Value(0),
            },
        };
        this.onFlipButtonPress = this.onFlipButtonPress.bind(this);
    }

下面代码都是View1的布局。其中当执行pullUp动画时,插入改变View1背景透明度的动画,其中inputRange为[0,1],outputRange为[1,0],就是,当pullUp.pullUp的value为0时,View1的opacity为1,不透明;而当pullUp.pullUp的value变为1的时候,View1的opacity为0 ,完全透明,用户将看不到View1。

   render(){
    return (
    <Animated.View style={[styles.container,
    {
    opacity:
    this.state.pullUpAnim.pullUp.interpolate({
    inputRange: [0, 1],
    outputRange: [1, 0],
    })
    }
    ]}>
    <View style={styles.navBar}>
    <Image style={[styles.navBarImage,
    { resizeMode: 'stretch' }]}
    source={App.Image.bg.voiceHeaderShadow} />
    </View>

    <View style={styles.navButtonContainer}>
    <TouchableOpacity
    style={styles.returnBtn}
    onPress={this.onReturnButtonPress}>
    <Image source={App.Image.btn.navBackWhite} />
    </TouchableOpacity>

    <TouchableOpacity
    style={styles.shareBtn}
    onPress={this.onShareButtonPress}>
    <Image source={App.Image.btn.navShare} />
    </TouchableOpacity>
    </View>

    <View style={styles.titleContainer}>
    <Text style={styles.title}>
    {title}
    </Text>
    </View>
    {this.state.voiceData &&
    <NativeComponent.RCTVoicePlayView
    voiceData={this.state.voiceData}
    fromScanQR={this.state.fromScanQR}
    style={{
        flexDirection: 'row',
        alignItems: 'center',
        justifyContent: 'center',
        width: App.Constant.screenWidth,
        height: App.Constant.screenWidth === 320 ? App.Constant.screenWidth : App.Constant.screenWidth * 1.1,
        marginTop: 10,
    }}
    />}
{this.state.voiceData &&
    <Animated.View style={styles.functionContainer}>
        <TouchableOpacity
            style={styles.downloadBtn}
            onPress={this.onVoiceDownloadBtnPress}>
            {this.state.isDownload ?
                <Image source={App.Image.btn.voiceDownloaded} />
                :
                <Image source={App.Image.btn.voiceDownload} />
            }
        </TouchableOpacity>
        <TouchableOpacity
            style={styles.bookmarkBtn}
            onPress={this.onVoiceLikeBtnPress}>
            <Image source={this.state.isBookMark ? App.Image.btn.voiceLiked : App.Image.btn.voiceLike} />
        </TouchableOpacity>

        <View style={styles.voicestarBtnContainer}>
            <TouchableOpacity
                style={styles.voicestarBtn}
                onPress={this.onVoiceStarBtnPress}>
                <Image source={this.state.isCommented ? App.Image.btn.voiceStared : App.Image.btn.voiceStar} />
            </TouchableOpacity>
            {this.state.isCommented ?
                <Text style={styles.score}>
                                        {this.state.score.toFixed(1)}
                                    </Text> : null
                                }
                            </View>
                        </Animated.View>
                    }
                </Animated.View >

这里的Comment是我自定义的组件,这里可以理解成View2。从style中可以看出,我将View2的position设为绝对布局,也就是它的位置是固定的,不想对于任何其他控件的位置,不随上下左右控价坐标的改变而改变。而View2的动画效果是从View1的底部逐渐移动到手机屏幕的顶部,同样的,我们给pullUpAnim.pullUp设置再一个插值器,这个插值器主要是针对top属性做修改了,当pullUp为0时,view2的top为屏幕高度,也就是View2距屏幕顶部的距离为screenHeight,当pullUp为1时,View2距屏幕顶部距离为0。

{this.state.voiceData &&
    <Comment voiceID={this.state.voiceID}
        voiceData={this.state.voiceData}
        style={{
            position: 'absolute',
            width: App.Constant.screenWidth,
            height: App.Constant.screenHeight,
            top: this.state.pullUpAnim.pullUp.interpolate({
                inputRange: [0, 1],
                outputRange: [App.Constant.screenHeight, 0]
            }),
        }}
      displayAnim={this.state.pullUpAnim.pullUp} />
 }

下面这段代码就是对FlipButton的布局 ,上面提到过,FlipButton必须在View1 和 View2的上面,在Z轴的最上面,因此我将它放在View1和View2布局的后面,这种方法比较笨,但是我还没找到如何轻易的将一个组件提到Z轴最顶层。
其中FlipButton是自己封装的一个组件,里面主要实现背景色的变化和透明度的变化以及将按钮反转180度。

{this.state.voiceData &&
    <Animated.View style={{
        position: 'absolute',
        marginLeft: 20,
        top: this.state.pullUpAnim.pullUp.interpolate({
            inputRange: [0, 1],
            outputRange: [App.Constant.screenHeight - 40, 30],
        }),
        opacity: this.state.fadeAnim.descriptionAlpha.interpolate({
            inputRange: [0, 1],
            outputRange: [1, 0]
        }),
    }}>
        <FlipButton
            flip={this.state.pullUp}
            style={'white'}
            onPress={this.onFlipButtonPress}
            />
    </Animated.View>
        )
   }   
}

上面代码中在onFlipButtonPress方法中,使用到了渐变动画 timing 执行时间为180毫秒,并为toValue设置新的pullUp,因为上文提到的插值器会根据改值的变化而进行不同的响应,实现不同的透明度变化或top变化。this.state.pullUp的值为 bool 值,false 时为0,true时为1。之所以定义这个值,是因为在自定义的FlipButton中需要使用这个值来配置FlipButton的timing动画。

 // 点击FlipButton事件
     onFlipButtonPress() {
        const pullUp = !this.state.pullUp;
        Animated.timing(
            this.state.pullUpAnim.pullUp,
            {
                duration: 180,
                toValue: pullUp
            }
        ).start(() => {
            this.setState({
                pullUp,
            });
        });
    }

看到这里,是不是有一种感觉,其实this.state.pullUpAnim.pullUp动画并没有去实现任何动画,而是提供了一个容器而已,供其他插值器有容器可以依附,因为需求中的动画,需要我们在点击按钮时不仅改变View1的透明度,还要改变View2距顶部的位置,所以用基本的动画是无法实现的,必须使用插值器在不同的情况下来实现不同的动画效果。这下知道插值器的强大之处了吧,随时随地有需要就给容器动画加插值器就好啦!

OK,今天就到这里吧,如果在阅读过程用发现什么问题,欢迎指正,共勉!

完整代码:

/**
* Sample React Native App
*
* @flow
*/

import React, {Component} from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    Animated,
    TouchableOpacity,
    Easing,
    View
} from 'react-native';

class AnimationRotateScene extends Component {

    constructor(props) {
        super(props);
        this.spinValue = new Animated.Value(0)
    }

    componentDidMount () {
        this.spin()
    }

    spin () {
        this.spinValue.setValue(0)
        Animated.timing(
            this.spinValue,
            {
                toValue: 1,
                duration: 4000,
                easing: Easing.linear
            }
        ).start(() => this.spin())
    }

    render() {

        const
            spin = this.spinValue.interpolate({
                inputRange: [0, 1],
                outputRange: ['0deg', '360deg']
            })

        return (
            <View style={styles.container}>
原文链接:

                <Animated.Image
                    style={{
                        width: 227,
                        height: 200,
                        transform: [{rotate: spin}] }}
                    source={{uri: '
                />
                <TouchableOpacity onPress={() => this.spin()} style={styles.button}>
                    <Text>启动动画</Text>
                </TouchableOpacity>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        marginTop: 20,
        justifyContent: 'center',
        alignItems: 'center',
    },
    button: {
        marginTop: 20,
        backgroundColor:'#808080',
        height:35,
        width:140,
        borderRadius:5,
        justifyContent: 'center',
        alignItems: 'center',
    },
});

export default AnimationRotateScene;

Animated.spring()
使用 Animated.spring() 方法创建一个放大缩小的动画。
这里写图片描述
Animated.spring() 方法使用:

Animated.spring(
    someValue,
    {
      toValue: number,
      friction: number
    }
)
如上图所示,我们要使用Animated.spring()创建一个放大缩小的动画效果。
在构造函数中,创建一个 springValue 变量,初始化其值为0.3。

constructor () {
  super()
  this.springValue = new Animated.Value(0.3)
}

然后,删除 animated 方法和componentDidMount方法,创建一个新的 spring 方法。

spring () {
  this.springValue.setValue(0.3)
  Animated.spring(
    this.springValue,
    {
      toValue: 1,
      friction: 1
    }
  ).start()
}
然后我们给View的button添加一个点击事件,出发上面的spring动画。

<View style={styles.container}>
  <Text
    style={{marginBottom: 100}}
    onPress={this.spring.bind(this)}>Spring</Text>
    <Animated.Image
      style={{ width: 227, height: 200, transform: [{scale: this.springValue}] }}
      source={{uri: ';
</View>
完整代码如下:

/**
* Sample React Native App
*
* @flow
*/

import React, {Component} from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    Animated,
    TouchableOpacity,
    Easing,
    View
} from 'react-native';
原文链接:

class AnimationRotateScene extends Component {

    constructor(props) {
        super(props);
        this.spinValue = new Animated.Value(0)
    }

    componentDidMount () {
        this.spin()
    }

    spin () {
        this.spinValue.setValue(0)
        Animated.timing(
            this.spinValue,
            {
                toValue: 1,
                duration: 4000,
                easing: Easing.linear
            }
        ).start(() => this.spin())
    }

    render() {

        const
            spin = this.spinValue.interpolate({
                inputRange: [0, 1],
                outputRange: ['0deg', '360deg']
            })

        return (
            <View style={styles.container}>

                <Animated.Image
                    style={{
                        width: 227,
                        height: 200,
                        transform: [{rotate: spin}] }}
                    source={{uri: '
                />
                <TouchableOpacity onPress={() => this.spin()} style={styles.button}>
                    <Text>启动动画</Text>
                </TouchableOpacity>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        marginTop: 20,
        justifyContent: 'center',
        alignItems: 'center',
    },
    button: {
        marginTop: 20,
        backgroundColor:'#808080',
        height:35,
        width:140,
        borderRadius:5,
        justifyContent: 'center',
        alignItems: 'center',
    },
});

export default AnimationRotateScene;

Animated.parallel()
Animated.parallel() 会同时开始一个动画数组里的全部动画。parallel()会接受一个动画数组,首先看一下api:

Animated.parallel(arrayOfAnimations)
// In use:
Animated.parallel([
  Animated.spring(
    animatedValue,
    {
      //config options
    }
  ),
  Animated.timing(
     animatedValue2,
     {
       //config options
     }
  )
])
所以,我们先创建一个动画数组,并初始化。

constructor () {
  super()
  this.animatedValue1 = new Animated.Value(0)
  this.animatedValue2 = new Animated.Value(0)
  this.animatedValue3 = new Animated.Value(0)
}
然后,创建一个 animate 方法并在 componendDidMount() 中调用它。

componentDidMount () {
  this.animate()
}
animate () {
  this.animatedValue1.setValue(0)
  this.animatedValue2.setValue(0)
  this.animatedValue3.setValue(0)
  const createAnimation = function (value, duration, easing, delay = 0) {
    return Animated.timing(
      value,
      {
        toValue: 1,
        duration,
        easing,
        delay
      }
    )
  }
  Animated.parallel([
    createAnimation(this.animatedValue1, 2000, Easing.ease),
    createAnimation(this.animatedValue2, 1000, Easing.ease, 1000),
    createAnimation(this.animatedValue3, 1000, Easing.ease, 2000)       
  ]).start()
}
在 animate 方法中,我们将三个动画属性值重置为0。此外,还创建了一个 createAnimation 方法,该方法接受四个参数:value, duration, easing, delay(默认值是0),返回一个新的动画。

然后,调用 Animated.parallel(),并将三个使用 createAnimation 创建的动画作为参数传递给它。在 render 方法中,我们需要设置插值:
原文链接:

render () {
  const scaleText = this.animatedValue1.interpolate({
    inputRange: [0, 1],
    outputRange: [0.5, 2]
  })
  const spinText = this.animatedValue2.interpolate({
    inputRange: [0, 1],
    outputRange: ['0deg', '720deg']
  })
  const introButton = this.animatedValue3.interpolate({
    inputRange: [0, 1],
    outputRange: [-100, 400]
  })
  ...
}
最后,我们用一个主 View 包裹三个 Animated.Views:

<View style={[styles.container]}>
  <Animated.View
    style={{ transform: [{scale: scaleText}] }}>
    <Text>Welcome</Text>
  </Animated.View>
  <Animated.View
    style={{ marginTop: 20, transform: [{rotate: spinText}] }}>
    <Text
      style={{fontSize: 20}}>
      to the App!
    </Text>
  </Animated.View>
  <Animated.View
    style={{top: introButton, position: 'absolute'}}>
    <TouchableHighlight
      onPress={this.animate.bind(this)}
      style={styles.button}>
      <Text
        style={{color: 'white', fontSize: 20}}>
        Click Here To Start
      </Text>
   </TouchableHighlight>
  </Animated.View>
</View>

完整的代码如下:

/**
* Sample React Native App
*
* @flow 组动画
*/

import React, {Component} from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    Animated,
    TouchableOpacity,
    TouchableHighlight,
    Easing,
    View
} from 'react-native';

原文链接:
class AnimationGroupScene extends Component {

    constructor() {
        super()
        this.animatedValue1 = new Animated.Value(0)
betway,        this.animatedValue2 = new Animated.Value(0)
        this.animatedValue3 = new Animated.Value(0)
    }

    componentDidMount() {
        this.animate()
    }

    animate() {
        this.animatedValue1.setValue(0)
        this.animatedValue2.setValue(0)
        this.animatedValue3.setValue(0)
        const createAnimation = function (value, duration, easing, delay = 0) {
            return Animated.timing(
                value,
                {
                    toValue: 1,
                    duration,
                    easing,
                    delay
                }
            )
        }
        Animated.parallel([
            createAnimation(this.animatedValue1, 2000, Easing.ease),
            createAnimation(this.animatedValue2, 1000, Easing.ease, 1000),
            createAnimation(this.animatedValue3, 1000, Easing.ease, 2000)
        ]).start()
    }

    startAnimation() {
        this.state.currentAlpha = this.state.currentAlpha == 1.0 ? 0.0 : 1.0;
        Animated.timing(
            this.state.fadeAnim,
            {toValue: this.state.currentAlpha}
        ).start();
    }

    render() {

        const scaleText = this.animatedValue1.interpolate({
            inputRange: [0, 1],
            outputRange: [0.5, 2]
        })
        const spinText = this.animatedValue2.interpolate({
            inputRange: [0, 1],
            outputRange: ['0deg', '720deg']
        })
        const introButton = this.animatedValue3.interpolate({
            inputRange: [0, 1],
            outputRange: [-100, 400]
        })

        return (
            <View style={styles.container}>

                <Animated.View
                    style={{transform: [{scale: scaleText}]}}>
                    <Text>Welcome</Text>
                </Animated.View>
                <Animated.View
                    style={{marginTop: 20, transform: [{rotate: spinText}]}}>
                    <Text
                        style={{fontSize: 20}}>
                        to the App!
                    </Text>
                </Animated.View>
                <Animated.View
                    style={{top: introButton, position: 'absolute'}}>
                    <TouchableHighlight
                        onPress={this.animate.bind(this)}
                        style={styles.button}>
                        <Text>启动组动画</Text>
                    </TouchableHighlight>
                </Animated.View>

            </View>
        );
    }
}
原文链接:

const styles = StyleSheet.create({
    container: {
        flex: 1,
        marginTop: 20,
        justifyContent: 'center',
        alignItems: 'center',
    },
    button: {
        marginTop: 20,
        backgroundColor: '#808080',
        height: 35,
        width: 140,
        borderRadius: 5,
        justifyContent: 'center',
        alignItems: 'center',
    },
});

export default AnimationGroupScene;

示例使用说明
这里写图片描述
这里写图片描述
如图所示,我对动画的代码做了一个简单的整理,大家在使用的时候直接引入AnimationRoot文件即可。
AnimationRoot文件内容如下:

/**
* Sample React Native App
*
* @flow
*/

import React, {Component} from 'react';
import { StackNavigator } from 'react-navigation';

import AnimationIndex from './AnimationIndex';
import AnimationSpringScene from './AnimationSpringScene';//缩放动画
import AnimationRotateScene from './AnimationRotateScene';//旋转动画
import AnimationAlphaScene from './AnimationAlphaScene';//Alpha动画
import AnimationGroupScene from './AnimationGroupScene';//组动画
import AnimationFrameScene from './AnimationFrameScene';//帧动画

const anim = StackNavigator({
    AnimationIndex: { screen: AnimationIndex },
    AnimationSpringScene: { screen: AnimationSpringScene },
    AnimationRotateScene: { screen: AnimationRotateScene },
    AnimationAlphaScene: { screen: AnimationAlphaScene },
    AnimationGroupScene: { screen: AnimationGroupScene },
    AnimationFrameScene: { screen: AnimationFrameScene },
});
export default anim;

最后是项目实现的最终结果图,代码地址动画源码
原文链接:

本文由必威发布于Web前端,转载请注明出处:Native学习实践,ReactNative学习实践