H5活动开发必备之动效实践

2,797 阅读15分钟

1. 背景

本文为网易LOFTER近期开发的一款H5动效活动的相关技术总结和分享。旨在分享和交流前端常见的H5动效方案原理及使用场景,以及一些H5活动开发的适配和优化技巧。 感兴趣的可以点击下方链接体验,请在移动端打开。

录取通知书活动

2. 目录

  • 动画效果实现方案对比
  • 移动端适配
  • 视频预加载
  • 学习资料推荐

3. 动画效果实现方案对比

3.1 水平运动动画

纯css实现方案

大概的代码

.animate .p3_d {
  animation: p3D 1s ease-in 0.1s forwards;
}

@keyframes p3D {
  0% {
    transform: translate3d(24rem, 0, 0);
  }

  90% {
    transform: translate3d(7.2rem, 0, 0);
  }

  100% {
    transform: translate3d(8.2rem, 0, 0);
  }
}

实现效果地址

可以看到整体的动画效果其实不够灵活,显得呆板,影响动画效果的两个关键因素一个是关键帧(keyframes),一个是动画的过渡效果(transition-timing-function),这里使用的是ease-in,一般好的过渡效果都会用贝塞尔曲线实现(感觉都可以单独写一篇文章讲解贝塞尔曲线对动画的影响了,篇幅原因不做展开)。

个人建议如果设计师能够在这两个关键因素上提供帮助,那么就用css实现,如果不能,可以考虑其他的方案

使用dynamics.js实现

dynamics.js的官网,借助这个库可以实现一些逼真的物理运动动画

const p30 = document.querySelector('.p3_0');
dynamics.animate(p30, { translateX: '1.7rem' }, {
    type: dynamics.spring,
    frequency: 40,
    friction: 200,
    duration: 1000,
    delay: 0.2
});

可以看到它的使用方式相当简单,要说缺点,个人感觉就是源码用coffee.js写的,这门语言现在基本已经被ts替代了,如果觉得它不能满足需求,想改源码可能比较困难,下面看一下使用dynamics.js实现的效果

实现效果地址

其实动画效果就是这样,乍一看实现效果都差不多,但仔细看,它们之间还是会有很多细微的差别,往往就是这些微小的差别,值得我们深入研究,让动画效果显得更加灵动,逼真。

3.2 随机运动动画

方式1:纯css实现类似的运动(不是随机)

可以参考这个例子,这个例子虽然不是真正的随机运动,但是实现效果也不错,所以做为一个参考的案例也放进来一起比较。

观察optionFloatAniP2Key的实现,可以看到,为了运动效果的平滑,设置了非常多的关键帧,这就非常依赖视觉把关键帧导出给前端,光靠前端自己可能很难实现这么丝滑的动画效果。

方式2:使用js实现随机运动

首先确定选项一开始运动的方向

image

我们想让选项可以往上下左右随机一个方向开始运动,可以这样实现:

  1. 生成0~9的随机数(其他数值当然也是可以的,确保几率是50%即可),判断是否为偶数,如果是偶数往正方向运动,奇数则往负方向运动
function randomDirection(velocity) {
    const isEventNum = Math.floor(Math.random() * 10) % 2;

    return isEventNum == 0 ? velocity : -velocity;
}

const velocityX = randomDirection(0.2)
const velocityY = randomDirection(0.2)

这样每个选项一开始就会有[x, y], [-x, y],[x, -y], [-x, -y]四种选择,实现了初始运动方向的随机。

  1. 给选项设置最大运动范围

如图所示,红框是选项的运动范围(这里只是为了展示,实际范围会小很多)

image

如果运动范围是固定的,运动就显得呆板,可以让这个运动范围也随机一下

function randomMax(num) {
    return num - Math.floor(Math.random() * num);
}

randomMax(25)

物体运动到运动范围时就让其往反方向运动,并且再次调用函数,更新运动范围的距离。比如第一次运动,物体x轴正方向运动范围是elemet.originLeft(originLeft是初始坐标值,这个值一直保持不变) + 25,达到这个坐标位置后,物体往返x轴负方向运动,并且更新运动范围x坐标值,那么下次物体再往x轴正方向运动的时候可能运动到elemet.originLeft + 20的位置就往负方向运动了,这就实现了运动距离的随机。

