前言
最近在业务中接触到了勋章改版的需求,勋章属于荣誉性激励,为了提升用户的体验和达到激励的效果,不论是对勋章本身的样式还是勋章的呈现形式都要求比较高。基于此需求,有2个难点动画,分别是解锁新勋章动画和已获得勋章的陀螺仪动画。为了鼓励大家解锁新勋章,对于新获得的勋章,首次进入会有个从旧变新的过程,而难点在于我们如何完美地衔接从旧变到新的过程。对于已获得的勋章,都会跟随手机转动进行变化,这就用到了陀螺仪。陀螺仪是之前的项目从未涉及到的,所以实现陀螺仪的动画,也是个值得记录的地方;下面我们就来详细介绍下。
解锁新勋章
先上张图,如何使用css实现如下效果?
只是简单的动画?在继续阅读前下文之前,你可以先缓一缓。
尝试思考一下上面的效果或者动手尝试一下。
好,继续来看下我们的思路。上面的整个动画过程,如果按实现思路分,我们大致分了4部分。
01
新旧重叠
首先,我们简单分析下,新勋章露出,旧勋章隐藏,并且二者是无缝衔接的,露出的和隐藏的是完全匹配的。所以,布局的时候肯定是有2层的,底层和顶层。底层的很简单,就是一个一直存在的图。而顶层为了实现动画,必须与底层完全重叠。如下图所示:
02
顶层隐藏
完全重叠之后,如果要露出新勋章,那就要不断的隐藏旧勋章。我们可以改变顶层的高度或者宽度,不缩小高度是因为方向不对,所以我们的初步想法是缩小宽度,直至为0,具体效果如下:
可以比较下,从现在的效果看,无缝衔接是可以了,但是移动方向不对。
03
改变方向
基于这个方向,我们做了如下改造,旧勋章进行旋转,使现在的右边旋转到左上方,这样就接近需要的效果了。一旦旋转,那如果还要保证2个图重叠,那顶层就需要改变大小了,具体是多少呢?不旋转时候的尺寸是124*124,旋转之后的长度,勾股定理大家应该都能算出来,124*124+124*124的平方根,这个长度大概是175,即为之前的对角线长度,如下图所示:
04
设置样式
旋转之后进行动画,必须设置2个样式才能实现最终的效果:一个是transform-origin设置为具体的px值,不能设置为center,否则在width不断变小的过程中,顶层会不断的移动,不能无缝衔接;另一个是绝对定位,如果不设置绝对定位的话,勋章也会随着顶层宽度的变小而移动。
具体的页面布局如下:
css部分:
到这里,扫光动画基本完成,现在把光加上,看下效果:
问题
clip-path解决
为了更方便发现问题,实现过程中特意改变了背景颜色,相信大家都发现了,光漏出了。如何解决这个问题呢?如果是圆形等规则图形,我们可以使用overflow,但是对于不规则图形,我们怎么处理呢?clip-path登场!clip-path可以通过裁剪的方式创建元素的可显示区域。区域内的部分显示,区域外的部分隐藏。基于图标的特殊性,这里,clip-path使用的是SVG元素中的路径作为裁剪路径。代码如下:
css中直接使用:
到这里,需要的效果完美实现啦。
陀螺仪动画
所谓的陀螺仪动画就是根据手机的转动反映到操作元素上的一种动画体现。项目中的效果如下图所示:
陀螺仪相关
我们可以通过监听deviceorientation来获得一些关键值。但是不同的手机厂商对这个属性的支持不同,有的手机不支持,iOS13版本以上的则需要申请用户权限,否则也不支持。我们可以使用window.DeviceOrientationEvent来判断当前设备是否支持陀螺仪。而对于iOS13版本以上的用户则需要申请权限,而且必须手动触发(click/touchend等)才会触发授权操作。
监听方式如下:
在实际业务中,基于不同手机厂商的支持问题以及授权问题,我们并不是完全使用原生api,而是客户端内添加的bridge获取角度,对于不支持bridge的那些用户再使用原生api去监听,如果原生的api不支持,那就坚持不动了。
部分关键值的输出:
我们主要关心的输出值其实就3个:alpha(绕z轴, 0 ~ 360),beta(绕x轴, -180 ~ 180),gamma(绕y轴, -90 ~ 90)。沿当前轴,逆时针为不断增加,顺时针为不断减小。但需要注意的一点是,到达极值后,再转动,会变为相反的极值不断变小,举个栗子,假如当前沿y轴转到90度,再转的话,变为了-90度,所以这种情况也是在处理图标转动时需要处理的情况,否则,图标会在临界位置晃动。
陀螺仪开发调试
简单介绍下在chrome上调试陀螺仪的方法,这样大家就不用一直拿着手机来测试了。打开chrome开发者工具,点击三个点,more tools下的sensors,这个控制台下有个orientation选项,打开就可以通过拖动手机调试陀螺仪了(如下)。
业务应用
手机移动的角度知道了,那开始操作图标。
我们的业务场景是以进入时的角度为初始位置,在进入的时候我们看到的是一个正常的图标,平行于手机,手机转动多少图标变化多少。所以需要记录一个初始值,之后的变化都要与初始值取差值,具体图标怎么变化,以及图标变化的幅度,则根据实际业务需求,根据具体的差值,乘系数或者三角函数等变化改变图标的style。业务中的主要js代码如下:
这样是可以改变角度,但是我们的图标毕竟是在二维空间移动,所以沿x轴前后翻转的时候,始终看着图标是在平面动,没有立体的效果。为了解决这个问题,设置了下css,这样3D效果出现了。transform-style:preserve-3d是设置元素中的子元素位于3D空间中,再加上perspective透视转换,实现了3D效果。注意二者分别是设置在父元素和子元素上,否则不起作用。
基本到这里我们的功能就完成,但是有动画的地方就伴随有性能问题,这个也不例外,在某些机型上能看到明显卡顿。介绍下动画卡顿的分析过程和解决方法。
动画性能优化
性能整体表现
首先,通过chrome浏览器的perfomance监测了下性能,fps和cpu的使用率如下:
0-3s的渲染时间:
具体到某个task,可以看到具体执行的内容,Recalculate style -> Layout -> Update Layer Tree -> Paint -> Composite Layers。
我们以此task为例,看下浏览器执行了哪些操作,我们该从何处开始优化?
浏览器渲染原理
从task可以看出,浏览器渲染流程可以抽象为5大步骤:
Recalculate Style:使用javascript计算出需要如何操作DOM结构并计算节点最终样式规则;
Layout:根据节点的css规则,计算节点在屏幕上的位置和尺寸,一个节点的布局发生变化,可能使得多个节点重新布局;
Update Layer Tree: 一个页面可能有多个渲染层,Layer Tree用来维护各个渲染层的顺序;
Paint: 填充像素的过程。绘制一般是在多个层上同时进行的;
Composite Layers: 在多个层上分别绘制完成后,浏览器会按各个绘制层的正确顺序拼合成一个图层,最终显示在屏幕上。
而在实际动画中,应该尽量减少重绘和重排,即Layout环节;从我们的task可以看出,Composite Layers的时间有点长;基于这两点可以进行优化。
01
减少重绘和重排
如果不想触发重排(Layout)和重绘(Paint),那我们应该避免修改一些会引起重绘和重排的样式属性(如:width,height,top,left等),可以尽量使用transform、opacity等,这样layout和paint阶段就没有了。
优化如下:
改为
耗时:
我们再来看下某个task
没有了Layout和Paint,减少了重绘和重排的时间。但是发现Update Layer Tree和Composite Layers的时间还很长,继续优化。
02
减少过多渲染层
过多的创建合成层,会在Update Layer和Composite Layer的时候耗费时间,所以尽量合理使用图层提升。
先介绍下动画的层级,parent这个层级是随着陀螺仪的转动不断的改动rotateX,child-light这个层级是随着陀螺仪的转动不断的改变translateY。为了让动画看着更流畅,我们在parent和child-light上各自加了个transition。如下:
样式设置
但是,对transform应用了animation或者transition可以提升合成层,但是当animation或者transition效果未开始或者结束后,提升合成层也会失效,所以怀疑是因为频繁的修改transform,所以导致合成层在不断变化,增加了update layer tree和composite layer时间。所以我们把transition去掉了进行尝试:
耗时:
可以看到耗时,fps以及cpu的使用率都明显减少了很多。
03
使用requestAnimationFrame实现缓动画
因为上面我们去掉了transition,所以动画动起来没有那么有缓冲的感觉,接下来我们使用requestAnimationFrame实现了transition的效果,使动画动起来更平滑。陀螺仪函数的执行间隔大概是60ms,之前的处理逻辑是接收到的值直接进行style变化。为了使动画更加平滑,在60ms内,从a到b,逐步+step进行变化。
这里采用的曲线函数是ease-in
具体实现如下:
改造后效果:
耗时:
可以看到,加了缓动画后,Paint时间有所增加,这是因为我们在不断更新样式。但是比之前使用transition的效果要好很多。
改造到这里,动画卡顿的问题基本解决了。
////////// 总结 //////////
最后,再总结下动画卡顿的解决方案:
避免改变top,left,width,height等,因为这些属性会引起重绘和重排,尽量使用opacity和transform,对于复杂布局的元素尤其明显。
避免在不断变化的dom元素上使用transition/animation,尤其嵌套元素父和子都用了transition,更会增加Update Layer Tree和Composite Layer的时间,比较消耗性能。
尽可能使用requestAnimationFrame,可以跟浏览器的渲染频率保持同步。
不要乱使用translatez(0)提升合成层,合成层越多,Update Layer Tree和Composite Layer的时间就会耗时越多。所以要在必要时合理使用。
THE END