学习和应用 react-native 已经有快一年的时间了,在平时的工作实践中遇到的坑和痛点也是不胜枚举的。自己使用 RN 的动画组件 Animated 也有一段时间了,从刚开始的简单动画慢慢到复杂动画,中间也经历了不少“微妙”的事情让自己苦不堪言。这次就谈一谈自己在实现一个较为复杂动画的经历和遇到的一些坑,记录一下这次“微妙”的经历,也希望能给广大 react-native 的初学者带来参考,如果写的有欠缺的地方希望大家可以不吝赐教,可以相互学习,相互促进。不再废话,进入本次主题:

今天要实现的是一个点赞动画:ui 的效果图如下:

1baace79fade35afc76189bf1525e01a.gif

这是大小两个动画,比例是 3:1,小的点赞动画多了一个+1 的动画。最开始的时候是用图片的轮帧播放实现的,由于图片较多,虽然使用提前预加载图片的方式,但是在 android 第一次加载动画上还是会有丢帧的现象。接下来考虑用 gif 代替,发现用 gif 的时候,动画里面的一下渐变的透明的效果都出不来,最后把以上两个方案都 pass 掉了。那么接下来就是用 RN 的 Animatied 组件来实现这个动画,由于大拇指有一个变形的过程程序实现起来会有困难,所以最后去掉了这个效果,手的动画只有旋转,缩放,渐变消失三部分了。

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                            }                        ]                    },                ]}            >+1Animated.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 设计的还原度还算可以)

27b8f82c6558f49822ce3bc6c6ef547e.gif

7. 总结

经过这次的动画实践,得出以下几点结论:对复杂的动画进行合理的拆解,然后对单个动画进行处理;遇到走不通的地方我们可以换一种思维,用其他的可替代方式来实现我们需要的效果;要深入的去研究和了解 RN 的性能问题以及动画的性能问题,这样才能从根本上解决问题;最后一点就是做技术不能闭门造车,这样不仅影响自己成长,也会使自己的思维更加局限,通过跟同事之间相互讨论,学习别人的思维和解决问题的思路,会使自己受益匪浅。

到此结束,在实际 app 应用中,还是需要结合实际情况不断优化,希望可以给 RN 的初学者带来一些借鉴。

791538e93e1001fd5c77dd8c77c015a0.png

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