React-Native-动画

一、LayoutAnimation

当布局变化时,自动将视图运动到它们新的位置上。也就是在调用LayoutAnimation函数后,调用setState完成布局。它常用来更新flexbox布局,因为它可以无需测量或者计算特定属性就能直接产生动画,由于直接影响全局页面,所以可以比Animated更方便全局调整动画。
一个常用的调用此API的办法是调用LayoutAnimation.configureNext(config),然后调用setState

注意如果要在Android上使用此动画,则需要在代码中启用:

import { UIManager } from 'react-native';

UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true);

推荐将代码写到初始index.js中。

configureNext

配置下一次布局要发生的动画。
gif

static configureNext(config, onAnimationDidEnd?)

名称 类型 必填 说明
config object 如下
onAnimationDidEnd func 动画结束后的回调。目前仅 iOS 可用。

config:

duration: 动画持续时间,单位是毫秒。
create: 配置创建新视图时的动画。(为Anim类型)
update: 配置更新后的视图的动画。
delete: 配置删除后的视图的动画。
config对象可以直接手写配置,也可以使用create函数生成。

type Anim = {
// 动画时间
duration?: number,
// 延迟指定时间
delay?: number,
// 弹跳动画阻尼系数,配合spring
springDamping?: number,
// 初始速度
initialVelocity?: number,
// 动画效果
type?: $Enum<typeof TypeEnum>,
// 动画对应属性
property?: $Enum<typeof PropertiesEnum>
}

type对应于LayoutAnimation.Type:

spring:弹跳
linear:线性
easeInEaseOut:缓入缓出
easeIn:缓入
easeOut:缓出

property对应于LayoutAnimation.Properties:

opacity:透明度
scaleXY:缩放

代码实例:

LayoutAnimation.configureNext(
{
// 持续时间
duration: 1000,
// 创建视图
create: {
type: LayoutAnimation.Types.linear,
property: LayoutAnimation.Properties.scaleXY
},
// 更新视图
update: {
type: LayoutAnimation.Types.linear
}
},
() => {
console.log('-----');
}
);
this.setState({
width: 250,
height: 150
});

效果图如下:
gif

create

static create(duration, type, creationProp)
用来创建configureNext所需的 config 参数的辅助函数。

duration:动画持续时间。
type:create和update时的动画类型,定义在LayoutAnimation.Types
creationProp:create时的动画属性,定义在LayoutAnimation.Properties

代码实例(效果与上述一样):

LayoutAnimation.configureNext(
LayoutAnimation.create(
700,
LayoutAnimation.Types.linear,
LayoutAnimation.Properties.scaleXY
)
);
this.setState({
width: 250,
height: 150
});

Presets(快捷动画)

LayoutAnimation.Presets中,已经帮我们写好了几个简单的默认动画config:

easeInEaseOut: 淡入淡出
linear: 线性
spring: 弹性

源码如下:

const Presets = {
easeInEaseOut: (create(
300,
'easeInEaseOut',
'opacity',
): LayoutAnimationConfig),
linear: (create(500, 'linear', 'opacity'): LayoutAnimationConfig),
spring: {
duration: 700,
create: {
type: 'linear',
property: 'opacity',
},
update: {
type: 'spring',
springDamping: 0.4,
},
delete: {
type: 'linear',
property: 'opacity',
},
},
};

我们可以直接使用他来配置我们的config

LayoutAnimation.configureNext(LayoutAnimation.Presets.linear);
this.setState({
width: 250,
height: 150
});

可以看到LayoutAnimation在配置和使用上很简捷、方便,并且在全局控制方面也很方便。但是只能满足单个动画效果。面对组合动画如顺序动画,同步动画,则会很无力。并且在结束动画的回调时只有ios端可以接受通知。
因此在面对更复杂的动画效果时,就需要Animated来完成。

二、Animated

流畅、有意义的动画对于移动应用体验非常,相较于 LayoutAnimation ,Animated更具有强壮的API,使动画变得流畅,强大并易于构建和维护。

Animated仅关注动画的输入与输出声明,在其中建立一个可配置的变化函数,然后使用简单的start/stop方法来控制动画按顺序执行。

Value

