React Native 动画(Animated)

在上篇文章中介绍了 LayoutAnimation 的用法,本篇文章就来详细介绍一下 Animated 的用法。

Animated 简介

Animated 库用于创建更精细的交互控制的动画,它使得开发者可以非常容易地实现各种各样的动画和交互方式,并且具备极高的性能。Animated 旨在以声明的形式来定义动画的输入与输出,在其中建立一个可配置的变化函数,然后使用简单的 start/stop 方法来控制动画按顺序执行。

Animated 提供了两种类型的值:

  • Animated.Value() 用於单个值。
  • Animated.ValueXY() 用于矢量值。

Animated.Value() 可以绑定到样式或是其他属性上,也可以进行插值运算。单个 Animated.Value() 可以用在任意多个属性上。

Animated 用于创建动画的主要方法:

  • Animated.timing():最常用的动画类型,使一个值按照一个过渡曲线而随时间变化。
  • Animated.spring():弹簧效果,基础的单次弹跳物理模型实现的 spring 动画。
  • Animated.decay():衰变效果,以一个初始的速度和一个衰减系数逐渐减慢变为0。

Animated 实现组合动画的主要方式:

  • Animated.parallel():同时开始一个动画数组里的全部动画。默认情况下,如果有任何一个动画停止了,其余的也会被停止。可以通过stopTogether 选项设置为 false 来取消这种关联。
  • Animated.sequence():按顺序执行一个动画数组里的动画,等待一个完成后再执行下一个。如果当前的动画被中止,后面的动画则不会继续执行。
  • Animated.stagger():一个动画数组,传入一个时间参数来设置队列动画间的延迟,即在前一个动画开始之后,隔一段指定时间才开始执行下一个动画里面的动画,并不关心前一个动画是否已经完成,所以有可能会出现同时执行(重叠)的情况。

Animated 封装了四个可以动画化的组件:

  • Animated.View
  • Animated.Text
  • Animated.Image
  • Animated.ScrollView

也可以使用 Animated.createAnimatedComponent() 来封装你自己的组件(用 Animated.View 包裹可以达到同样的效果)。

合成动画值:

  • Animated.add()
  • Animated.divide()
  • Animated.modulo()
  • Animated.multiply()

可以使用加减乘除以及取余等运算来把两个动画值合成为一个新的动画值。

插值函数:

  • interpolate():将输入值范围转换为输出值范围。

譬如:把0-1映射到0-10。

接下来,我们结合一个个的例子介绍它们的用法:

1. Animated.timing()

最常用的动画类型,使一个值按照一个过渡曲线而随时间变化,格式如下:
Animated.timing(animateValue, conf<Object>),conf参数格式:

 

{
  duration:动画持续的时间(单位是毫秒),默认为500。
  easing:一个用于定义曲线的渐变函数。Easing模预置了 linear、ease、elastic、bezier 等诸多缓动特性。iOS默认为Easing.inOut(Easing.ease),。
  delay:开始动画前的延迟时间(毫秒),默认为0。
}

一个简单的例子:

 

export default class Opacity extends Component {
    constructor(props) {
        super(props)

        this.state = {
            fadeOutOpacity: new Animated.Value(1),
        }

        this.fadeOutAnimated = Animated.timing(
            this.state.fadeOutOpacity,
            {
                toValue: 0,  //透明度动画最终值
                duration: 3000,   //动画时长3000毫秒
                easing: Easing.linear,
            }
        );
    }

    _startAnimated() {
        this.fadeOutAnimated.start(() => this.state.fadeOutOpacity.setValue(1));
    }

    render(){
        return (
            <View style={styles.mainStyle}>
                 <Animated.View style={{width: 200, height: 300, opacity: this.state.fadeOutOpacity}}>
                    <Image ref="image" style={{width:200,height:300}}
                           source={{uri:'beauty.jpg'}}>
                    </Image>
                 </Animated.View>

                <TouchableOpacity style={styles.touchStyle} onPress={this._startAnimated.bind(this)}>
                    <Text style={{width:200,height:100,textAlign:'center',lineHeight:100}}>点击开始动画</Text>
                </TouchableOpacity>
            </View>
        );
    }
}

