1. 首先对动画的内容进项拆分:icon、光环、发光效果、+1四部分,先看下布局:(从上到下依次是+1、光线、icon、光环)
render() { return ( <View style={[styles.container]}> //+1 <Animated.Text style={[ styles.txt, { opacity: this.animate.txtOpacityAnim, transform: [ { translateY: this.animate.txtMoveAnim } ] }, ]} >+1</Animated.Text> //光线1 <Animated.View style={[ styles.ray, styles.ray1, { transform: [ { rotate: '-60deg' }, { translateY: this.animate.lightMoveAnim }, { scaleY: this.animate.lightScaleAnim } ], opacity: this.animate.lightOpacityAnim, height: this.animate.lightHeightAnim } ]} /> //光线2 <Animated.View style={[ styles.ray, styles.ray2, { transform: [ { rotate: '-30deg' }, { translateY: this.animate.lightMoveAnim }, { scaleY: this.animate.lightScaleAnim } ], opacity: this.animate.lightOpacityAnim, height: this.animate.lightHeightAnim } ]} /> //光线3 <Animated.View style={[ styles.ray, styles.ray3, { transform: [ { rotate: '30deg' }, { translateY: this.animate.lightMoveAnim }, { scaleY: this.animate.lightScaleAnim } ], opacity: this.animate.lightOpacityAnim, height: this.animate.lightHeightAnim } ]} /> //光线4 <Animated.View style={[ styles.ray, styles.ray4, { transform: [ { rotate: '60deg' }, { translateY: this.animate.lightMoveAnim }, { scaleY: this.animate.lightScaleAnim } ], opacity: this.animate.lightOpacityAnim, height: this.animate.lightHeightAnim } ]} /> //手icon <Animated.Image style={[ styles.icon, { transform: [ { rotate: this.animate.iconRotateAnim.interpolate({ inputRange: [-6, 6], outputRange: ['-6deg', '6deg'] }) }, { scale: this.animate.iconScaleAnim } ] } ]} source={{ uri: 'https://img.58cdn.com.cn/newsfe/toutiao/icon_praise_big.png' }} /> //光环 <Animated.View style={[ styles.circle, { borderWidth: this.animate.circleBorderWidthAnim, opacity: this.animate.circleOpacityAnim, transform: [{ scale: this.animate.circleScaleAnim }] } ]} /> </View> ); }
2. 接下来是初始化动画参数:
constructor(props) {
super(props);
this.animate = {
txtMoveAnim: new Animated.Value(0),
txtScaleAnim: new Animated.Value(0.01), // set scale0.01 for android bug
txtOpacityAnim: new Animated.Value(1),
iconRotateAnim: new Animated.Value(6),
iconScaleAnim: new Animated.Value(0.6),
circleScaleAnim: new Animated.Value(0.01),
circleBorderWidthAnim: new Animated.Value(3),
circleOpacityAnim: new Animated.Value(0.4),
lightHeightAnim: new Animated.Value(0.01),
lightMoveAnim: new Animated.Value(4),
lightOpacityAnim: new Animated.Value(1),
lightScaleAnim: new Animated.Value(1)
};
}
注意:这里并没有把动画的初始化参数放在state里面,而是赋值给一个变量this.animate,因为state是react的状态,触发setState才会改变,animated.Value 是动画的状态,只要调用动画的start()方法就会改变,可以减少不必要的性能浪费;接下来还有一点就是初始化的时候,+1和光圈的缩放状态都是0,这里为什么初始化写成0.01呢,是因为在ios初始化0是没有问题的,但是在android上面部分手机在动画开始的时候会闪现出光圈实际的大小(样式设置的大小),为什么设置成0.01也是经过了多次的尝试,设置过大或者过小还是会有问题,具体原因这里也不是很明白,应该算是RN动画的小坑吧,如果大家有不同观点望不吝留言赐教。
3. icon动画:点赞过程中,大拇指的抖动、大小变化效果,用transform的缩放scale、旋转rotate实现。
const icon = [
Animated.sequence([
Animated.timing(this.animate.iconRotateAnim, {
toValue: -6,
duration: 400,
delay: 0,
easing: Easing.easeOut
}),
Animated.timing(this.animate.iconRotateAnim, {
toValue: 0,
duration: 300,
delay: 0,
easing: Easing.spring
})
]),
Animated.sequence([
Animated.timing(this.animate.iconScaleAnim, {
toValue: 1.4,
duration: 400,
delay: 0,
easing: Easing.easeOut
}),
Animated.spring(
//缩小动画
this.animate.iconScaleAnim,
{
tension: 100,
friction: 5,
toValue: 1
}
)
])
];
4. +1动画:此动画包括文字上移、透明度变化、大小变化,使用transform的tranlateY、缩放scale和透明度opacity样式实现。
const txt = [
Animated.sequence([
Animated.timing(this.animate.txtMoveAnim, {
toValue: -13,
duration: 400,
delay: 0,
easing: Easing.bezier(0.25, 0.1, 0.25, 0.1)
}),
Animated.timing(this.animate.txtOpacityAnim, {
toValue: 0,
duration: 500,
delay: 100,
easing: Easing.bezier(0.25, 0.1, 0.25, 0.1)
}),
]),
Animated.timing(this.animate.txtScaleAnim, {
toValue: 1.5,
duration: 400,
delay: 0,
easing: Easing.bezier(0.25, 0.1, 0.25, 0.1)
}),
];
5. 光环动画:光环变过程中宽度、透明度、大小变化,使用边框的宽度改变、缩放scale和透明度opacity样式实现。
const circle = [
Animated.timing(this.animate.circleOpacityAnim, {
toValue: 0,
duration: 300,
delay: 400,
easing: Easing.bezier(0.25, 0.1, 0.25, 0.1)
}),
Animated.sequence([
Animated.timing(this.animate.circleScaleAnim, {
toValue: 2.5,
duration: 600,
delay: 100,
easing: Easing.bezier(0.25, 0.1, 0.25, 1)
})
]),
Animated.sequence([
Animated.timing(this.animate.circleBorderWidthAnim, {
toValue: 4,
duration: 150,
delay: 400,
easing: Easing.easeOut
}),
Animated.timing(this.animate.circleBorderWidthAnim, {
toValue: 1,
duration: 150,
delay: 0,
easing: Easing.bezier(0.25, 0.1, 0.25, 1)
})
])
];
以上三部分的动画变化稍微简单一点,布局主要是使用绝对定位,只要初始位置正确,其他的动画变化就按照设计给的数据,设置对应的属性就可以了,主要的难点在于接下来要说的光线的动画变化,这个稍微复杂一点。再说这个动画之前我们先了解一下css3动画的一个属性transform-origin(该属性允许您改变被转换元素的位置),定义是这样的,通过此属性可以设置动画的起始位置,默认是在变化的Dom节点的中心位置,为什么要说这个属性呢,因为光线的变化效果是这样的,首先在所在的位置长度慢慢变大,然后再进行移动,最后长度慢慢变小直至消失。如果按照css3的这个属性,我们可以设置变化的位置,这样看来这个动画也不是很复杂;但是RN里面的样式跟css还是有一定的差别的,RN里面的样式没有transform-origin这个属性,那么我们怎么来实现光线的动画呢,不卖关子了,直接上代码:
6. 光线动画:变化过程中主要涉及光线长度、位置、透明度的变化。
const light = [
// light opcity
Animated.timing(this.animate.lightOpacityAnim, {
toValue: 0,
duration: 450,
delay: 550,
easing: Easing.bezier(0.25, 0.1, 0.25, 1)
}),
// light height
Animated.sequence([
Animated.timing(this.animate.lightHeightAnim, {
toValue: 6,
duration: 150,
delay: 150,
easing: Easing.easeOut
}),
Animated.timing(this.animate.lightHeightAnim, {
toValue: 6,
duration: 250,
delay: 0,
easing: Easing.bezier(0.25, 0.1, 0.25, 1)
}),
Animated.timing(
// replace tanslate-origin
this.animate.lightScaleAnim,
{
toValue: 0.2,
duration: 200,
delay: 0,
easing: Easing.bezier(0.25, 0.1, 0.25, 1)
}
)
]),
// light tanslateY
Animated.sequence([
Animated.timing(this.animate.lightMoveAnim, {
toValue: -2,
duration: 200,
delay: 100,
easing: Easing.bezier(0.25, 0.1, 0.25, 1)
}),
Animated.timing(this.animate.lightMoveAnim, {
toValue: -4 - 4,
duration: 600,
delay: 0,
easing: Easing.bezier(0.25, 0.1, 0.25, 1)
})
])
];
四条光线是以0度为分界点,以30度的间隔平均分配,由于没有transform-origin这个属性,我们在长度开始变化的时候,同时进行位置的移动,这样一来就可以模拟以初始位置为动画的起始点,进行长度的变化;最后消失的过程采用位置变化的同时进行缩放处理,可以达到ui图上的消失的动画效果。
最后附上样式代码:
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
width: 32,
height: 39,
position: 'absolute',
zIndex: 9999,
bottom: 6,
left: 1.5,
borderTopRightRadius: 16,
borderTopLeftRadius: 16,
borderColor: '#fff',
},
txt: {
position: 'absolute',
top: 9,
left: 9,
zIndex: 90,
height: 12,
color: '#FD8C20',
fontSize: 12,
fontFamily: 'System'
},
icon: {
width: 33 / 2,
height: 33 / 2,
position: 'absolute',
top: 17,
left: 8,
zIndex: 100
},
circle: {
height: 11,
width: 11,
position: 'absolute',
left: 10,
top: 15,
zIndex: 79,
borderWidth: 11 / 2,
borderRadius: 11 / 2,
borderColor: '#FBCB45',
transform: [{ scale: 0 }]
},
ray: {
width: 1.5,
height: 3,
borderRadius: 1.5,
position: 'absolute',
backgroundColor: '#FD8C20',
zIndex: 81
},
ray1: {
left: 10,
top: 17,
transform: [{ rotate: '-60deg' }]
},
ray2: {
left: 12,
top: 15,
transform: [{ rotate: '-30deg' }]
},
ray3: {
left: 16,
top: 15,
transform: [{ rotate: '30deg' }]
},
ray4: {
left: 18,
top: 17,
transform: [{ rotate: '60deg' }]
}
});
至此动画的部分已经介绍完毕,接下来要说的是RN动画性能方面的影响,由于点赞是在列表里面实现的,本来已经认为大功告成了,可是天不遂人愿,尤其是在android上表现的特别明显,动画会随着翻页的增多,开始时间会变长,而且动画也出现卡顿现象,百思不得其解,最后和同事经过半天的调试发现主要的原因是页面的重新渲染会影响动画的性能,最后的解决方法是组件状态的更新放到动画结束之后再进行,这样页面重新渲染时动画已经结束了,给用户呈现出来的效果是动画比较流畅了。效果图如下图:(ui设计的还原度还算可以,可以做一下对比)
7. 总结
经过这次的动画实践,得出以下几点结论:对复杂的动画进行合理的拆解,然后对单个动画进行处理;遇到走不通的地方我们可以换一种思维,用其他的可替代方式来实现我们需要的效果;要深入的去研究和了解RN的性能问题以及动画的性能问题,这样才能从根本上解决问题;最后一点就是做技术不能闭门造车,这样不仅影响自己成长,也会使自己的思维更加局限,通过跟同事之间相互讨论,学习别人的思维和解决问题的思路,会使自己受益匪浅。
到此结束,在实际app应用中,还是需要结合实际情况不断优化,希望可以给RN的初学者带来一些借鉴。