React Native中Animated库的使用

1,736 阅读7分钟
在移动客户端开发中流畅、有意义的动画是非常重要的,现实生活中的物体在开始移动和停下来的时候都具有一定的惯性,我们在界面中也可以使用动画来实现契合物理规律的交互。

官方在开发RN的时候,提供了两个互补的动画系统:用于创建精细的交互控制的动画Animated和用于全局的布局动画LayoutAnimation。

本篇文章主要讲Animated动画库的使用

执行一个动画最基本的原理就是调用动画函数改变一个变量值,而这个变量值绑定到控件的属性上,比如透明的、位置等。通过动画函数改变值从而使控件动起来。所以最基本的动画要有2个属性

  • 变量值(Animated.Value和Animated.ValueXY)区别一个是单个值,一个是矢量值(可以理解为二维坐标值)
  • 动画类型:timing、spring、decay

先简单看个例子

const App = () => {
  const scaleAnim = useRef(new Animated.Value(0)).current;

  const fadeIn = () => {
    // Will change fadeAnim value to 1 in 2 seconds
    Animated.timing(scaleAnim, {
      toValue: 1,
      duration: 2000,
    }).start();
  };

  const fadeOut = () => {
    // Will change fadeAnim value to 0 in 2 seconds
    Animated.timing(scaleAnim, {
      toValue: 0,
      duration: 2000,
    }).start();
  };

  return (
    <View style={styles.container}>
      <Animated.View
        style={[
          styles.fadingContainer,
          {
            transform: [
              {
                scale: scaleAnim,
              },
            ],
          },
        ]}
      />
      <View style={styles.buttonRow}>
        <Button title="Fade In" onPress={fadeIn} />
        <Button title="Fade Out" onPress={fadeOut} />
      </View>
    </View>
  );
};

要想写一个动画需要以下几步:

  1. 使用基本的Animated组件,如Animated.View、Animated.Image、Animated.Text和其他(使用AnimatedImplementation来包装);
  2. 使用Animated.Value设定一个或多个初始化值,如位置属性、透明属性、角度属性等;
  3. 将初始化值绑定到动画目标的属性上,如style;
  4. 通过动画类型Api设定动画参数,如spring、decay、timing三种动画类型;
  5. 调用start启动动画,同时可以在start里面回调相关功能;

动画类型 timing、spring、decay

  1. Animated.decay() 以指定的初始速度开始变化,然后变化速度越来越慢直至停下。

decay.gif

decay 动画参数 
export type DecayAnimationConfigSingle = AnimationConfig & {
    velocity: number,         // 起始速度,必填参数
    deceleration?: number,    // 速度衰减比例,默认为0.997
};

decay典型的使用场景是某个以组件一定的速度运行并不断减速,比如,移动(swipe或者fling)一张卡片。手指让卡片滑动并具有速度,最后卡片会因为阻力减速,并最终停止。 一般都是配合手势一起用

  1. Animated.spring()提供了一个简单的弹簧物理模型.

spring.gif

export type SpringAnimationConfig = AnimationConfig & {
    toValue: number | AnimatedValue | {x: number, y: number} | AnimatedValueXY,
    overshootClamping?: boolean,                    // 布尔表示弹簧是否应夹紧而不是弹跳,默认为false。
    restDisplacementThreshold?: number,             // 静止位移的阈值,低于该阈值时应考虑弹簧处于静止状态。默认值为0.001。
    restSpeedThreshold?: number,                    // 静止时应考虑弹簧的速度,以每秒像素为单位。默认值为0.001。
    velocity?: number | {x: number, y: number},     // 附着在弹簧上的物体的初始速度。默认值为0(对象处于静止状态)
    delay?: number,                                 // 延迟(毫秒)后启动动画。默认值为0。

    bounciness?: number,                            // 控制弹性,默认8。
    speed?: number,                                 // 控制动画的速度。默认12。

    tension?: number,                               // 张力,默认40
    friction?: number,                              // 摩擦力,默认为7.

    stiffness?: number,                             // 弹簧刚度系数,默认100。
    damping?: number,                               // 定义了由于摩擦力如何阻尼弹簧的运动,默认10。
    mass?: number,                                  // 物体的质量附着在弹簧的末端,默认1。
};
不能同时定义 bounciness/speed、tension/friction 或 stiffness/damping/mass 这三组数据,只能指定其中一组,即三选一
  1. Animated.timing()使用easing 函数让数值随时间动起来。

timing.gif

export type TimingAnimationConfigSingle = AnimationConfig & {
    toValue: number | AnimatedValue,      //
    easing?: (value: number) => number,   // 缓动函数。 默认为Easing.inOut(Easing.ease)。
    duration?: number,                    // 动画的持续时间(毫秒)。默认值为 500.
    delay?: number,                       // 开始动画前的延迟时间(毫秒)。默认为 0.
};

以上三个都有两个共同的不常用的参数: isInteraction: 指定本动画是否在InteractionManager的队列中注册以影响其任务调度。默认值为 true。 useNativeDriver: 启用原生动画驱动。默认不启用(false)。
注意⚠️:启动原生动画驱动时,位移translateX这种动画属性时不能与其他动画属性一起使用。安卓机上会有问题

组合动画

parallel(同时执行)、sequence(顺序执行)、delay(延时执行)、``

1、使用sequence可以把多个动画组织起来,让他们依次顺序执行。

动画1 -> 动画2 -> 动画3 ->动画4 -> 动画5

sequen.gif

例子中有两个动画。一个控制透明度,另一个移动一定距离

