1. 背景
最近开发了一个录取通知书活动,取得了比较好的效果,所以写篇文章总结一下经验,有兴趣的可以点击下方链接,请在移动端打开。
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实现随机运动
首先确定选项一开始运动的方向
我们想让选项可以往上下左右随机一个方向开始运动,可以这样实现:
- 生成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]四种选择,实现了初始运动方向的随机。
- 给选项设置最大运动范围
如图所示,红框是选项的运动范围(这里只是为了展示,实际范围会小很多)
如果最大范围是固定的,运动就显得呆板,可以让这个最大范围也随机一下
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绘制的图形不能像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区域,以此类推,帧数切换的时候就会产生动画,所以这种效果被称为帧动画。
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 动画效果适用场景及总结
css实现动画
如果只是一些简单的动画效果,直接用css实现是最方便的。
复杂一点的效果最好让设计师提供keyframes,以及transition-timing-function,如果无法提供,可以考虑其他方案,不然很可能做出来之后要花费比较多的时间与设计师联调动效。
如果在安卓机子出现性能问题,需要优化一下性能,可以用下面两种方式。
// 方式1:
transform: translate3d(0,0,0);
// 方式2:
will-change: auto;
lottie动画
能够完美还原设计师在AE上制作的动画,大幅度节省开发,联调动效时间,常用于展示类型的动画,需要交互的元素酌情使用,大部分场景推荐使用。
js实现动画
利用js的能力可以实现一些使用css不好实现的效果,比如生成随机数,物体重力下落,物体碰撞回弹等物理运动,比如一个篮球运动员在运球,如果能越完美的还原篮球的运动轨迹,动画效果就会显得更真实。
js实现动画可以有2中方式:
- 如果运动的元素不多,并且对性能没有特别高的要求可以使用操作dom的方式实现,因为可以方便的绑定点击等事件
- canvas实现动画动画性能优秀,当页面动画元素较多可以考虑,但是它有一定的学习成本,2d的动画其实也还好,如果要更进一步实现3d的效果需要涉及到一些图形学的知识,学习曲线比较陡峭。
帧动画
帧动画实现的效果较为自然,各种效果也都能实现,但受到图片大小的限制,比较适用于小型物体帧数较少的动画,比如题目选项,手势动作等。因为如果帧数过多,图片较大,对手机的渲染有压力。
视频动画
技术上不好实现的特效可以做成视频,但是视频的播放在移动端往往会遇到一些坑,也要考虑视频的大小,按需做预加载,并且在移动端通常需要点击才能播放视频。
4. 移动端页面适配
移动端虽然使用了rem布局,但还是有某些特殊场景需要进行适配,比如页面在谷歌iphone5模拟器环境中页面下方被截断了一些
在真机上的表现那就更为不堪了。
要适配这种小屏幕的手机,可能我们会想到使用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,即项目本来的大小
接着观察页面
图中: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版本会出现元素无法缩小的情况。
看看最终效果
结合autoprefixer,可以让flex布局有很好的兼容性,下面我们看看使用autoprefixer生成的兼容性代码display: -webkit-box的设备兼容情况
可以看到兼容性已经非常不错了
总结两种方案
- 媒体查询适合单一渠道,比如只在某个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属性有值的时候证明视频开始播放了,这时可以把图片隐藏。
伪代码实现,可做参考。
6. 学习资料推荐
最后给大家推荐一本书《HTML5 Canvas核心技术图形动画与游戏开发》,这本书的教学风格是我喜欢的,首先介绍知识点,然后运用这些知识点做demo,缺点就是代码偏多,知识点讲解不够详细