在移动客户端开发中流畅、有意义的动画是非常重要的,现实生活中的物体在开始移动和停下来的时候都具有一定的惯性,我们在界面中也可以使用动画来实现契合物理规律的交互。
官方在开发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>
);
};
要想写一个动画需要以下几步:
- 使用基本的Animated组件,如Animated.View、Animated.Image、Animated.Text和其他(使用AnimatedImplementation来包装);
- 使用Animated.Value设定一个或多个初始化值,如位置属性、透明属性、角度属性等;
- 将初始化值绑定到动画目标的属性上,如style;
- 通过动画类型Api设定动画参数,如spring、decay、timing三种动画类型;
- 调用start启动动画,同时可以在start里面回调相关功能;
动画类型 timing、spring、decay
- Animated.decay() 以指定的初始速度开始变化,然后变化速度越来越慢直至停下。
decay 动画参数
export type DecayAnimationConfigSingle = AnimationConfig & {
velocity: number, // 起始速度,必填参数
deceleration?: number, // 速度衰减比例,默认为0.997
};
decay典型的使用场景是某个以组件一定的速度运行并不断减速,比如,移动(swipe或者fling)一张卡片。手指让卡片滑动并具有速度,最后卡片会因为阻力减速,并最终停止。 一般都是配合手势一起用
- Animated.spring()提供了一个简单的弹簧物理模型.
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 这三组数据,只能指定其中一组,即三选一
- Animated.timing()使用easing 函数让数值随时间动起来。
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
例子中有两个动画。一个控制透明度,另一个移动一定距离
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
来修改这个行为。
3、stagger
和parallel
类似,也是使一组动画同时运行。但是,稍有一点不同的是这些动画之间开始时间依次会有一定的延迟。
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']
})
}
]
常用方法
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
}
]}]}
效果演示:
上面例子还使用了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
,提供了很有用的值dx
和dy
。这两个值就是用户从触碰屏幕开始手指移动的距离。
自定义动画createAnimatedComponent
默认的情况下Animated.View
、Animated.Text
和Animated.Image
支持动画。有的时候,你需要其他的某些组件也变成动画组件,方法createAnimatedComponent
可以完成这个任务。它会把的组件props或者state等属性和Animated.Value
自动绑定起来。
比如ScrollView:
let AnimatedScrollView = Animated.createAnimatedComponent(ScrollView)