React Native入门(十四)之动画(1)Animated详解

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

前言

在APP的开发中,流畅合理的动画能大大提高用户体验,Android和iOS原生都有对应的动画系统,同样的在RN中也有用于创建动画的API,就是Animated。Animated库使得开发者可以非常容易地实现各种各样的动画和交互方式,并且具备极高的性能。

基本介绍

组件类型

我们想要文本,图片等可以进行动画,就需要使用Animated进行封装
所以创建动画组件有以下5种:

  • Animated.View
  • Animated.Text
  • Animated.Image
  • Animated.ScrollView
  • 封装自定义动画组件:Animated.createAnimatedComponent()

其中前4个使用的时候,只需使用<Animated.xxx>封装即可,之后与普通组件没有区别!第5个封装自定义的动画组件,一般来说使用的不多!

动画类型

Animated提供了3种动画类型,每种动画类型都提供了特定的函数曲线,用于控制动画值从初始值变化到最终值的变化过程:

  • Animated.decay():以指定的初始速度开始变化,然后变化速度越来越慢直至停下。
  • Animated.spring() :提供一个简单的弹性物理模型。
  • Animated.timing() :驱动一个Value使用 easing函数随着时间的变化。这个类型我们平时用的最多!默认情况下,它采用了对称easeInOut 曲线传达一个对象的逐渐加速到全速,最后逐渐减速到停止。

值类型

Animated的值类型有两种:

  • AnimatedValue:单个值
  • AnimatedValueXY:向量值,二维平面

从字面上也能看出,AnimatedValue就是一个单一的值,而AnimatedValueXY则是XY两个方向的值!

动画配置

举个例子,这里以timing()类型为例说一下动画的配置:

Animated.timing(
this.state.xPosition,
{
toValue: 100,
easing: Easing.back,
duration: 2000,
}
).start();

这里提一下,有关Animated基本上所有相关的API都在AnimatedImplementation.js这个文件中,路径在
node_modules/react-native/Libraries/Animated/src/AnimatedImplementation.js。我们可以看一下timing函数!

const timing = function(
value: AnimatedValue | AnimatedValueXY,
config: TimingAnimationConfig,
)
: CompositeAnimation {

......
}

这个函数接收两个参数,第一个参数value,就是上边值类型中所说的两个值类型,第一个参数config,是一个TimingAnimationConfig类型的参数,用来具体的动画配置!
比如例子中toValue到达的值,duration动画持续时间(毫秒),默认值为500。easing设置easing函数类型!关于easing函数类型下边再说!

那么我们来具体看一下TimingAnimationConfig都可以设置哪些参数?
它的源码在TimingAnimation.js

export type TimingAnimationConfig = AnimationConfig & {
toValue: number | AnimatedValue | {x: number, y: number} | AnimatedValueXY,
easing?: (value: number) => number,
duration?: number,
delay?: number,
};

我们可以看到它是在AnimationConfig的基础上又添加了4个属性,例子中写了3个,还有一个delay,从字面上都可以看出是来设置动画延迟执行的时间(毫秒),默认为0。!
下边来看一下AnimationConfig,它在Animation.js中:

export type AnimationConfig = {
isInteraction?: boolean,
useNativeDriver?: boolean,
onComplete?: ?EndCallback,
iterations?: number,
}
;

isInteraction:是否一起同步运行。这个属性可用于优化动画卡顿,将逻辑计算工作放在UI动画完成后执行,从而来保证动画的流畅度!这块我们放在后边UI优化的时候再具体深入的了解!可以参考InteractionManager!
useNativeDriver:是否使用原生驱动,默认为false不开启!
onComplete:设置动画完成时的回调
iterations:设置动画运行迭代次数

最后的最后,别忘了调用start()方法来启动动画!!!!

Easing函数

