react native学习笔记29——动画篇 Animated高级动画

时间:2022-09-04 10:01:55

1.前言

上一节我们学习了全局的布局动画api——LayoutAnimation,体验到其流畅柔和的动画效果,但有时我们需要实现一些更精细化的动画,或者完成一些组合动画,这时我们可以使用React Native提供的另一个高级动画api——Animated。
Animated使得我们可以非常容易地实现各种各样的动画和交互方式,并且具备极高的性能。Animated旨在以声明的形式来定义动画的输入与输出,通常只需要关注设置动画的开始和结束即可,在其中建立一个可配置的变化函数,然后使用start/stop方法来控制动画按顺序执行。
下面通过一个简单的淡入动画逐步展开介绍Animated的具体使用,还是基于上一节的LayoutAnimation的使用实例修改。

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

export default class AnimatedAnimationDemo extends Component {
    constructor(props) {
        super(props);
        // 初始状态
        this.state = {
            fadeInOpacity: new Animated.Value(0),
        };
        this._onPress = this._onPress.bind(this);
    }

    _onPress() {
        Animated.timing(this.state.fadeInOpacity, {
            toValue: 1,
            duration: 2000,
            easing: Easing.linear,// 线性的渐变函数
        }).start();
    }

    render() {
        return (
            <View style={styles.container}> <Animated.View // 可选的基本组件类型: Image,Text,ScrollView,View(可以包裹任意子View) style={[styles.content, {opacity: this.state.fadeInOpacity,}]}> <Text style={[{textAlign: 'center'}]}>Hello World!</Text> </Animated.View> <TouchableOpacity style={styles.content} onPress={this._onPress}> <View style={styles.button}> <Text style={styles.buttonText}>Press me!</Text> </View> </TouchableOpacity> </View> ); } } const styles = StyleSheet.create({ container: { marginTop:25, flex: 1, }, content: { backgroundColor: 'rgba(200, 230, 255, 0.8)', marginBottom:10, justifyContent:"center", alignSelf:"center", }, button: Platform.select({ ios: {}, android: { elevation: 4, // Material design blue from https://material.google.com/style/color.html#color-color-palette backgroundColor: '#2196F3', borderRadius: 2, width:100, height:30, }, justifyContent:"center", alignSelf:"center", }), buttonText: { alignSelf:"center", } });

效果如下:
react native学习笔记29——动画篇 Animated高级动画

2.基本Animated动画的步骤

上例是一个基本的Animated动画的实现,其步骤可以分为以下几步:
1.使用Animated.Value设定一个或多个初始值,包括透明度、位置等。
2.使用基本的Animated封装的组件,如Animated.ViewAnimated.TextAnimated.ImageAnimated.ScrollView
3.将初始值绑定到动画目标属性上。
4.通过Animated.timeing等方法设定动画参数。
5.调用start控制动画的启动。

2.1值类型

Animated提供两种值类型

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

Animated.Value可以绑定到样式或是其他属性上,也可以进行插值运算。单个Animated.Value可以用在任意多个属性上。多数情况下,Animated.Value可以满足需求(上面的示例),但有些情况下我们可能会需要AnimatedValueXY。例如:需要某一组件沿着X轴和Y轴交叉方向,向右下移动一段距离。
上例中通过

  constructor(props) {
      super(props);
      this.state = {
          fadeOutOpacity: new Animated.Value(0),
      };
      ...
  }

设置了组件透明度的初始值。

2.2支持的组件类型

支持Animated的组件类型:Animated.ViewAnimated.TextAnimated.ImageAnimated.ScrollView
你也可以使用Animated.createAnimatedComponent()来封装你自己的组件。不过该方法较少使用, 通常可以通过View包裹其他任意组件达到同样的效果。
上例中是对Animated.View组件进行动画设置。

2.3将初始值绑定到动画目标属性上

将动画绑定在<Animate.View /> 上,把实例化的动画初始值传入 style 中:

  <Animated.View style={[styles.content, {opacity: this.state.fadeInOpacity,}]}>
            ...
  </Animated.View>

2.4配置动画并启动

通过Animated.timeing等方法设定动画参数,调用start控制动画的启动。

     Animated.timing(this.state.fadeInOpacity, {
            toValue: 1,
            duration: 2000,
            easing: Easing.linear,// 线性的渐变函数
        }).start();

以上便是一个简单Animated动画的实现。

2.5动画类型

上例中使用了Animated.timing方法基于时间配置实现渐变动画,Animated共提供以下三种动画类型:

  • spring:基础的弹跳物理模型动画
  • timing:带有时间的渐变动画
  • decay:以一个初始速度开始并且逐渐减慢停止的动画

这三个动画配置api是Animated的核心api, 具体定义如下:

  • static decay(value, config)
  • static timing(value, config)
  • static spring(value, config)

创建动画的参数:
@value:AnimatedValue | AnimatedValueXY(X轴或Y轴 | X轴和Y轴)
@config:SpringAnimationConfig | TimingAnimationConfig | DecayAnimationConfig(动画的参数配置)

分别列出各config的特性参数:

  • TimingAnimationConfig动画配置选项定义如下:
type TimingAnimationConfig = AnimationConfig & {
  toValue: number | AnimatedValue | {x: number, y: number} | AnimatedValueXY,
  easing?: (value: number) => number,  //缓动函数,默认 Easing.inOut(Easing.ease).
  duration?: number,  //动画时长,单位毫秒,默认500
  delay?: number,  //动画执行延迟时间,单位:毫秒,默认为0
};
  • DecayAnimationConfig动画配置选项定义如下:
type DecayAnimationConfig = AnimationConfig & {
  velocity: number | {x: number, y: number},  //初始速度,必须要填写
  deceleration?: number,  //速度减小的比例,加速度。默认为0.997
};
  • SpringAnimationConfig动画配置选项定义如下:
type SpringAnimationConfig = AnimationConfig & {
  toValue: number | AnimatedValue | {x: number, y: number} | AnimatedValueXY,
  overshootClamping?: bool,
  restDisplacementThreshold?: number,
  restSpeedThreshold?: number,
  velocity?: number | {x: number, y: number},   //初始速度,默认0
  bounciness?: number,      //反弹系数,默认8
  speed?: number,           //速度,默认12
  tension?: number,         //张力系数,默认7
  friction?: number,        //摩擦系数,默认40
};

注:只能定义其中一组:bounciness/speedtension/friction

下面通过逐步扩展丰富上面的例子,介绍Animated的更多特性。
先回到前面值类型AnimatedValueXY,我们如何实现向右下移动一段距离的动画。请看下例:

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

export default class AnimatedAnimationDemo2 extends Component {
    constructor(props) {
        super(props);
        this.state = {
            translateValue: new Animated.ValueXY({x:0, y:0}), // 二维坐标
        };
        this._onPress = this._onPress.bind(this);
    }

    _onPress() {
        this.state.translateValue.setValue({x:0, y:0});
        Animated.decay( // 以一个初始速度开始并且逐渐减慢停止。 S=vt-(at^2)/2 v=v - at
            this.state.translateValue,
            {
                velocity: 7, // 起始速度,必填参数。
                deceleration: 0.1, // 速度衰减比例,默认为0.997。
            }
        ).start();
    }

    render() {
        return (
            <View style={styles.container}>
                <Animated.View style={[styles.content, {transform: [
                    {translateX: this.state.translateValue.x}, // x轴移动
                    {translateY: this.state.translateValue.y}, // y轴移动
                ]}]}>
                    <Text style={[{textAlign: 'center'}]}>Hello World!</Text>
                </Animated.View>
                <TouchableOpacity style={styles.content} onPress={this._onPress}>
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>Press me!</Text>
                    </View>
                </TouchableOpacity>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        marginTop:25,
        flex: 1,
    },
    content: {
        backgroundColor: 'rgba(200, 230, 255, 0.8)',
        marginBottom:10,
        justifyContent:"center",
        alignSelf:"center",
    },
    button: Platform.select({
        ios: {},
        android: {
            elevation: 4,
            // Material design blue from https://material.google.com/style/color.html#color-color-palette
            backgroundColor: '#2196F3',
            borderRadius: 2,
            width:100,
            height:30,
        },
        justifyContent:"center",
        alignSelf:"center",
    }),
    buttonText: {
        alignSelf:"center",
    }
});

通过new Animated.ValueXY({x:0, y:0})设置了在xy轴上的初始坐标。
其中,组件Animated.View的属性transform是一个变换数组,常用的有scale, scaleX, scaleY, translateX, translateY, rotate, rotateX, rotateY, rotateZ,其使用方式可以如下:

transform: [  // scale, scaleX, scaleY, translateX, translateY, rotate, rotateX, rotateY, rotateZ
    {scale: this.state.bounceValue},  // 缩放
    {rotate: this.state.rotateValue.interpolate({ // 旋转,使用插值函数做值映射
        inputRange: [0, 1],
        outputRange: ['0deg', '360deg']})},
    {translateX: this.state.translateValue.x}, // x轴移动
    {translateY: this.state.translateValue.y}, // y轴移动
],
]

3插值函数interpolate

在transform的使用示例中使用了interpolate插值函数。这个函数实现了数值大小、单位的映射转换,允许一个输入的区间范围映射到另外一个输入的区间范围。比如:将0-1数值转换为0deg-360deg角度,旋转View时:

this.state.rotateValue.interpolate({ // 旋转,使用插值函数做值映射 inputRange: [0, 1], outputRange: ['0deg', '360deg']})

具体的实例:
还是在上例的基础上,初始值改为:

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

<Animated.View />的style样式中的transform属性改为:

transform: [
              {scale: this.state.translateValue.interpolate({
                      inputRange: [0, 1],
                      outputRange: [1, 3],
              })},
              {translateX: this.state.translateValue.interpolate({
                      inputRange: [0, 1],
                      outputRange: [0, 300],
              })},
              {rotate: this.state.translateValue.interpolate({
                      inputRange: [0, 1],
                      outputRange: ['0deg', '720deg'],
              })},
           ]

动画配置改为:

    Animated.spring(this.state.translateValue, {
            toValue: 0,
            velocity: 7,
            tension: -20,
            friction: 3,
    }).start();

全部代码如下(篇幅原因,省略import与样式代码,同上例):

...
export default class AnimatedAnimationDemo2 extends Component {
    constructor(props) {
        super(props);
        this.state = {
            translateValue: new Animated.Value(1),
        };
        this._onPress = this._onPress.bind(this);
    }

    _onPress() {
        Animated.spring(this.state.translateValue, {
            toValue: 0,
            velocity: 7,
            tension: -20,
            friction: 3,
        }).start();
    }

    render() {
        return (
            <View style={styles.container}> <Animated.View style={[styles.content, {transform: [ {scale: this.state.translateValue.interpolate({ inputRange: [0, 1], outputRange: [1, 3], })}, {translateX: this.state.translateValue.interpolate({ inputRange: [0, 1], outputRange: [0, 300], })}, {rotate: this.state.translateValue.interpolate({ inputRange: [0, 1], outputRange: [ '0deg', '720deg' ], })}, ]}]}> <Text style={[{textAlign: 'center'}]}>Hello World!</Text> </Animated.View> <TouchableOpacity style={styles.content} onPress={this._onPress}> <View style={styles.button}> <Text style={styles.buttonText}>Press me!</Text> </View> </TouchableOpacity> </View> ); } } ...

效果如下:
react native学习笔记29——动画篇 Animated高级动画

4组合动画

Animated可以通过以下方法将多个动画组合起来执行:
* parallel:同时执行
* sequence:顺序执行
* stagger:错峰,其实就是插入了delay的parrllel
* delay:组合动画之间的延迟方法,严格来讲,不算是组合动画

4.1串行动画

下面用sequence演示一个顺序执行的串行动画。
还是在上例的基础上只用修改动画配置,在Animated.sequence中顺序执行Animated.springAnimated.delayAnimated.timing方法。

 _onPress() {
        Animated.sequence([
            Animated.spring(this.state.bounceValue,{toValue:1}),  
            Animated.delay(500),
            Animated.timing(this.state.rotateValue, {
                toValue: 1,
                duration: 800,
                easing: Easing.out(Easing.quad),
            })
        ]).start(() => this._onPress());
    }

设定初始值:

constructor(props) { super(props); this.state = { bounceValue: new Animated.Value(0), rotateValue: new Animated.Value(0), };
        this._onPress = this._onPress.bind(this);
    }

<Animated.View />的style中的transform属性改为:

    transform: [
                    {rotate: this.state.rotateValue.interpolate({
                        inputRange: [0, 1],
                        outputRange: [
                            '0deg', '360deg'
                        ],
                    })},
                    {
                        scale:this.state.bounceValue,
                    }
                ]

完整代码如下AnimatedAnimationDemo2 .js(省略了未修改代码):

...
export default class AnimatedAnimationDemo2 extends Component {
    constructor(props) {
        super(props);
        this.state = {
            bounceValue: new Animated.Value(0),
            rotateValue: new Animated.Value(0),
        };
        this._onPress = this._onPress.bind(this);
    }

    _onPress() {
        Animated.sequence([
            Animated.spring(this.state.bounceValue,{toValue:1}),
            Animated.delay(500),
            Animated.timing(this.state.rotateValue, {
                toValue: 1,
                duration: 800,
                easing: Easing.out(Easing.quad),
            })
        ]).start(() => this._onPress());
    }

    render() {
        return (
            <View style={styles.container}>
                <Animated.View style={[styles.content, {transform: [
                    {rotate: this.state.rotateValue.interpolate({
                        inputRange: [0, 1],
                        outputRange: [
                            '0deg', '360deg'
                        ],
                    })},
                    {
                        scale:this.state.bounceValue,
                    }
                ]}]}>
                    <Text style={[{textAlign: 'center'}]}>Hello World!</Text>
                </Animated.View>
                <TouchableOpacity style={styles.content} onPress={this._onPress}>
                    <View style={styles.button}>
                        <Text style={styles.buttonText}>Press me!</Text>
                    </View>
                </TouchableOpacity>
            </View>
        );
    }
}
...

效果如下:先拉伸,延迟500毫秒后再旋转
react native学习笔记29——动画篇 Animated高级动画

4.2并行动画

说完串行动画,很自然的我们会想到并行动画。并行动画主要通过Animated.parallel方法将多个动画并行同时执行。可修改上述的_onPress方法:

_onPress() {
        Animated.parallel([
            Animated.spring(this.state.bounceValue, {
                toValue: 1,
            }),
            Animated.timing(this.state.rotateValue, {
                toValue: 1,
                easing: Easing.elastic(1),
            })
        ]).start();
    }

效果如下:拉伸同时旋转
react native学习笔记29——动画篇 Animated高级动画

5动画循环

Animated的start方法可以接受一个回调函数,在动画或某个流程结束的时候执行,通过该方法监听动画的结束在回调函数中再次执行上例中的_onPress即可重复执行动画:

_onPress() {
        this.state.translateValue.setValue(0);
        Animated.timing(this.state.translateValue, {
            toValue: 1,
            duration: 800,
            easing: Easing.linear
        }).start(() => this._onPress());
    }

<Animated.View />中style的transform属性改为:

transform: [
                    {rotate: this.state.translateValue.interpolate({
                        inputRange: [0, 1],
                        outputRange: [
                            '0deg', '360deg'
                        ],
                    })},
                ]

实现如下循环动画效果:
react native学习笔记29——动画篇 Animated高级动画

6追踪动态值

React Native动画支持跟踪功能,只需要将toValue设置成另一个动态值而不是一个普通数字即可,比如可以通过Animated.timing设置duration:0来实现快速跟随的效果。它们还可以使用插值来进行组合:

Animated.spring(follower, {toValue: leader}).start();
Animated.timing(opacity, {
  toValue: pan.x.interpolate({
    inputRange: [0, 300],
    outputRange: [1, 0],
  }),
}).start();

7手势控制动画

除了上述自发进行的动画外,有时候我们需要根据Scroll或者手势来手动的控制动画的过程。Animated.event是实现手势控制动画的关键,允许手势或其它事件直接绑定到动态值上。这里的Aniamted.event的输入是一个数组,用来做数据绑定 。
在ScrollView中:
我们把event.nativeEvent.contentOffset.x的值赋值到scrollX变量中(event一般为回调方法的第一个参数):

onScroll={Animated.event(
  [{nativeEvent: {contentOffset: {x: scrollX}}}]   //把contentOffset.x绑定给this.state.xOffset
)}

在Pan手势中:
把手势状态的gestureState.dxgestureState.dy的值赋值到pan.x何pan.y变量中(gestureState通常为PanResponder回调方法中的第二个参数):

onPanResponderMove={Animated.event([
  null,                                          // 忽略原生事件
  {dx: pan.x, dy: pan.y}                         // 从gestureState中解析出dx和dy的值
]);

7.1Scroll驱动

目标效果如下:随着ScrollView的向左滑动,最左边的一个Image透明度逐渐降低为0。
react native学习笔记29——动画篇 Animated高级动画

实例代码如下:

//AnimatedScrollDemo.js
import React, { Component } from 'react';
import {
    StyleSheet,
    Text,
    View,
    TouchableOpacity,
    Platform,
    Image,
    Animated,
    Easing,
    ScrollView
} from 'react-native';

let deviceHeight = require('Dimensions').get('window').height;
let deviceWidth = require('Dimensions').get('window').width;
export default class AnimatedScrollDemo extends React.Component {
    state: {
        xOffset: Animated,
    };
    constructor(props) {
        super(props);
        this.state = {
            xOffset: new Animated.Value(1.0)
        };
    }
    render() {
        return (
            <View style={styles.container}>
                <ScrollView horizontal={true} //水平滑动
                            showsHorizontalScrollIndicator={false}
                            style={{width:deviceWidth,height:deviceHeight}}//设置大小
                            onScroll={Animated.event(
                                [{nativeEvent: {contentOffset: {x: this.state.xOffset}}}]//把contentOffset.x绑定给this.state.xOffset
                            )}
                            scrollEventThrottle={100}//onScroll回调间隔
                >
                    <Animated.Image source={require('../images/1.jpg')}
                                    style={{height:deviceHeight,
                                        width:deviceWidth,
                                        opacity:this.state.xOffset.interpolate({//映射到0.0,1.0之间
                                            inputRange: [0,375],
                                            outputRange: [1.0, 0.0]
                                        }),}}
                                    resizeMode="cover"
                    />
                    <Image source={require('../images/2.jpg')} style={{height:deviceHeight, width:deviceWidth}} resizeMode="cover" />

                </ScrollView>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        marginTop:25,
        flex: 1,
    },
});

7.2手势驱动

React Native最常用的手势就是PanResponser,由于本文侧重讲解动画,所以不会详细介绍PanResponser,仅仅介绍用到的几个属性和回调方法。关于手势的详细介绍会在后面的文章中介绍。

onStartShouldSetPanResponder: (event, gestureState) => {}//是否相应pan手势
onPanResponderMove: (event, gestureState) => {}//在pan移动的时候进行的回调
onPanResponderRelease: (event, gestureState) => {}//手离开屏幕
onPanResponderTerminate: (event, gestureState) => {}//手势中断

这些方法中都需要输入event与gestureState参数。
* 通过event可以获得触摸的位置,时间戳等信息。
* 通过gestureState可以获取移动的距离,速度等。

目标效果如下:View随着手拖动而移动,手指离开会到原点。
react native学习笔记29——动画篇 Animated高级动画

实例代码:

//AnimatedGestureDemo.js
import React, { Component } from 'react';
import {
    StyleSheet,
    Text,
    View,
    TouchableOpacity,
    Platform,
    Image,
    Animated,
    Easing,
    PanResponder
} from 'react-native';

export default class AnimatedGestureDemo extends Component {
    state:{
        trans:AnimatedValueXY,
    }
    _panResponder:PanResponder;
    constructor(props) {
        super(props);
        this.state = {
            trans: new Animated.ValueXY(),
        };
        this._panResponder = PanResponder.create({
            onStartShouldSetPanResponder: () => true, //响应手势
            onPanResponderMove: Animated.event(
                [null, {dx: this.state.trans.x, dy:this.state.trans.y}] // 绑定动画值
            ),
            onPanResponderRelease: ()=>{//手松开,回到原始位置
                Animated.spring(this.state.trans,{toValue: {x: 0, y: 0}}
                ).start();
            },
            onPanResponderTerminate:()=>{//手势中断,回到原始位置
                Animated.spring(this.state.trans,{toValue: {x: 0, y: 0}}
                ).start();
            },
        });
    }
    render() {
        return (
            <View style={styles.container}>
                <Animated.View style={{width:80,
                    height:80,
                    borderRadius:40,
                    backgroundColor:'blue',
                    transform:[
                        {translateY:this.state.trans.y},
                        {translateX:this.state.trans.x},
                    ],
                }}
                  {...this._panResponder.panHandlers}
                >
                </Animated.View>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        marginTop:25,
        flex: 1,
    },
});

监听当前的动画值

  • addListener(callback):动画执行过程中的值
  • stopAnimation(callback):动画执行结束时的值
    监听AnimatedValueXY类型translateValue的值变化:
this.state.translateValue.addListener((value) => {   
    console.log("translateValue=>x:" + value.x + " y:" + value.y);
});
this.state.translateValue.stopAnimation((value) => {   
    console.log("translateValue=>x:" + value.x + " y:" + value.y);
});

监听AnimatedValue类型translateValue的值变化:

this.state.translateValue.addListener((state) => { console.log("rotateValue=>" + state.value); }); this.state.translateValue.stopAnimation((state) => { console.log("rotateValue=>" + state.value); });

8小结

今天介绍了Animated的常用方法,并列举了相关的代码示例,例子比较多,对于有些效果有所省略,建议读者多动手尝试下每个效果,举一反三,加深理解。