Animated.Value是完成单个值连接动画组件和样式的函数。
Animated.ValueXY…..向量值…..。

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

配置好变量值后就可以添加至动画中。

函数

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

动画类型

Animated提供了三种动画类型。
Animated.xxx(绑定的Value, 配置的Config);

Animated.timing()

提供一种普通时间渐变类型。按照一个缓动曲线而随时间变化。Easing模块定义了一些曲线(https://reactnative.cn/docs/easing/),也可以使用自己的函数。

其中config如下:

duration: 动画的持续时间(毫秒)。默认值为 500.
easing: 函数。 默认为Easing.inOut(Easing.ease)。
delay: 开始动画前的延迟时间(毫秒)。默认为 0.
isInteraction: 指定本动画是否在InteractionManager的队列中注册以影响其任务调度。默认值为 true。
useNativeDriver: 启用原生动画驱动。默认不启用(false)。

Animated.spring()

提供一种简单的弹簧物理模型。
注意:不能同时定义 bounciness/speedtension/friction 或 ** stiffness/damping/mass。只能指定其中的一组。

其中config如下:

velocity: 附着在弹簧上的物体的初始速度。默认值0(对象处于静止状态)。
overshootClamping: 布尔值指示是否应夹紧弹簧而不反弹。默认为false。
restDisplacementThreshold: 静止位置的位移阈值,低于此阈值应考虑弹簧处于静止状态。默认值0.001。
restSpeedThreshold: 应该考虑弹簧静止的速度,以每秒像素为单位。默认值0.001。
delay: 延迟(毫秒)后开始播放动画。默认值0。
isInteraction: 指定本动画是否在InteractionManager的地下中注册以影响其任务调度。替换为true。
useNativeDriver: 启用原生动画驱动。暂时不启用(false)。

1 摩擦/拉伸或弹跳/速度 模型

它会在toValue值更新的同时跟踪当前的速度状态,以确保动画连贯。

其中config如下:

friction: 控制 bounciness。默认值7,值越小,弹性幅度越大。
tension: 控制速度。默认值40,值越小,越平缓。
speed: 控制动画的速度。默认值12, 值越大,速度越快。
bounciness: 控制弹动值。默认值8,值越大,弹性幅度越大。

2 刚度/阻尼/质量 模型

对弹簧动力学背后的物理学更加精确。

其中config如下:

stiffness: 弹簧刚度系数。默认值100, 值越大,弹性越大。
damping: 定义由于摩擦力应如何抑制弹簧的运动。默认值10,值越大,摩擦越强,回弹效果越小。
mass: 附着在弹簧末端的物体的质量。默认值1,值越大,震荡幅度越大,回弹速度越慢。

Animated.decay()

提供一种指定初始速度开始,然后开始减速的模型。

其中config如下:

velocity: 初始速度,必填
deceleration: 衰减系数。最小值为0.997。
isInteraction: 指定本动画是否在InteractionManager的队列中注册以影响其任务调度。默认值为 true。
useNativeDriver: 启用原生动画驱动。默认不启用(false)。

如没有特殊要求,一般都会使用timing来完成动画需求。

代码实例:

class LoginContainer extends React.Component {

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

componentDidMount() {
Animated.timing(this.state.opacity, {
toValue: 1.5,
duration: 100
});
}

render() {
return (
<Animated.View style={[..., {opacity: this.state.opacity}]}>
<View >

</View>
</Animated.View>
);
}
}

Easing函数

上面配置中的easing是用来设置Easing函数,它的作用是来设置我们要动画改变的value要以什么状态来改变,是加速改变,还是减速改变,还是是弹性改变等等。
Easing.js文件中有很多函数:

'use strict';
let ease;
class Easing {
static bezier(
x1: number,
y1: number,
x2: number,
y2: number,
): (t: number) => number {
const _bezier = require('./bezier');
return _bezier(x1, y1, x2, y2);
}

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

static linear(t: number): number {
return t;
}
...
}

可以看到这里面有许多y(t)这样的函数。

组合动画

在需要多个动画组合使用时,Animated提供了以下四种组合方式:(默认情况下,如果一个动画停止或中断,则组合中的所有其他动画也会停止)

Animated.delay(time): 在给定延迟后开始动画。
Animated.parallel(animations, config): 同时启动多个动画。config中stopTogether可以关闭同步停止效果。
Animated.sequence(animations): 按顺序启动动画,等待每一个动画完成后再开始下一个动画。
Animated.stagger(time, animations): 按照给定的延时间隔,顺序并行的启动动画。

代码示例:

Animated.parallel([
Animated.sequence([
Animated.timing()
])
])

自定义动画组件

绑定的Value必须绑定到指定的动画组件,才能生效,并且在一帧帧执行动画时避免 react 重新渲染和重新调和的开销。此外还得在组件卸载时做一些清理工作,使得这些组件在使用时是安全的。

createAnimatedComponent(): 用来处理组件,使其可以用于动画。

Animated中默认导出了以下这些可以直接使用的动画组件,当然它们都是通过使用上面这个方法进行了封装:

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

代码示例:

class AnimatedView extends React.Component {

render() {
.....
}

}

class AnimatedContainerView extend React.Componet {

const AView = Animated.createAnimatedComponent(AnimatedView);

render() {
return (
<AView />
);
}
}

开始动画

调用start()开启动画,在参数中接受完成动画回调的函数(成功回调finished为true)。如果由于某些原因停止动画,该回调函数也会回调。(失败则回调finished为false)

start((obj) => {
console.log(obj.finished);
})

启用原生动画驱动

使用原生动画,我们会在开始动画之前将所有关于动画的内容发送到原生代码,从而使用原生代码在 UI 线程上执行动画,而不是通过对每一帧的桥接去执行动画。一旦动画开始,JS 线程就可以在不影响动画效果的情况下阻塞(去执行其他任务)掉了。

Animated.timing(this.state.xxx, {
duration: 1000,
useNativeDriver: true
})

因此如果你在某个动画中启用了原生驱动,那么所有和此动画依赖相同的动画值的其他动画也必须启用原生驱动。
本地驱动程序目前不支持Animated所能做的一切。 主要限制是您只能对非布局属性进行动画处理:像transform和opacity这样的东西可以工作,但是flexbox和position属性不会。

合成动画值

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

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

代码实例:

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

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

插值

interpolate(config: InterpolationConfigType函数允许输入范围映射到不同的输出范围。

InterpolationConfigType类型如下:

export type InterpolationConfigType = {
// 输入区间
inputRange: Array<number>,
// 输出区间
outputRange: Array<number> | Array<string>,
// 输入数据曲线
easing?: (input: number) => number,
// 'extend' | 'identity' | 'clamp',用于限制输出区间
extrapolate?: ExtrapolateType,
extrapolateLeft?: ExtrapolateType,
extrapolateRight?: ExtrapolateType,
};

代码实例:

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

并且在动画回退时自动调整。

区间节点控制

Interpolate()还支持定义多个区间基线,常用来定义静止区间等。用来控制动画出入值的关键点位。

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

值类型转换

interpolate()还支持到字符串的映射,从而可以实现颜色以及带有单位的值的动画变换。

value.interpolate({
inputRange: [0, 360],
outputRange: ["0deg", "360deg"]// 角度
});

追踪动态值

动画中所设的值还可以通过跟踪别的值得到。你只要把toValue设置成另一个动态值而不是一个普通数字就行了。

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

输入事件

Animated.event(argMapping: Array<?Mapping>, config?: EventConfig)通过结构化映射,返回的是一个函数。
第一个参数是一个数组,可以将手势,如平移或滚动,以及其他事件可以使用直接映射到动画值。
第二个参数是一个正常的函数回调,是默认的回调函数。

代码实例:

class xxx extends React.Component {
this.state = {
scroll: new Animated.Value(1)
}
...
render () {
return (
<Scroll
onScroll={
(e) => {
Animated.event(
[{ nativeEvent: {contentOffset: {x: this.state.scroll}} }],
{
listener: (e) => { console.log(e); },
useNativeDriver: false,
}
)(e);
}
}>
...
</Scroll>
);
}
}

上述代码中,将e.nativeEvent.contentOffset.x值绑定到this.state.scroll上,即this.state.scroll=e.native.Event.contentOffset.x