效果图:

 

image.gif

上面的代码做了下面这些事情:

  • 1.首先实例化动画的初始值 fadeOutOpacity,作为图片的透明度值。
  • 2.然后定义了一个 Animated.timing() 动画事件,并配置相应的参数,把它赋予 this.fadeOutAnimated 变量,在后续可以使用 .start() 或 .stop() 方法来开始/停止该动画。
  • 3.把动画添加到在 <Animate.View></Animate.View> 上,我们把实例化的动画初始值传入 style 中的 opacity。
  • 4.最后我们点击按钮,调用 this.fadeOutAnimated.start() 方法开启动画,在这里将 () => this.state.fadeOutOpacity.setValue(1) 传给了 start(),用于在一次动画完成之后把图片的透明度再次设置为1,这也是创建无穷动画的方式。

interpolate() 这个函数很强大,实现了数值大小、单位的映射转换,下面我们结合插值函数:interpolate() 搞些事情:

 

export default class Mixture extends Component {

    constructor(props) {
        super(props)

        this.state = {
            animatedValue: new Animated.Value(0),
        }

        this.rotateAnimated = Animated.timing(
            this.state.animatedValue,
            {
                toValue: 1,
                duration: 3000,
                easing: Easing.in,
            }
        );
    }

    _startAnimated() {
        this.state.animatedValue.setValue(0);
        this.rotateAnimated.start(() => this._startAnimated());
    }

    render(){

        const rotateZ = this.state.animatedValue.interpolate({
            inputRange: [0, 1],
            outputRange: ['0deg', '360deg']
        });

        const opacity = this.state.animatedValue.interpolate({
            inputRange: [0, 0.5, 1],
            outputRange: [0, 1, 0]
        });

        const rotateX = this.state.animatedValue.interpolate({
            inputRange: [0, 0.5, 1],
            outputRange: ['0deg', '180deg', '0deg']
        });

        const textSize = this.state.animatedValue.interpolate({
            inputRange: [0, 0.5, 1],
            outputRange: [18, 32, 18]
        });

        const marginLeft = this.state.animatedValue.interpolate({
            inputRange: [0, 0.5, 1],
            outputRange: [0, 200, 0]
        });

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

                <Animated.View
                    style={{
                        marginTop: 10,
                        width: 100,
                        height: 100,
                        transform: [
                            {rotateZ:rotateZ},
                        ]
                    }}
                >
                    <Image style={{width:100,height:100}}
                           source={{uri:'out_loading_image.png'}}>
                    </Image>
                </Animated.View>

                <Animated.View
                    style={{
                        marginTop: 10,
                        width: 100,
                        height: 100,
                        opacity:opacity,
                        backgroundColor:'red',
                    }}
                />

                <Animated.Text
                    style={{
                        marginTop: 10,
                        width:100,
                        fontSize: 18,
                        color: 'white',
                        backgroundColor:'red',
                        transform: [
                            {rotateX:rotateX},
                        ]
                    }}
                >
                    窗外风好大,我没有穿褂。
                </Animated.Text>

                <Animated.Text
                    style={{
                        marginTop: 10,
                        height: 100,
                        lineHeight: 100,
                        fontSize: textSize,
                        color: 'red'
                    }}
                >
                    IAMCJ嘿嘿嘿
                </Animated.Text>

                <Animated.View
                    style={{
                        marginTop: 10,
                        width: 100,
                        height: 100,
                        marginLeft:marginLeft,
                        backgroundColor:'red',
                    }}
                />

                <TouchableOpacity style={styles.touchStyle} onPress={this._startAnimated.bind(this)}>
                    <Text style={{width:200,height:100,textAlign:'center',lineHeight:100}}>点击开始动画</Text>
                </TouchableOpacity>
            </View>
        );
    }
}

效果图:

 

Mixture.gif

 

其中 transform 是一个变换数组,常用的有 scale, scaleX, scaleY, translateX, translateY, rotate, rotateX, rotateY, rotateZ,其他用法与上面相同,在这就不多BB了。

