前段时间上线了全息机器人项目,线上访问可直接在手机百度 APP 中语音搜索 百度神灯
即可(请在wifi环境下访问)。项目部分截图如下:
通过部分截图可以看出,这个项目主要是两部分,一部分正常的动画引导页面(前四个截图所示),另外一部分是全息播放页面(最后一个截图所示)。其中动画引导页面共有十一个,全息播放页面有四个,进入每个全息页面前都会一些动画引导的页面。
这篇文章主要从 方案制定
,遇到的问题以及应对方法
等方面来对项目中这两部分做一个介绍。
在没有老旧 IE 的情况下,做动画的选择是比较多的,SVG
,CSS3
,CANVAS
等等。最开始拿到这个项目的时候,结合这三种各自的特点,最终选择了主要动画由 CANVAS
来实现,具体原因如下:
SVG。大家都知道 SVG
是一种在内存中持久保存的保留模式矢量图形,这个特性让 SVG
有很多优势:不受像素影响,无论屏幕分辨率如何,SVG
图形的表现都十分良好;SVG
对动画支持较好,JS 可以完全控制 SVG DOM
元素;可被 CSS
样式化等等。但同时 SVG
也有一些缺点:SVG DOM
结构如果过多,那么会比正常的图形慢;无法动态的修改动画内容;不能与 HTML 内容集成等等。我们这个项目是一个移动端的单页应用,十一个页面加上四个全息播放页面,总的来看,如果用 SVG
的话,页面上 SVG DOM
元素过多,而且几乎每个节点上都会有一些动画效果,动画效果是比较开放的,元素的很多几何属性都会改变,会不断的触发页面的 reflow
。整体来看,对性能的消耗比较大,因此放弃了这个方案。
CSS3。CSS3
做动画的手段主要是 transition
和 animation
(transform
本质上并不是动画属性,它是一个静态属性,一旦写进 style 里,将会直接显示效果,没有任何变化过程,主要用途是用来做元素的变形)。transition
属性是一个简单的动画属性,方便易用,可以说它是 animation
的简化版本。animation
是 CSS3
制作动画的终极武器,通过 @keyframes
定义时间轴和关键帧,可以比较精确的控制动画中任何一个时间点的属性效果,还可以通过 animation-fill-mode
属性来控制动画结束时的样式。 CSS3
动画在性能上是很不错的,不依懒于主线程,还可以吃进 GPU 加速的 BUFF。从这些方面来看,应该毋庸置疑的选择 CSS3
来实现。但是 CSS3
同样有一些缺点:无法在程序运行至某个时间点去动态定义或者修改动画内容;各个不同的动画之间无法保持同步;无法非常精确的控制各个动画之间的衔接。针对我们这个项目的特点,我们也放弃 CSS3
来作为主要动画实现方式,仅仅只是在简单、各个动画无衔接的地方来使用 CSS3
。
CANVAS。CANVAS
是一种不会在内存中保存的即时模式图形。在绘制 2D 图形时,页面渲染性能比较高,而且受图形复杂度的影响不大,只是和图形的分辨率有关,比较适合基于像素的图形操作;DOM 元素唯一,对页面结构几乎没有影响;和 DOM 元素结合起来非常方便。虽然对文本的渲染支持很差,但是文本元素可以通过 DOM 元素来 HACK。另外还有个原因是我们对 CANVAS
这块有一些积累,更熟悉一些。
综合以上的分析,我们最终选择 CANVAS + CSS3 + DOM 的方案来实现整个项目中的正常动画部分,CANVAS 来实现主体部分动画、CSS3 实现一些简单无依赖的动画、而简单的按钮以及文本元素则直接采用 DOM 来实现。
从最终上线的效果来看,在动画的流畅性上的确不如 CSS3
,偶尔会出现一些小卡顿,但总体来说是在可接受的范围内。(后续会使用 CSS3
来重构这块)
在正常动画这部分,没有遇到什么困难,所以接下来的部分着重一下全息播放这部分。
全息播放这部分本质上就是 N 张图片,每隔一段时间切换图片来达到动画播放的效果。首选方案应该就是视频了,但是我们没有采用视频,主要原因是因为在播放视屏的时候,实际上是调用手机自带播放器来播放,一开始会出现上下两个导航条(上方是进度控制导航,下方是音量控制导航),这完全不符合设计要求。那么另外一种方案就是用图片了,通过不断的切换图片来达到动画播放的效果。
全息播放这部分,一共是分为四组,第一组 415 张,第二组 675 张,第三组 281 张,第四组 497 张。而且最开始设计师给的图质量比较高,1242 * 1242 px(当时和 UE、PM 沟通压缩图片并未允许)。
通过上面的描述,主要的问题大家应该可以看出来,就是性能。要加载这么多高质量的图片,而且要以动画形式来展现,如何保证程序不崩溃,是一个很大的问题。下面就结合加载和渲染两个方面来说一说我们做过的尝试以及最终上线的方案。
加载无非是两种方式,一种是直接加载图片,另一种是事先做一些处理,譬如生成 base64 文件,然后去加载 base64 文件。
canvas.drawImage
来渲染的话会导致 crash,通过其他方式渲染的话,会导致跳帧。\n
区分每张图片的 base64 串而已。加载的时候,也不做任何的处理,事先存的是文本,那么加载过来的就是文本,不是什么对象了,加载完成直接挂载到模块导出对象上,不会有其他影响性能的地方,这样加载的事情就完成了。这也是我们最终采用的方案。在渲染这块,我们先后尝试过如下几种方案:
到这里,加载和渲染的主要问题我们解决了。接下来在上线后的自测过程中又出现了一个问题,由于这个项目最终是要在手机百度 APP 上以语音搜索的形式进入,手百 APP 语音搜索后打开的运营页面是运行在他们的 O2O 框架中,这个框架网页渲染能力比手百 APP 要差一些,而且有一个很奇葩的特点,就是当 url 发生变化时(包括加上一个 #
),框架就认为页面重新渲染了,就会去触发页面上 ajax 请求,而在 IOS 端,为了良好的用户体验,加载请求的时候框架会在 APP 自动生成一个 正在加载...
的 loading 提示。所以当时我们的问题就是切换页面时,突然出现一个 正在加载...
的提示框,而且会持续一段时间,我们判断这个请求是在加载预先生成好的 base64 文本文件,后来把加载所有 base64 文本文件的请求全部写在第一个页面。但这也没用,到那个页面切换的时候还会出现,然后经过排查,才知道是切换页面的 a
标签元素没有阻止默认事件,导致页面 url 上出现了 #
,框架会触发 ajax 请求,阻止了默认事件之后,这个问题也就解决了。
主要的问题就是上面这些了,当然也有一些其他的问题,比如 base64 文件存放在哪,如何去加载?最开始我们把文件放在 edp bcs 上(线上服务器对 baidu 域名有白名单,不会跨域),直接 ajax 加载,后来发现 edp bcs 上传的服务器没有开启 gzip
,对文件大小有影响。因此放到了一个静态资源服务器上,这个服务器会跨域,所以最终采用 jsonp 去加载这些 base64 文本文件。
还有就是 android audio 的播放问题,播放音频,我们采用的是 Howler
,但在某些 android 系统上无法播放,后来我们采用的是切换到需要播放音频的页面时,动态创建 audio 标签来实现播放。
全息这个项目比较极端,但正因为如此,积累了特殊、丰富的经验,后续再遇上此类项目时,定会处理得游刃有余。