把随机运动的函数封装好,所有的选项都可以使用。

优势

这种实现方法的好处就是不需要设计师提供支持,毕竟不是每个设计师都能够把自己在AE上做的动画效果导出关键帧。

我们需要做的只是调一下物体运动的速度和最大运动距离即可。

视频效果地址

实现方式1:操作dom

一开始我想到可以使用操作dom的方式实现,但是思考了一下,如果开一个定时器,频繁使用transform对dom进行translateX,translateY变换,在dom元素比较多的情况,低端的安卓机子上可能会存在性能问题,为了更好的用户体验,我放弃了这种实现方式。

实现方式2:canvas绘图

canvas绘图的实现方式性能优于操作dom,知道了随机运动的思路,实现起来其实并不难,无非就是调用drawImage()方法绘图,我这里就不再赘述了,只是canvas实现有一定的学习成本,大家可以了解一下,酌情使用。

绘图不清晰

现在的主流手机都采用高清屏,屏幕上的一个点需要用3个像素绘制。为了显示高清页面,我们的活动都使用宽度为1125的3倍图做视觉稿,canvas绘图也需要进行类似的处理,可以参考下面的文章

canvas绘图模糊处理

canvas点击事件处理

canvas绘制的图形不能像dom一样绑定一些点击事件,如果需要对绘制的图形进行交互操作如点击,可以根据点击的坐标进行判断

// 把需要点击的元素存在数组中
let clickElements = [a, b, c, d]

function onClick(clientX, clinetY) {
    clickElements.forEach((element) => {
        if (
            clinetY > element.top
            && clinetY < element.top + element.height
            && clientX > clientX.left
            && clientX < element.left + element.width
        ) {
            // 选中物体,进行一些操作
        }
    })
}

3.3 帧动画

点击p4页面的选项,会有一个精灵动画,原理是这样的:

精灵图预览

ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)

sx,sy是绘制的x,y坐标,比如第一帧绘制图片中的1区域,第二帧绘制图片中的2区域,以此类推,帧数切换的时候就会产生动画,所以这种效果被称为帧动画。

image

css的animation steps也是同理。

视频效果地址

3.4 lottie动画

说到动画效果,不得不提一下lottie-web,通常设计师都会用AE软件制作动画,他们可以把做好的动画导出一份json文件,使用lottie-web执行,就能够完美的还原动画,使用方式也相当的简单:

lottie.loadAnimation({
    renderer: 'svg',
    loop: false,
    autoplay: false,
    container: document.querySelector('.p6_a'),
    name: 'p6a',
    animationData: p6aJson // 设计师导出的json
});

lottie-web能实现的动画效果有:

  • 平移
  • 放大
  • 旋转
  • 淡入淡出
  • svg的各种动画

...等等

基本能满足大部分的动画场景,最大的好处就是能够大量节省开发时间以及和设计师联调的时间。

之前一提起要做动画效果,我想到的就是效果类的实现估计又得花上不少时间进行开发与调试,使用lottie-web确实可以大量提升效率。

这个库的体积也比较mini,只有67kb左右,兼容性也比较好,亲测在安卓4.4版本动画也能运行。

lottie-web的缺点

一些特效类的效果无法实现

比如这种效果

点击元素需要切换图片的效果不好实现

lottie-web主要做一些用来展示用的动画,一些需要交互的动画可能要慎重考虑,比如p3页面的选项,点击之后需要切换图片这种就不太好做了。如果元素是纯色的可以实现,比如可以让设计师使用svg代替image,而svg的颜色可以继承父元素,点击元素之后切换颜色是可以做到的。

3.5 视频动画

一些特效类的效果,可以考虑做为背景视频实现,比如第1页的转场

视频效果地址

使用视频做动画的好处是效果炫酷,接入成本较低,如果一些动效通过技术手段不好实现,可以考虑做成视频接入,这类动效不能有交互操作,所以一般做为背景。

3.6 动图实现动画

第8页的题目使用apng实现,点击可以切换颜色,先看看效果

视频效果地址

实现原理是这样的,默认未选择状态使用饱和度saturate(0)和亮度brightness(0)设为0将他们变成黑色,选中后恢复成1就变成青色了。

.img {
    filter: saturate(0) brightness(0);
}

// 点击
.active .img {
     filter: saturate(1) brightness(1);
}

