前言
任何一款应用无疑都或多或少的使用到动画效果,它对于提升用户体验有着无比重要的作用。React Native同样提供了丰富的动画API供开发者调用,而对于此部分知识的掌握无疑是RN进阶的必经之路,本文通过案例带大家实践掌握Animated、ART等动画及绘图知识。
Animated
ART
手势系统
实现的效果
动画基础
RN目前已更新至v0.56,其动画API也在不断的丰富,其中以Animated
为主要核心,集中了动画创建、执行(组合)、运算(插值)、事件等功能。
创建动画值对象
this.anim1 = new Animated.Value(0) // 用于单个值
this.anim2 = new Animated.ValueXY({x: 0, y: 0}) // 用于矢量值
复制代码
执行与组合
- 所谓动画的执行,实质是改变动画对象的值,我们可以通过
this.anim1.setValue(1)
方法直接赋值,也可以利用Animated.timing
Animated.spring
Animated.decay
等方法以动画的方式运行,以timing为例:
Animated.timing(
this.anim1, // 定义的动画值对象
{
toValue: 1, // 执行到的动画值
duration: 300, // 持续时间
easing: Easing.bounce, // Easing类提供了多种动画效果, 须注意属性和函数的区别
isInteraction: true, // 是否在InteractionManager创建一个interaction handle,此动画可以加入同步队列,完成之后会执行runAfterInteractions函数
useNativeDriver:true // 使用原生动画驱动,启动动画前将所有配置信息发送至原生端,之后利用原生代码在UI线程执行动画,无需两端沟通,脱离JS线程,更加流畅。
}
).start() // 调用start方法开始执行
复制代码
- 同时也可以通过parallel(同时执行)、sequence(顺序执行)、loop(循环执行)、stagger和delay等方法组合动画,例如:
Animated.sequence([ // 首先执行decay动画,结束后同时执行spring和twirl动画
Animated.decay(position, { // 滑行一段距离后停止
velocity: {x: gestureState.vx, y: gestureState.vy}, // 根据用户的手势设置速度
deceleration: 0.997,
}),
Animated.parallel([ // 在decay之后并行执行:
Animated.spring(position, {
toValue: {x: 0, y: 0} // 返回到起始点开始
}),
Animated.timing(twirl, { // 同时开始旋转
toValue: 360,
}),
]),
]).start(); // 执行这一整套动画序列
复制代码
运算与插值
- Animated提供了对动画值的加、减、乘、除、取模等运算。
Animated.add(a, b) // 加
Animated.subtract(a, b) // 减 v0.56新增
Animated.multiply(a, b) // 乘
Animated.divide(a, b) // 除
Animated.modulo(a, modulus) // 取模
Animated.diffClamp(a, min, max) // 返回一个介于min和max之间动画值
复制代码
- 插值也属于运算的一种方式,可以使用动画值对象的
interpolate
方法,进行输入与输出的映射. 例如:
this.anim1.interpolate({
inputRange: [0, 1],
outputRange: [0, 100], // 将动画0-1值映射为0-100进行输出
});
复制代码
组件
动画必须作用在特定组件之上,目前Animated封装了Image、ScrollView、Text、View组件并导出,可以直接通过Animated.XXX
的形式使用。对于自定义组件,可以通过createAnimatedComponent
方法创建,具体使用见案例部分。
事件
对于连续调用的事件,比如使用手势进行滚动、平移、缩放等操作,可以通过Animated.event
进行结构化映射,从复杂的事件中提取值并映射到动画值对象,自动完成setValue
方法的调用,例如:
onScroll={Animated.event(
[{ nativeEvent: {
contentOffset: {
x: this.scrollAnimX // scrollAnimX为创建的动画对象, 映射到e.nativeEvent.contentOffset.x的值
}
}
}]
)}
复制代码
绘图基础
ART
ART(iOS端需预先引入)是RN提供的绘图API,主要涵盖了以下内容:
const {
Surface,
Shape,
Group,
Text,
Path,
ClippingRectangle,
LinearGradient,
RadialGradient,
Pattern,
Transform
} = React.ART
复制代码
同普通组件使用一致,通过设置属性的方式绘制图形,重点掌握Path类对路径的绘制,这里不再赘述,点击了解更多
一些插件
- d3-shape 更加方便的绘制路径
- d3-scale 实现抽象的数据映射
- react-native-svg 对于熟悉Svg开发的同学推荐使用
案例1
接下来开始我们的编码工作,首先实现抖音的双击点赞特效
原理解析
利用手势系统监听双击事件,拿到当前触摸点的坐标值并创建心形组件,组件内部执行放大并降低透明度的动画,动画完毕后移除组件。
实现代码
- 单独创建一个心形组件,其内部完成动画效果
const ROTATE_ANGLE = ['-35deg','-25deg','0deg','25deg','35deg']
class AnimHeart extends Component{
constructor(props){
super(props)
// 创建一个动画值对象,并使用插值运算实现透明度和缩放的效果
this.anim = new Animated.Value(0)
// 设置随机旋转角度
this.rotateValue = ROTATE_ANGLE[Math.floor(Math.random()*4)]
}
render(){
const {x, y} = this.props
return <Animated.Image
style={{
position:'absolute',
left: x,
top: y,
opacity: this.anim.interpolate({
inputRange:[0, 1, 2],
outputRange:[1, 1, 0] // 根据动画值0-1-2的变化,调整透明度
}),
transform: [{
scale: this.anim.interpolate({
inputRange: [0, 1, 2],
outputRange: [1, 0.8, 2] // 根据动画值0-1-2的变化,调整缩放比例
})
},{
rotate: this.rotateValue
}]
}}
source={require('./cc-heart.png')}
/>
}
componentDidMount(){
// 使用顺序执行动画函数
Animated.sequence([
// 使用弹簧动画函数
Animated.spring(
this.anim,
{
toValue: 1,
useNativeDriver: true, // 使用原生驱动
bounciness: 5 // 设置弹簧比例
}
),
// 使用定时动画函数
Animated.timing(
this.anim,
{
toValue: 2,
useNativeDriver: true
}
)
]).start(()=>{
// 动画完成后回调
this.props.onEnd && this.props.onEnd()
})
}
componentWillUnmount(){
//console.warn('unmount')
}
// 禁止该组件重新渲染,提升性能
shouldComponentUpdate(){
return false
}
}
/*
注意:
动画序列中,如果第一个动画中的useNativeDriver设置为true,
此时动画便交于原生端进行执行,不可再切换为JS驱动,后续动画的useNativeDriver也必须设置为true
*/
复制代码
- 创建手势,检测双击事件,根据坐标点渲染
AnimHeart
class App extends Component {
constructor(props){
super(props)
this.state = {
heartList: []
}
this.tapStartTime = null
this._panResponder = PanResponder.create({
onStartShouldSetPanResponder: (evt, gestureState) => true,
onPanResponderGrant: this._onPanResponderGrant
})
}
_onPanResponderGrant = (ev)=>{
if(!this.isDoubleTap()) return
const { pageX, pageY } = ev.nativeEvent
// 设置位置数据,渲染AnimHeart组件
this.setState(({heartList})=>{
heartList.push({
x: pageX - 60,
y: pageY - 60,
key: shortid.generate() // 使用shortid生成唯一的key值
})
return {
heartList
}
})
}
// 检测是否为双击
isDoubleTap(){
const curTime = +new Date()
if(!this.tapStartTime || curTime - this.tapStartTime > 300) {
this.tapStartTime = curTime
return false
}
this.tapStartTime = null
return true
}
render() {
return (
<View
{...this._panResponder.panHandlers}
style={styles.container}>
{
this.state.heartList.map(({x, y, key}, index)=>{
return <AnimHeart
onEnd={()=>{
// 动画完成后销毁组件
this.setState(({heartList})=>{
heartList.splice(index,1)
return {
heartList
}
})
}}
key={key} // 不要使用index作为key值
x={x}
y={y}
/>
})
}
</View>
);
}
}
复制代码
- 最终效果如下