2. Animated.spring()

弹簧效果,基础的单次弹跳物理模型实现的 spring 动画,格式如下:
Animated.spring(animateValue, conf<Object>),conf参数格式:

 

{
  friction: 控制“弹跳系数”、夸张系数,默认为7。
  tension: 控制速度,默认为40。
  speed: 控制动画的速度,默认为12。
  bounciness: 反弹系数,默认为8。
}

一个 Animated.spring() 的例子:

 

export default class AnimatedSpring extends Component {

    constructor(props) {
        super(props);

        this.state = {
            springValue: new Animated.Value(1),
        };

        this.springAnimated = Animated.spring(
            this.state.springValue,
            {
                toValue: 1,
                friction: 2,    //弹跳系数
                tension: 10,   // 控制速度
            }
        );
    }

    _startAnimated() {
        this.state.springValue.setValue(0.1);
        this.springAnimated.start();
    }

    render(){
        return (
            <View style={styles.mainStyle}>

                <Animated.View
                    style={{
                        width: 282,
                        height: 51,
                        transform:[
                            {scale:this.state.springValue}
                        ]
                    }}
                >
                    <Image ref="image" style={{width:282,height:51}}
                           source={{uri:'appstore_comment_image.png'}}>
                    </Image>
                </Animated.View>

                <TouchableOpacity style={styles.touchStyle} onPress={this._startAnimated.bind(this)}>
                    <Text style={{width:200,height:100,textAlign:'center',lineHeight:100}}>点击开始动画</Text>
                </TouchableOpacity>
            </View>
        );
    }
}

效果图:

 

AnimatedSpring.gif

上面的代码做了下面这些事情:

  • 1.首先实例化动画的初始值 springValue,作为图片的初始 scale 大小。
  • 2.然后定义了一个 Animated. spring() 动画事件,并配置相应的参数,把它赋予 this. springAnimated 变量。
  • 3.把动画添加到在 <Animate.View></Animate.View> 上,我们把实例化的动画初始值传入 style 中的 transform:[{scale:this.state.springValue}]。
  • 4.最后我们点击按钮,在动画开启之前把 scale 调到0.1,然后调用 this. springAnimated.start() 方法开启动画。

最近写 native 也有一个类似的需求,我的实现方式是先放大一个比例如1.05,然后动画完成之后再添加动画缩小到正常比例,个人感觉实现起来和这个复杂度差不多,而在 native 可以根据 position 和锚点设置图片是从哪个点以及自身的哪个方向开始放大,例如在屏幕右上角出现图片,并且是从图片右上角开始放大这样,我写了一个简单易用的工具类,设置位置和弹出方向即可,添加任意内容,几行代码就可以搞定,有需要的话点击这里获取。

3. Animated.decay()

衰变效果,以一个初始的速度和一个衰减系数逐渐减慢变为0,格式如下:
Animated.decay(animateValue, conf<Object>),conf参数格式:

 

{
  velocity: 起始速度,必填参数。
  deceleration: 速度衰减比例,默认为0.997。
}

一个 Animated.decay() 的例子:

 

export default class AnimatedDecay extends Component {

    constructor(props) {
        super(props);

        this.state = {
            decayValue: new Animated.ValueXY({x:0,y:0}),
        };

        this.decayAnimated = Animated.decay(
            this.state.decayValue,
            {
                velocity: 5,       // 起始速度,必填
                deceleration: 0.95,  // 速度衰减比例,默认为0.997
            }
        );
    }

    _startAnimated() {
        this.decayAnimated.start();
    }

    render(){
        return (
            <View style={styles.mainStyle}>

                <Animated.View
                    style={{
                        width: 100,
                        height: 150,
                        transform:[
                            {translateX: this.state.decayValue.x}, // x轴移动
                            {translateY: this.state.decayValue.y}, // y轴移动
                        ]
                    }}
                >
                    <Image ref="image" style={{width:100,height:150}}
                           source={{uri:'beauty.jpg'}}>
                    </Image>
                </Animated.View>

                <TouchableOpacity style={styles.touchStyle} onPress={this._startAnimated.bind(this)}>
                    <Text style={{width:200,height:100,textAlign:'center',lineHeight:100}}>点击开始动画</Text>
                </TouchableOpacity>
            </View>
        );
    }
}

 

 