3.7 动画效果适用场景及总结

3.7.1 css实现动画总结

如果只是一些简单的动画效果,直接用css实现是最方便的。

复杂一点的效果最好让设计师提供keyframes,以及transition-timing-function,如果无法提供,可以考虑其他方案,不然很可能做出来之后要花费比较多的时间与设计师联调动效。

如果在安卓机子出现性能问题,需要优化一下性能,可以用下面两种方式。

// 方式1transform: translate3d(0,0,0);

// 方式2will-change: auto;

3.7.2 lottie动画总结

能够完美还原设计师在AE上制作的动画,大幅度节省开发,联调动效时间,常用于展示类型的动画,需要交互的元素酌情使用,大部分场景推荐使用。

3.7.3 js实现动画总结

css实现的动画运动轨迹是固定的,利用js的计算能力可以让动画效果有更为丰富的展现,比如生成随机数实现物体随机运动,物体向下位移如果能加上一个重力加速度可以更好的模拟物体下落的效果,物体运动碰撞到墙体回弹受到物体运动的速度、空气阻力,物体的弹力影响等。

js实现动画可以有2种方式:

  1. 如果运动的元素不多,并且对性能没有特别高的要求可以使用操作dom的方式实现,因为可以方便的绑定点击等事件
  2. canvas实现动画动画性能优秀,当页面动画元素较多可以考虑,但是它有一定的学习成本,2d的动画其实也还好,如果要更进一步实现3d的效果需要涉及到一些图形学的知识,学习曲线比较陡峭。

3.7.4 帧动画总结

帧动画实现的效果较为自然,各种效果也都能实现,但受到图片大小的限制,比较适用于小型物体帧数较少的动画,比如题目选项,手势动作等。因为如果帧数过多,图片较大,对手机的渲染有压力。

3.7.5 视频动画总结

技术上不好实现的特效可以做成视频,但是视频的播放在移动端往往会遇到一些坑,也要考虑视频的大小,按需做预加载,并且在移动端通常需要点击才能播放视频。

3.7.6 动图实现动画总结

这一类动画和帧动画一样,受到图片大小的限制,适用于小型物体的动画,区别就是动图实现的动画只能循环播放,帧动画则不受这个限制,其中动图有2种常用的格式:

  1. apng格式

它可以实现背景颜色透明,这是gif做不到的

image

值得注意的是使用apng需要注意兼容性问题,尤其是安卓机子

image

可以看到,ios对apng的支持还是相当不错的,安卓机子就不太行了,经测试同学亲测安卓5无法支持apng,但是图片依旧可以显示,只是不能动了。

  1. WebP图片同等质量下图片体积更小,并且压缩之后质量无明显变化,也可以完美支持无损图像,但是也一样要注意兼容问题

image

安卓的支持还是不错的,但是ios的支持就不太行了。

总的来说个人还是比较看好WebP,因为apng目前并没有获得PNG组织官方的认可,而WebP除了兼容性问题,其他的表现都挺优秀,相信随着浏览器版本的升级WebP会越来越受欢迎。

4. 移动端页面适配

移动端虽然使用了rem布局,但还是有某些特殊场景需要进行适配,比如页面在谷歌iphone5模拟器环境中页面下方被截断了一些

image

在真机上的表现那就更为不堪了。

要适配这种小屏幕的手机,可能我们会想到使用css的媒体查询。通过观察,我们可以看到元素的间距还是挺大的,可以通过调整间距来达到适配的目的。

coding...

// iphone 5
@media only screen
  and (min-device-width : 320px)
  and (max-device-height : 568px) {
      div1 {
          margin-top: xxx;
      }
  }

想法很美好,但是我们的活动需要在各种环境下投放,在安卓原生浏览器中,底部会带有返回,前进等操作的区域,这无疑让屏幕的显示区域变小了,即使是大屏手机,底部的元素也会被截取部分。而且我们也只适配了iphone5这个尺寸的手机,我意识到市面上手机尺寸繁多,如果出现了问题就要专门给这个尺寸的手机写个媒体查询,这并不是一种优雅的方案。

使用flex布局适配