上边配置中easing用来设置一个Easing函数类型,它的作用是来设置我们要动画改变的value要以什么状态来改变,是加速改变,还是减速改变,还是是弹性改变等等!
我们还是来看一下源码,它都可以设置哪些状态!
路径在`node_modules/react-native/Libraries/Animated/src/Easing.js

还记得上边设置中easing属性要接收的类型吗?
easing?: (value: number) => number,可以看出接收的是一个函数,将一个value传入,并返回一个改变后的value!而Easing这个类就是干这个的,我们可以随便找一个看一下,比如例子中Easing.back

static back(s: number): (t: number) => number {
if (s === undefined) {
s = 1.70158;
}
return (t) => t * t * ((s + 1) * t - s);
}

我们可以看出Easing这个类中的每一个函数都是(value: number) => number类型的,所以我们才可以easing : Easing.back这样写!
也就是说官方给我们写好了一系列的easing 供我们选择使用,而不需要我们自己再写了!当然,如果我们想自定义value的变化形式的话,那就要自己写了!

那么我们想要使用哪一种变化类型,就去Easing中选择就可以了,这里就不再一一列举了!

组合动画

我们可以将多个动画效果组合在一起进行动画,RN提供了四种组合的动画的API:

  • Animated.delay(time: number)
    starts an animation after a given delay.设置动画延迟执行。(相当于timing动画类型中的delay属性)。
  • Animated.parallel(animations: Array<CompositeAnimation>,
    config?: ?ParallelConfig,)

    starts a number of animations at the same time.同时执行一组动画。

    ParallelConfig里边只有一个属性stopTogether,是否一起停止默认为true,如果任何一个动画被停止或中断了,组内所有其它的动画也会被停止。如果想要禁止自动停止,设置为false!

  • Animated.sequence(animations: Array<CompositeAnimation>)
    starts the animations in order, waiting for each to complete before starting the next.按照顺序执行动画,等到上一个动画完成之后才能启动下一个动画。如果当前的动画被中止,后面的动画则不会继续执行。
  • Animated.stagger(time: number,
    animations: Array<CompositeAnimation>)

    starts animations in order and in parallel, but with successive delays.顺序且并行启动动画,但随着连续的延误。

合成动画值

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

  • Animated.add():将两个动画值相加计算,得出一个新的动画值。
  • Animated.divide():将两个动画值相除计算,得出一个新的动画值。
  • Animated.modulo():将两个动画值做取模(取余数)计算,得出一个新的动画值。
  • Animated.multiply();将两个动画值相乘计算,得出一个新的动画值。

举个官网的例子,转换缩放比例(2X—>0.5X):

const a = new Animated.Value(1);
const b = Animated.divide(1, a);

Animated.spring(a, {
toValue: 2,
}).start();

手势和事件输入

Animated.event是Animated API中与输入有关的部分,允许手势或其它事件直接绑定到动态值上。它通过一个结构化的映射语法来完成,使得复杂事件对象中的值可以被正确的解开。
我们先来看一下event()方法的样子,源码同样在AnimatedImplementation.js

const event = function(argMapping: Array<?Mapping>, config?: EventConfig): any {
const animatedEvent = new AnimatedEvent(argMapping, config);
if (animatedEvent.__isNative) {
return animatedEvent;
} else {
return animatedEvent.__getHandler();
}
};

可以看到event()函数接收两个参数,第一个参数接受一个映射的数组,对应的解开每个值,然后调用所有对应的输出的setValue方法。第二个参数是可选的,接收一个EventConfig,我们看一下具体可以设置什么?源码在AnimatedEvent.js中:

export type EventConfig = {
listener?: ?Function,
useNativeDriver?: boolean,
}
;

可以看到能够设置两个属性,第一个设置一个监听函数,第二个设置是否使用原生驱动。

举个例子,列表滑动:

onScroll={Animated.event(
[{nativeEvent: {contentOffset: {x: scrollX}}}],
// scrollX = e.nativeEvent.contentOffset.x
{listener}// 可选的异步监听函数
)}

上边scrollX被映射到了event.nativeEvent.contentOffset.x(event通常是PanResponder回调函数的第一个参数),而且设置了一个异步的监听函数!

关于PanResponder手势系统,我们在之后再做深入了解!本篇文章只了解动画相关的内容!

AnimatedValue和AnimatedValueXY

AnimatedValue

用于驱动动画的标准值。一个Animated.Value可以用一种同步的方式驱动多个属性,但同时只能被一个行为所驱动。启用一个新的行为(譬如开始一个新的动画,或者运行setValue)会停止任何之前的动作。
下边我只列表几个比较常用的方法:

  • constructor(value) 构造器
  • setValue(value) 直接设置值,会导致动画终止,然后更新所有绑定的属性。
  • setOffset(offset) 设置当前的偏移量。常用来在拖动操作一开始的时候用来记录一个修正值(譬如当前手指位置和View位置)。
  • flattenOffset() 将偏移量合并到最初值中,并把偏移量设为0。最终输出的值不会变化。常在拖动操作结束后调用。
  • addListener(callback) 增加一个异步的动画监听者,监听动画值的变更。
  • stopAnimation(callback?)终止动画,并在动画结束后执行callback,参数是动画结束后的最终值,这个值可能会用于同步更新状态与动画位置。
  • interpolate(config) 插值,在更新可动画属性前用插值函数对当前值进行变换

插值函数interpolate

我们先来看一下interpolate函数:

interpolate(config: InterpolationConfigType): AnimatedInterpolation {
return new AnimatedInterpolation(this, config);
}

可以看出它的全部实现都是在AnimatedInterpolation.js中!
函数接收一个InterpolationConfigType类型参数:

export type InterpolationConfigType = {
inputRange: Array<number>,
/* $FlowFixMe(>=0.38.0 site=react_native_fb,react_native_oss) - Flow error
* detected during the deployment of v0.38.0. To see the error, remove this
* comment and run flow
*/

outputRange: Array<number> | Array<string>,
easing?: (input: number) => number,
extrapolate?: ExtrapolateType,
extrapolateLeft?: ExtrapolateType,
extrapolateRight?: ExtrapolateType,
};

interpolate可以接受一个输入区间,然后将其映射到另一个的输出区间。
比如一个最简单的例子:

value.interpolate({
inputRange: [0, 1],
outputRange: [0, 100],
});

outputRange参数还可以接收字符串数组,常用于角度旋转时的转换

value.interpolate({
inputRange: [0, 360],
outputRange: ['0deg', '360deg']
})

easing参数应该不陌生了吧,在上边已经提到过了,就是来设置变化的类型!

剩下的3个参数extrapolateextrapolateLeftextrapolateRight属性可以用来限制输出区间outputRange。我们在上边可以看到接收的值类型是ExtrapolateType
ExtrapolateType的取值有3个:

type ExtrapolateType = 'extend' | 'identity' | 'clamp';

默认值是extend,表示允许超出。如果不允许超出输出区间,可以设置clamp

响应当前的动画值

(这里就摘取官网的翻译吧!)
你可能会注意到这里没有一个明显的方法来在动画的过程中读取当前的值——这是出于优化的角度考虑,有些值只有在原生代码运行阶段中才知道。如果你需要在JavaScript中响应当前的值,有两种可能的办法:

  • spring.stopAnimation(callback)会停止动画并且把最终的值作为参数传递给回调函数callback——这在处理手势动画的时候非常有用。
  • spring.addListener(callback) 会在动画的执行过程中持续异步调用callback回调函数,提供一个最近的值作为参数。这在用于触发状态切换的时候非常有用,譬如当用户拖拽一个东西靠近的时候弹出一个新的气泡选项。不过这个状态切换可能并不会十分灵敏,因为它不像许多连续手势操作(如旋转)那样在60fps下运行。

AnimatedValueXY

用来驱动2D动画的2D值,譬如滑动操作等。API和普通的Animated.Value几乎一样,只不过是个复合结构。它实际上包含两个普通的Animated.Value。

相关的API这里就不再详细说了,需要了解的可以去查看官方文档!

使用原生驱动

是否开启使用原生驱动,useNativeDriver这个属性,我们在上边也已经提到过,但是它具体是干嘛的呢?

Animated的API是可序列化的(即可转化为字符串表达以便通信或存储)。
通过启用原生驱动,我们在启动动画前就把其所有配置信息都发送到原生端,利用原生代码在UI线程执行动画,而不用每一帧都在两端间来回沟通。 如此一来,动画一开始就完全脱离了JS线程,因此此时即便JS线程被卡住,也不会影响到动画了。
所以需要开启使用原生驱动的话,很简单,配置useNativeDriver:true即可!

需要注意的问题
①动画值在不同的驱动方式之间是不能兼容的。因此如果你在某个动画中启用了原生驱动,那么所有和此动画依赖相同动画值的其他动画也必须启用原生驱动。
②还有官方文档中最后一句话,需要注意:
(翻译)本地驱动程序目前不支持Animated所能做的一切。 主要限制是您只能对非布局属性进行动画处理:像transform和opacity这样的东西可以工作,但是flexbox和position属性不会。 使用Animated.event时,只能使用直接事件而不冒泡事件。 这意味着它不适用于PanResponder,但可以处理像ScrollView#onScroll这样的东西。

结语

关于React Native中Animated相关的内容就先了解到这里,应该涵盖了几乎Animated所有相关的知识点。
好了,先这样,我们下篇再见!

参考文章
[React Native]动画-Animated
React Native开发之动画(Animations)
ReactNative Animated动画详解