效果图:

AnimatedDecay.gif

 

嗯,就这个样子。

这个就不说了,使用方式和 Animated.spring()、Animated.timing() 类似,如果想要使用的话,最好能了解一些数学的知识。

接下来做些组合动画:

4. Animated.parallel()

同时开始一个动画数组里的全部动画。默认情况下,如果有任何一个动画停止了,其余的也会被停止。可以通过stopTogether 选项设置为 false 来取消这种关联,格式如下:
Animated.parallel(Animates<Array>, [conf<Object>]):,conf参数格式:

 

{
  stopTogether: false
}

第一个参数接受一个元素为动画的数组,通过执行 start() 方法可以并行执行该数组中的所有方法。如果数组中任意动画被中断的话,该数组内对应的全部动画会一起停止,不过我们可以通过第二个(可选)参数 conf 中的 stopTogether 来取消这种牵连特性。

来个酷炫的例子:

 

export default class AnimatedParallel extends Component {

    constructor(props) {
        super(props);

        this.state = {
            dogOpacityValue: new Animated.Value(1),
            dogACCValue : new Animated.Value(0)
        };

        this.parallelAnimated = Animated.parallel(
            [
                Animated.timing(
                    this.state.dogOpacityValue,
                    {
                        toValue: 1,
                        duration: 1000,
                    }
                ),
                Animated.timing(
                    this.state.dogACCValue,
                    {
                        toValue: 1,
                        duration: 2000,
                        easing: Easing.linear,
                    }
                ),
            ],
            {
                stopTogether: false
            }
        );
    }

    _startAnimated() {
        this.state.dogOpacityValue.setValue(0);
        this.state.dogACCValue.setValue(0);
        this.parallelAnimated.start();
    }

    render(){

        //透明度
        const dogOpacity = this.state.dogOpacityValue.interpolate({
            inputRange: [0,0.2,0.4,0.6,0.8,1],
            outputRange: [0,1,0,1,0,1]
        });

        //项链上面
        const neckTop = this.state.dogACCValue.interpolate({
            inputRange: [0, 1],
            outputRange: [350, 235]
        });

        //眼镜左边
        const left = this.state.dogACCValue.interpolate({
            inputRange: [0, 1],
            outputRange: [-120, 127]
        });

        //眼镜旋转
        const rotateZ = this.state.dogACCValue.interpolate({
            inputRange: [0, 1],
            outputRange: ['0deg', '360deg']
        });

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

                {/*// 狗头*/}
                <Animated.View
                    style={{
                        width: 375,
                        height: 240,
                        opacity:dogOpacity,
                    }}
                >
                    <Image ref="image" style={{width:375,height:242}}
                           source={{uri:'dog.jpg'}}>
                    </Image>
                </Animated.View>

                {/*// 项链*/}
                <Animated.View
                    style={{
                        width: 250,
                        height: 100,
                        position: 'absolute',
                        top:neckTop,
                        left:93,
                    }}
                >
                    <Image ref="image" style={{width:250,height:100,resizeMode:'stretch'}}
                           source={{uri:'necklace.jpg'}}>
                    </Image>
                </Animated.View>

                <View
                    style={{
                        width: 375,
                        height: 200,
                        backgroundColor:'white',
                    }}
                />

                {/*// 眼镜*/}
                <Animated.View
                    style={{
                        width: 120,
                        height: 25,
                        position: 'absolute',
                        top:160,
                        left:left,
                        transform:[
                            {rotateZ:rotateZ}
                        ],
                    }}
                >
                    <Image ref="image" style={{width:120,height:25,resizeMode:'stretch'}}
                           source={{uri:'glasses.png'}}>
                    </Image>
                </Animated.View>

                <TouchableOpacity style={styles.touchStyle} onPress={this._startAnimated.bind(this)}>
                    <Text style={{width:200,height:100,textAlign:'center',lineHeight:100}}>点击开始动画</Text>
                </TouchableOpacity>
            </View>
        );
    }
}