首先我们先来了解一下flex的一些属性

  • flex-grow:定义项目的放大比例,默认为0,及时存在剩余空间,也不放大
  • flex-shrink:定义项目的缩小比例,默认为1,如果空间不足,该项目缩小,为0则不缩小
  • flex-basis:定义在分配多余空间之前,项目占据的主轴空间。浏览器根据这个属性,计算主轴是否有多余空间,默认值为auto,即项目本来的大小

接着观察页面

image

图中:1,2,3,4部分可以根据屏幕的尺寸进行动态缩小,序号,题目,图片,密封线等元素不要缩小,并且题目选项显示区域作为页面最主要的部分应该随着手机屏幕变大而动态调整。想好了思路之后开始着手实现

// 页面使用flex布局,并且将主轴设置为垂直方向
.page {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
}

// 图中标识为1的区域使用div填充
.div1 {
    flex-basis: 0.95rem;
    flex-shrink: 1;
}

// 题目显示区域,默认大小为13.36rem,即使空间不足也不缩小,空间剩余则变大
.content {
  flex-shrink: 0;
  flex-grow: 1;
  flex-basis: 13.36rem;
}

... 其他元素类似

flex-shrink也可以定义缩小的优先级,比如div1的flex-shrink = 1,div2的flex-shrink = 2,则优先缩小div2的高度

flex-basis是一个非常关键的属性,通过flex-basis浏览器可以更准确的给项目分配空间,如果使用高度替代flex-basis,在ios 10.3版本会出现元素无法缩小的情况。

看看最终效果

image

结合autoprefixer,可以让flex布局有很好的兼容性,下面我们看看使用autoprefixer生成的兼容性代码display: -webkit-box的设备兼容情况

image

可以看到兼容性已经非常不错了

总结两种方案

  • 媒体查询适合单一渠道,比如只在某个app内,且出现布局问题的机型不是很多的情况下使用,操作简单,调整一下出现问题的元素即可
  • flex布局适合多种场景,并且经过autoprefixer后兼容性较好,推荐使用

5. 视频的预加载

由于活动中有好几个视频做为背景,为了给用户更好的观感体验,开发者通常会对视频进行预加载,下面来谈谈进行视频预加载的两种方式。

方式1:提前一个页面加载视频

如果你的页面遵循固定的访问顺序,比如p1 => p2 => p3,你可以考虑在访问p1的时候就先生成p2的video标签,并且给标签添加 preload="auto"属性,以此类推,达到一个预加载的目的。但是这种方式限制比较大。

  • 页面访问顺序需固定
  • 有些手机浏览器为了用户的流量考虑会把preload属性强制设置成none,这就达不到预加载的目的了

方式2:提前请求视频资源数据

axios({
  method: 'get',
  url: 'video url',
  responseType: 'blob'
}).then(res => {
   const blobUrl = URL.createObjectURL(res)
   // 生成video标签,并且设置src = blobUrl
})

blob就是视频的原始数据,通过createObjectURL,我们可以生成一个blob url,然后创建video标签,这样就可以达到一个预加载的目的。

如果觉得还不够保险,还可以监听video标签的canplaythrough事件,当浏览器判断视频可以无需缓冲,能够流畅的播放视频就触发此事件。

this.video.addEventListener('canplaythrough', () => {
  callback && callback()
});

试想,活动一开始有一个loading,背后进行视频预加载,加载完毕后正式进入页面,这样的用户体验是比较好的。

这种实现方式可以适用于多种视频播放场景,但值得注意的是如果要请求站外的视频资源,需要处理一下跨域的问题。

视频播放的坑

生成video标签之后需要

this.video.load()

load()方法重置媒体成初始化状态,亲测如果在chrome中视频播放了多次,却没有调用load()方法,可能视频会无法播放,具体原因我还没了解清楚。

在移动端微信浏览器下,如果没有调用load()方法,某些ios手机无法触发canplaythrough事件。

某些安卓手机播放视频之前会黑屏进行解码,可以在视频上面蒙上第一帧图片,监听视频的timeupdate事件,当视频的currentTime属性有值的时候证明视频开始播放了,这时可以把图片隐藏。

伪代码实现,可做参考。

本文发布自网易元气事业部前端团队,文章未经授权禁止任何形式的转载。欢迎与我们交流前端相关的技术问题和经验,同时,团队以及部门正在招聘前端、服务端以及客户端各岗位的开发人员,以上都可以联系LofterFrontendTeam@corp.netease.com进行交流。