ReactNative 动画

如何使用

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

export default class AnimatedDocument extends Component{
    constructor(props){
        super(props);

        this.state = {
            fadeInOpacity: new Animated.Value(0)
        };
    }

    componentDidMount() {

        Animated.timing(this.state.fadeInOpacity, {
            toValue: 1, //  目标值
            duration: 2500, // 动画时间
            easing: Easing.linear  //  直线
        }).start();
    }

    render(){
        return(
            <Animated.View style={[styles.demo, {
                opacity: this.state.fadeInOpacity
            }]}>
                <Text style={styles.text}>悄悄的,我出现了</Text>
            </Animated.View>
        );
    }
}

const styles = StyleSheet.create({
    demo: {
        flex: 1,
        alignItems: 'center',
        justifyContent: 'center',
        backgroundColor: 'white'
    },
    text: {
        fontSize: 30
    }
});

效果演示

Animated_Image.png

步骤拆解
一个RN的动画可以按照如下的步骤进行:

  • 使用基本的Animated组件,如Animated.View、Animated.Image、Animated.Text(目前RN只支持这三个组件,官网也提供了自定义Animated组件的方法,不过不推荐)
  • 使用Animated.Value设定一个或多个初始值(透明度、位置等)
  • 将初始化值绑定到动画目标的属性上(如Style)
  • 通过Animatd.timing等函数设定动画参数
  • 调用start启动动画

更复杂的例子

constructor(props){
        super(props);

        this.state = {
            fadeInOpacity: new Animated.Value(0),
            rotation: new Animated.Value(0),
            fontSize: new Animated.Value(0)
        };
    }

    componentDidMount() {
        Animated.parallel(['fadeInOpacity', 'rotation', 'fontSize'].map(index => {
            return Animated.timing(this.state[index], {
                toValue: 1,
                duration: 1000,
                easing: Easing.linear
            })
        })).start();
    }

    render(){
        return(
            <Animated.View style={[styles.demo, {
                opacity: this.state.fadeInOpacity,
                transform: [{
                    rotateZ: this.state.rotation.interpolate({
                        inputRange: [0, 1],
                        outputRange: ['0deg', '360deg']
                    })
                }]
            }]}>
                <Animated.Text style={{
                    fontSize: this.state.fontSize.interpolate({
                        inputRange: [0, 1],
                        outputRange: [12, 26]
                    })
                }}>我骑着七彩祥云出现了~~</Animated.Text>
            </Animated.View>
        );
    }

注意到我们使用了Animated的一个新方法,parallel,它表示同时执行一组动画

Paste_Image.png

强大的interpolate
上面的例子使用了interpolate函数,也就是插值函数。这个函数很强大,实现了数值大小、单位的映射转换,比如:

{
  inputRange: [0, 1],
  outputRange: [‘0deg’, '180deg']
}

当setValue(0.5)时,会自动映射成90deg。inputRange并不局限于[0, 1]区间,可以画出多段。interpolate一般用于多个动画公用一个Animated.Value,只需要在每个属性里面映射好对应的值,就可以用一个变量控制多个动画。事实上,上例中的Animated.Value可以用一个变量来声明,这里只是为了演示parallel的用法

流程控制
在刚才的例子中,我们使用了Parallel来实现多个动画并行渲染,其它用于流程控制的API还有:

  • sequence接受一系列动画数组为参数,并依次执行
  • stagger接受一系列动画数组和一个延迟时间,按照序列,每隔一个延迟时间后执行下一个动画(其实就是插入了delay的parrllel)
  • delay生成一个延时时间(基于timing的delay参数生成)
constructor(props){
        super(props);

        this.state = {
            anim: [1, 2, 3].map(() => new Animated.Value(0))    //  初始化3个值
        };
    }

    componentDidMount() {
        let timing = Animated.timing;
        Animated.sequence([
            Animated.stagger(200, this.state.anim.map(left => {
                return timing(left, {
                    toValue: 1,
                });
            }).concat(
                this.state.anim.map(left => {
                    return timing(left, {
                        toValue: 0,
                    });
                })
            )),     // 三个view滚到右边再还原,每个动作间隔200ms
            Animated.delay(400),        //  延迟400ms,配合sequence使用
            timing(this.state.anim[0], {
                toValue: 1,
            }),
            timing(this.state.anim[1], {
                toValue: -1,
            }),
            timing(this.state.anim[2], {
                toValue: 0.5
            }),
            Animated.delay(400),
            Animated.parallel(this.state.anim.map((anim) => {
                timing(anim, {
                    toValue: 0,
                })
            }))     //  同时回到原位
        ]).start();
    }

    render(){

        let views = this.state.anim.map((value, i) => {
            return (
                <Animated.View key={i}
                    style={[styles.demo, styles['demo' + 1], {
                        left: value.interpolate({
                            inputRange: [0, 1],
                            outputRange: [0, 200]
                        })
                    }]}>
                    <Text style={styles.text}>我是第{i + 1}个View</Text>
                </Animated.View>
            );
        });

        return(
            <View style={styles.container}>
                <Text>sequence/delay/stagger/paraller演示</Text>
                {views}
            </View>
        );
    }
Paste_Image.png

Spring/Decay/Timing
前面的几个动画都是基于时间实现的,事实上,在日常的手势操作中,基于时间的动画往往难以满足复杂的交互动画。对此,RN太提供了另外两种动画模式

  • Spring 弹簧效果
    • friction 摩擦系数,默认40
    • tension 张力系数,默认7
    • bounciness
    • speed
  • Decay 衰变效果
    • velocity 初速率
    • deceleration 衰减系数 默认0.997
      Spring支持friction与tension或者bounciness与speed两种组合模式,这两种模式不能并存。

Track && Event
RN动画支持跟踪功能,这也是日常交互中很常见的需求,比如跟踪用户的手势变化,跟踪另一个动画。而跟踪的用法也很简单,只需要指定toValue到另一个Animated.Value就可以了。交互动画需要跟踪用户的手势操作,Animated也很贴心地提供了事件借口的封装,如下:

//  Animated.event 封装手势事件等值映射到对应的ANimated.Value
        onPanResponderMove: Animated.event(
            [null, {dx: this.state.x, dy: this.state.y}]
        )

推荐阅读更多精彩内容