效果图:

 

AnimatedParallel.gif

上面的代码做了下面这些事情:

  • 1.首先实例化动画的初始值 dogOpacityValue,作为狗头图片的初始透明度,实例化动画的初始值 dogACCValue,作为墨镜、项链图片的初始值。
  • 2.然后定义了两个 Animated. timing() 动画事件,并配置相应的参数,第一个是对应狗头图片的透明度,第二个对应的是墨镜、金链子的 timing 动画,然后把这两个动画事件放入到 Animated.parallel() 的动画数组内,最后把 Animated.parallel() 赋予 this.parallelAnimated 变量,用法其实和单独的动画事件差不多。
  • 3.在 render 方法中,创建了 dogOpacity、left、rotateZ、neckTop 等变量,并调用了 interpolate() 方法对它们一一映射,分别对应的是透明度、金链子 top 的距离、墨镜 left 距离、眼镜旋转的角度,结合上面代码查看具体信息。
  • 4.把动画添加到在不同的 <Animate.View></Animate.View> 上,我们把实例化的动画初始值传入 style 中。
  • 5.最后动画开启之前把 dogOpacityValue、dogACCValue 调到0,然后调用 this. parallelAnimated.start() 方法开启动画。

5.Animated.sequence()

按顺序执行一个动画数组里的动画,等待一个完成后再执行下一个。如果当前的动画被中止,后面的动画则不会继续执行,格式如下:
Animated.sequence(Animates<Array>)
刚才知道了 Animated.parallel() 的用法,那么使用起来 Animated.sequence() 会上手更快,他们用法几乎一样,区别就在于 Animated.sequence() 顺序执行一个动画数组里的动画,可以理解为同步操作,而 Animated.parallel() 是异步操作,这两种方式并没有好与坏,只有适合不适合,例如下面的这个效果就是适合使用 Animated.sequence() 来完成,而使用 Animated.parallel() 的话则不行。

再来一个酷炫的效果:

 

export default class AnimatedSequence extends Component {

    constructor(props) {
        super(props);

        this.state = {
            turnRotateValue: new Animated.Value(0),
            turnShakeValue : new Animated.Value(0),
            macValue : new Animated.Value(0),
        };

        this.sequenceAnimated = Animated.sequence(
            [
                Animated.timing(
                    this.state.turnRotateValue,
                    {
                        toValue: 1,
                        duration: 5000,
                        easing: Easing.in,
                    }
                ),
                Animated.timing(
                    this.state.turnShakeValue,
                    {
                        toValue: 1,
                        duration: 500,
                        easing: Easing.in,
                        delay:300,
                    }
                ),
                Animated.spring(
                    this.state.macValue,
                    {
                        toValue: 1,
                        friction: 3,
                        tension:10,
                    }
                ),
            ]
        );
    }

    _startAnimated() {
        this.sequenceAnimated.start();
    }

    render(){

        //转盘旋转
        const turnRotateZ = this.state.turnRotateValue.interpolate({
            inputRange: [0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1],
            outputRange: [
                '0deg',
                '180deg',
                '360deg',
                '720deg',
                '1080deg',
                '1800deg',
                '2520deg',
                '3060deg',
                '3420deg',
                '3600deg',
                '3690deg',
            ]
        });

        //转盘震动
        const marginLeft = this.state.turnShakeValue.interpolate({
            inputRange: [0,0.2,0.4,0.6,0.8,1],
            outputRange: [0,-40,40,-40,40,0]
        });

        //MacTop
        const macTop = this.state.macValue.interpolate({
            inputRange: [0, 1],
            outputRange: [-200,150]
        });

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

                {/*// 转盘*/}
                <Animated.View
                    style={{
                        width: 300,
                        height: 300,
                        marginLeft:marginLeft,
                        transform:[
                            {rotateZ:turnRotateZ}
                        ],
                    }}
                >
                    <Image ref="image" style={{width:300,height:300}}
                           source={{uri:'turntable.jpg'}}>
                    </Image>
                </Animated.View>

                {/*// mac*/}
                <Animated.View
                    style={{
                        width: 300,
                        height: 204,
                        position: 'absolute',
                        top:macTop,
                        left:screenW / 2 - 150,
                    }}
                >
                    <Image ref="image" style={{width:300,height:204}}
                           source={{uri:'macpro.png'}}>
                    </Image>
                </Animated.View>

                <TouchableOpacity style={styles.touchStyle} onPress={this._startAnimated.bind(this)}>
                    <Text style={{width:200,height:100,textAlign:'center',lineHeight:100}}>点击开始动画</Text>
                </TouchableOpacity>
            </View>
        );
    }
}