const App = () => {
  const fadeAnim = useRef(new Animated.Value(0)).current;
  const translateAnim = useRef(new Animated.Value(0)).current;

  const fadeIn = () => {
    Animated.parallel([
    Animated.timing(fadeAnim, {
      toValue: 1,
      duration: 1000
    }),
    Animated.timing(translateAnim, {
      toValue: 50,
      duration: 1000
    }),
    ]).start();
  };
  return (
    <View style={styles.container}>
      <Animated.View
        style={[
          styles.fadingContainer,
          {
            opacity: fadeAnim, // Bind opacity to animated value
            transform: [
               {
                 translateX: translateAnim,
               }
            ]
          }
        ]}
      >
      </Animated.View>
      <View style={styles.buttonRow}>
        <Button title="Fade In" onPress={fadeIn} />
      </View>
    </View>
  );
}

2、使用parallel方法会让一组动画同时开始执行。这组动画里有一个停止了,其他的也全部都停止。不过你可以通过修改属性stopTogether来修改这个行为。

spall.gif

3、staggerparallel类似,也是使一组动画同时运行。但是,稍有一点不同的是这些动画之间开始时间依次会有一定的延迟。

stag.gif

const fadeIn = () => {
    Animated.stagger(4000,[
    Animated.timing(fadeAnim, {
      toValue: 1,
      duration: 1000
    }),
    Animated.timing(translateAnim, {
      toValue: 50,
      duration: 1000
    }),
    ]).start();
  };

3、Animated.delay的本质是它创建了一个什么都不干,只等待的Animated.timing。主要目的就是用来在stagger或者sequence里作为一组动画的一部分。

Animated.sequence([
    Animated.delay(300),
    Animated.timing(this._animatedValue, {
        toValue: 100,
        duration: 500
    })
]).start()

还可以作为参数使用

Animated.timing(this._animatedValue, {
    toValue: 100,
    delay: 300,
    duration: 500
}).start()

这二种方式动画效果一样

插值 interpolate

当我们动画的值与要改变的属性值不是同一单位的时候,就可以使用 interpolate 来帮我们进行一个单位的映射转换,插值的好处在于我们可以声明一个动画变量来控制多个并行动画,简单易控制。 例如:

transform: [
               {
                 rotateZ: translateAnim.interpolate({
                      inputRange: [0, 1],
                      outputRange:['0deg','60deg']
                    })
               }
            ]

inter.gif

常用方法

Animated.Value和Animated.ValueXY就是上面我们所说的变量值即动画执行过程中会不断的更改这个值Animated.Value提供了以下方法:

  • constructor:构造函数。new Animated.Value(0)
  • setValue:设置新的值,注意此方法会停止动画
  • setOffset:设置一个修正值,不论接下来的值是由setValue、一个动画,还是Animated.event产生的,都会加上这个值。常用来在拖动操作一开始的时候用来记录一个修正值(譬如当前手指位置和View位置)。
  • flattenOffset:该用来把相对值合并到值里,然后相对值设置为0,最终输出的值不会发生变化。常常在拖动操作结束以后调用。
  • addListener:异步监听动画值变化
  • removeListener:删除指定监听器
  • removeAllListeners:删除所有监听器
  • stopAnimation:停止动画,返回当前动画值。
  • interpolate:差值,可以将值映射成新的值,后面会具体介绍。

Animated.ValueXY其实是就是2个Animated.Value,方法和Animated.Value一样,不过比Animated.Value 多了2个方法:

  • getLayout:将一个{x, y} 组合转换为一个可用的位移变换(translation transform),例如:

    style={{
    transform: this.state.anim.getTranslateTransform()
    }}

  • getTranslateTransform:将一个{x, y} 组合转换为一个可用的位移变换(translation transform),例如:

    style={{
    transform: this.state.anim.getTranslateTransform()
    }}

import React, { useRef } from "react";
import { Animated, PanResponder, StyleSheet, View } from "react-native";

const DraggableView = () => {
  const pan = useRef(new Animated.ValueXY()).current;

  const panResponder = PanResponder.create({
    onStartShouldSetPanResponder: () => true,
    onPanResponderMove: Animated.event([
      null,
      {
        dx: pan.x, // x,y are Animated.Value
        dy: pan.y,
      },
    ]),
    onPanResponderRelease: () => {
      Animated.spring(
        pan, // Auto-multiplexed
        { toValue: { x: 0, y: 0 } } // Back to zero
      ).start();
    },
  });

  return (
    <View style={styles.container}>
      <Animated.View
        {...panResponder.panHandlers}
        style={[pan.getLayout(), styles.box]}
      />
    </View>
  );
};
style={[pan.getLayout(), styles.box]}
等同于:
style={[styles.box,{transform: [
    {
        translateX: pan.x
    },
    {
        translateY: pan.y
    }
]}]}

效果演示:

getlayout.gif

上面例子还使用了Animated.event方法,这个方法用来把一组数据匹配到动画需要的数据上,最后对匹配完成的数据调用setValue方法
PanResponder的例子:

 onPanResponderMove: Animated.event([
   null,                // raw event arg ignored
   {dx: this._pan.x, dy: this._pan.y},    // gestureState arg
 ]),

onPanResponderMove方法里,调用了Animated.event。参数是一个数组,里面有两个元素。我们忽略了第一个初始事件的值。第二个元素是gestureState,提供了很有用的值dxdy。这两个值就是用户从触碰屏幕开始手指移动的距离。

自定义动画createAnimatedComponent

默认的情况下Animated.ViewAnimated.TextAnimated.Image支持动画。有的时候,你需要其他的某些组件也变成动画组件,方法createAnimatedComponent可以完成这个任务。它会把的组件props或者state等属性和Animated.Value自动绑定起来。

比如ScrollView:

let AnimatedScrollView = Animated.createAnimatedComponent(ScrollView)