移动端拖拽

最近上线了一个动画效果偏多的小页面,由于上线时间紧张,来不及做优化,但是其中也不乏有一些功能点可以小小的记录一下。
要达到的效果如下图,需要在一定时间内不断向上拖拽图中的圆(圆会不断放大到某个倍数),同时,页面长背景同步以某个和谐的速率下移,拖拽到页面上方正方形的底部,做碰撞检测。
《移动端拖拽》

开发的过程中遇到一些难点:

  • 开发时间短的情况下选择什么方案实现

一开始评估的时候,其实没往拖拽这个重点想,尽往游戏框架用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)
  },

当然,以上的解决方法都十分笨拙,肯定有很多高性能的办法可以打磨得更好,但是呢,我想想,不管如何,是一个思考过程,还是需要记录一下。

    原文作者:laomao
    原文地址: https://segmentfault.com/a/1190000020226201
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