效果图:

 

AnimatedSequence.gif

这个动画的流程分为三个部分:

  • 1.转盘先由慢到快转了5秒钟;
  • 2.在转盘转圈停止之后,在短暂的延迟之后,震动了几下;
  • 3.最后 MacBook Pro 由天而降,惊不惊喜,意不意外,大奖来了!啊哈哈哈哈...

实现步骤就不一一解释了,对应上面 Animated.parallel() 的例子就可以看明白,主要区别在于动画的实现思路,这个动画就很好的利用了 Animated.sequence() 顺序执行的特性。

6.Animated.stagger()

一个动画数组,传入一个时间参数来设置队列动画间的延迟,即在前一个动画开始之后,隔一段指定时间才开始执行下一个动画里面的动画,并不关心前一个动画是否已经完成,所以有可能会出现同时执行(重叠)的情况,其格式如下:
Animated.stagger(delayTime<Number>, Animates<Array>)
其中 delayTime 为指定的延迟时间(毫秒),第二个和上面两个一样传入一个动画事件数组。

一个简单的红蓝块移动例子帮助理解:

 

export default class AnimatedStagger extends Component {

    constructor(props) {
        super(props);

        this.state = {
            redValue: new Animated.Value(0),
            blueValue : new Animated.Value(0),
        };

        this.staggerAnimated = Animated.stagger(2000,
            [
                Animated.timing(
                    this.state.redValue,
                    {
                        toValue: 1,
                        duration: 5000,
                        easing: Easing.in,
                    }
                ),
                Animated.timing(
                    this.state.blueValue,
                    {
                        toValue: 1,
                        duration: 5000,
                        easing: Easing.in,
                    }
                ),
            ]
        );
    }

    _startAnimated() {
        this.staggerAnimated.start();
    }

    render(){

        const redMarginLeft = this.state.redValue.interpolate({
            inputRange: [0,1],
            outputRange: [0,200]
        });

        const blueMarginLeft = this.state.blueValue.interpolate({
            inputRange: [0,1],
            outputRange: [0,200]
        });

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

                {/*// 红色*/}
                <Animated.View
                    style={{
                        width: 100,
                        height: 100,
                        backgroundColor:'red',
                        marginLeft:redMarginLeft,
                    }}
                >
                </Animated.View>


                {/*// 蓝色*/}
                <Animated.View
                    style={{
                        width: 100,
                        height: 100,
                        backgroundColor:'blue',
                        marginLeft:blueMarginLeft,
                    }}
                >
                </Animated.View>

                <TouchableOpacity style={styles.touchStyle} onPress={this._startAnimated.bind(this)}>
                    <Text style={{width:200,height:100,textAlign:'center',lineHeight:100}}>点击开始动画</Text>
                </TouchableOpacity>
            </View>
        );
    }
}

效果图:

 

AnimatedStagger.gif

这个动画的流程:
流程就是红的先移动,然后两秒后蓝的移动!(⊙﹏⊙)

看了这个例子,应该很容易明白 Animated.stagger() 的作用,在此不多做解释了。

OK,就到这吧,看完这篇文章,一般的动画效果应该都可以实现了,想了解 Animated 更多的特性可以去Animateddemo在这里。

参考:
ReactNative入门 —— 动画篇(下)
【译】详解React Native动画

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章