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", } });
效果如下:
2.基本Animated动画的步骤
上例是一个基本的Animated动画的实现,其步骤可以分为以下几步:
1.使用Animated.Value
设定一个或多个初始值,包括透明度、位置等。
2.使用基本的Animated封装的组件,如Animated.View
、Animated.Text
、Animated.Image
和Animated.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.View
、Animated.Text
、Animated.Image
和Animated.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/speed
或tension/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> ); } } ...
效果如下:
4组合动画
Animated可以通过以下方法将多个动画组合起来执行:
* parallel:同时执行
* sequence:顺序执行
* stagger:错峰,其实就是插入了delay的parrllel
* delay:组合动画之间的延迟方法,严格来讲,不算是组合动画
4.1串行动画
下面用sequence演示一个顺序执行的串行动画。
还是在上例的基础上只用修改动画配置,在Animated.sequence
中顺序执行Animated.spring
、Animated.delay
、Animated.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毫秒后再旋转
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();
}
效果如下:拉伸同时旋转
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'
],
})},
]
实现如下循环动画效果:
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.dx
和gestureState.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。
实例代码如下:
//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随着手拖动而移动,手指离开会到原点。
实例代码:
//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的常用方法,并列举了相关的代码示例,例子比较多,对于有些效果有所省略,建议读者多动手尝试下每个效果,举一反三,加深理解。