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

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~

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

金沙官网线上 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: '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
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();

金沙官网线上 2
WTF!
金沙官网线上 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));
}
...

效果粗来了!

金沙官网线上 4
怎么说呢金沙官网线上 5,

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

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

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

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

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

这四个动画是完全相同的,只是以一定延迟顺序进行的,因此分解之后只要实现一个就可以了,每个动画就是条块的高度随时间呈现规律变化:
金沙官网线上 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));
  }

最终效果:
金沙官网线上 7

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

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

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

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

打赏作者

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

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

任选一种支付方式

金沙官网线上 8 金沙官网线上 9

1 赞 2 收藏 1 评论

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

关于作者:D.son

金沙官网线上 10

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

金沙官网线上 11

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

创建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 的,其提供了三个主要的方法用于创建动画:

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

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

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。
接下来,需要在构造函数中初始化一个带动画属性的值用于旋转动画的初始值:

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

componentDidMount () {
  this.spin()
}
spin () {
  this.spinValue.setValue(0)
原文链接:http://click.aliyun.com/m/22229/
  Animated.timing(
    this.spinValue,
    {
      toValue: 1,
      duration: 4000,
      easing: Easing.linear
    }
  ).start(() => this.spin())
金沙官网线上,}
现在方法已经创建好了,接下来就是在UI中渲染动画了。

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: 'https://s3.amazonaws.com/media-p.slid.es/uploads/alexanderfarennikov/reactjs.png'}}
      />
    </View>
  )
}
实现效果:
这里写图片描述

完整代码:

/**
* Sample React Native App
* https://github.com/facebook/react-native
* @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}>
原文链接:http://click.aliyun.com/m/22229/

                <Animated.Image
                    style={{
                        width: 227,
                        height: 200,
                        transform: [{rotate: spin}] }}
                    source={{uri: 'https://s3.amazonaws.com/media-p.slid.es/uploads/alexanderfarennikov/reactjs.png'}}
                />
                <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: 'https://s3.amazonaws.com/media-p.slid.es/uploads/alexanderfarennikov/reactjs.png'}}/&gt;
</View>
完整代码如下:

/**
* Sample React Native App
* https://github.com/facebook/react-native
* @flow
*/

import React, {Component} from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    Animated,
    TouchableOpacity,
    Easing,
    View
} from 'react-native';
原文链接:http://click.aliyun.com/m/22229/

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

本文由金沙官网线上发布于Web前端,转载请注明出处:React Native学习实践:动画初探之加载动画

您可能还会对下面的文章感兴趣: