最近上线了一个动画效果偏多的小页面,由于上线时间紧张,来不及做优化,但是其中也不乏有一些功能点可以小小的记录一下。
要达到的效果如下图,需要在一定时间内不断向上拖拽图中的圆(圆会不断放大到某个倍数),同时,页面长背景同步以某个和谐的速率下移,拖拽到页面上方正方形的底部,做碰撞检测。
开发的过程中遇到一些难点:
- 开发时间短的情况下选择什么方案实现
一开始评估的时候,其实没往拖拽这个重点想,尽往游戏框架用canvas画去靠拢,因为动效很多,也涉及到碰撞检测等等的。本来想快速的看看游戏框架,并在脑子里看是否match这个项目的需求,但最后还是放弃了。怕遇到短时间会令人抓狂的坑。最后还是决定就用原生的touch事件写,这样出了bug也好解决一点。
当然,其实这种决策过程也能暴露很多的问题吧,说明也有非常多的进步空间。
总而言之,不管解决方案是什么,在时间紧张的情况下,至少兜底的方案得先出来。
- touchmove事件的一些莫名问题
向上提拉的过程,涉及到三个部分的同步动效。背景、圆圈、圆圈的放大。效果出来要和谐的话,就需要不断调试。
解决思路一开始想的很简单,无非是计算出来每一次move的时候手指在屏幕上滑动的距离。根据这个距离,除以一定的系数比例,嗯,一开始是晕着调的。
但是原生的touchmove卡顿严重。导致动效一直不理想。touchmove事件似乎不是一直触发的。
查到其他人说原因是因为
200ms超时导致内核不一定会一直处理touchmove事件,一旦超时会将后续所有的事件转交给UI处理,导致touchmove不会一直触发。系统浏览器也存在同样的问题,为了解决开发者需要,建议开发者在touchstart时调用event.preventDefault,这样就可以保证内核会一起触发touchmove事件了
但是调用了preventDefault依然会卡顿。网络上也有很多答案说是要加上passive属性,
Passive Event Listeners是Chrome提出的一个新的浏览器特性:Web开发者通过一个新的属性passive来告诉浏览器,当前页面内注册的事件监听器内部是否会调用preventDefault函数来阻止事件的默认行为,以便浏览器根据这个信息更好地做出决策来优化页面性能。当属性passive的值为true的时候,代表该监听器内部不会调用preventDefault函数来阻止默认滑动行为,Chrome浏览器称这类型的监听器为被动(passive)监听器。目前Chrome主要利用该特性来优化页面的滑动性能,所以Passive Event Listeners特性当前仅支持mousewheel/touch相关事件。
引用自: https://juejin.im/post/5b28d6...
但是发现效果依然不满意,还是十分卡顿。
叹了口气,最后的解决方案就是,还是去找了一个轻便的手势库hammerjs,用到了其中的pan相关的事件。
function () {
// 绑定手势
var hammertime = new window.Hammer(document.querySelector('xx'))
hammertime.get('pan').set({direction: window.Hammer.DIRECTION_ALL}) // 要设定方向才会开启垂直方向的移动
hammertime.on('panstart', this.touchStart)
// 上拉
hammertime.on('panup', this.touchMove)
hammertime.on('panend', this.touchEnd)
},
hammerjs倒是很方便, 主要用到其中的panup事件,panup的事件对象里,也暴露出了一些原本需要自己计算的参数,这样可以直接拿来使用, 主要使用到y轴的加速度,还有时间差,deltaTime, velocityY:
三个动画需要和谐,大致调整的比例是4:11,如果圆的位置要保持在屏幕中间的话
// 做边界限定,不然会一次增长过快
if (speed > 4) speed = 4
yuanSpeed= speed * 4 //圆的速率
background.bottom -= speed * 11 // 背景的速率
// 整体增大到1.8倍
yuan.scaleVal += speed / 110 // 圆整体增大
- 假重力回落
为了效果更好,需要加上一个如果touchend了,就圆就往回落一定距离的效果。
一开始的思路是在圆的现有高度的基础上*0.98,乘以某个系数。但是会出现一个问题,当每次提拉的距离小于0.98的时候,会出现往上拽不动的问题。所以,最终解决方法是,将这一次滑动所有的move累加的值记录下来,touchend的时候再减掉这个累加值的一半:
this.totalMove+= yuanSpeed
toucheEnd: function () {
// 当次距离的一半
this.yuanHeight-= this.totalMove / 2
}
- 如何动态计算圆到盒子底部的距离
由于这个方案涉及很多dom操作,不像canvas那样有xy坐标,方便做碰撞检测。dom操作在计算的过程中本来就十分消耗性能。
比如遇到一个问题:
圆的背景图片到盒子顶部有一定的距离,圆本身会不断放大,这个空白的距离怎么计算并且适配所有的屏幕呢?
加入1334设计稿上,这个空白的距离是50px,那么如何动态计算呢?
解决的办法也有点笨,但管用吧。就是用圆放大后的高度/原来圆背景图的高度得出的比例 * 50。
var yuanHeight= yuan.getBoundingClientRect().height // getBoundingClientRect能拿到scale后高度。直接用innerHeight不行
var yuanMargin = (yuanHeight / yuanImg.naturalHeight) * 50 // naturalHeight原本的高度
当然整个计算过程,额,有点费劲
// 主要是圆距离屏幕顶部的高度+超出屏幕部分的高度
// 在这个距离的基础上,减掉或者加上一些其他距离
// maxOffsetTop 是初始的屏幕顶部超出部分
// bottom是不断下移的距离
var top = Math.abs(this.maxOffsetTop - this.bottom) - self.boxBottom + yuanMargin + yuan.getBoundingClientRect().top
- transition动画闪烁的问题
当碰撞检测成功或者失败的时候都有相关的动画,一开始是直接用css3写,但是会出现奇怪的第一次闪烁的情况。
索性还是换了一种可能看上去不是十分优雅,但是好用的办法。
animateFn: function () {
// 图片张数和动画时间 01~30 1s
var time = 33, // 动画时间 / 图片张数
self = this,
idx = this.animateIndex % 30 // animateIndex 初始值为0
imgDom
img= $('divImg img')
setTimeout(function () {
// 控制图片显隐
$(img[idx]).removeClass('displaynone').siblings().addClass('displaynone')
self.animateIndex++
// 动画1s后结束
if (self.animateIndex <= 30) {
// 调用自身
self.animateFn()
} else {
// 动画结束
}
}, time)
},
当然,以上的解决方法都十分笨拙,肯定有很多高性能的办法可以打磨得更好,但是呢,我想想,不管如何,是一个思考过程,还是需要记录一下。