天天看点

移动Web触控事件总结

移动web风风火火几多年,让我这个在pc端漂流的前端er不免心生仰慕,的确入行几多年,也该是时候进军移动web了。移动web中踩到的第一个坑就是事件问题,所以在吸取众大神的经验后,特作总结以示后来者。

首先pc端那一堆非常happy的鼠标事件没了,<code>mousedown</code>, <code>mouseup</code>, <code>mousemove</code>, <code>mouseover</code>, <code>mouseout</code>, <code>mouseenter</code>, <code>mouseleave</code>全都没了,<code>click</code>也与之前有所差别。取而代之的是几个原始的事件。

-touchstart

-touchmove

-touchend

-touchcancel

同样事件处理函数中的<code>event</code>也与pc端有着极大的差别,最典型的是增加了三个与触摸相关的属性:

-touches

-changedtouches

-targettouches

在pc端一台机器只会有一个鼠标,所以与鼠标相关的属性都可以放到一个event对象上,但是移动端设备大多支持多点触控,这就意味着一个事件可能与多个触控点相关,每个触控点都需要记录自己单独的属性。所以event对象中与touch相关的三个属性都是<code>touchlist</code>类型,与触控位置、目标元素、全都放到了touch对象上。

touch对象主要属性如下:

-clientx / clienty:触摸点相对浏览器窗口的位置

-pagex / pagey:触摸点相对于页面的位置

-screenx / screeny:触摸点相对于屏幕的位置

-identifier:touch对象的id

-target:当前的dom元素

现在反过来看看几个touch相关事件,并与pc端事件做一下对比:

-<code>touchstart</code>: 触控最开始时发生,类似于pc端的mousedown事件

-<code>touchmove</code>: 触控点在屏幕上移动时触发,类似于mousemove。但是在当在移动设备上,触控点从一个元素移动到另一个元素上时,并不会像pc端一样触发类似<code>mouseover</code>/<code>mouseout</code> <code>mouseenter</code>/<code>mouseleave</code>的事件。

-<code>touchend</code>: 在触摸结束时触发,类似mouseup

-<code>touchcancel</code>: 当一些更高级别的事情发生时,浏览器会触发该事件。比如突然来了一个电话,这时候会触发touchcanel事件。如果是在游戏中,就要在touchcancel时保存当前游戏的状态信息。

-<code>click</code>: 移动端的click事件虽然存在,但与pc端有着明显的差异。这也就是著名的300ms问题,以及为了解决300ms延迟带来的点透问题。

这几个事件的事件对象的target属性永远是触控事件最先发生的那个元素

先把click的问题放一下,我们先考虑以下能否在移动端模拟pc事件呢?答案是可以的。首先我们需要定义一下标准事件:

press -&gt; mousedown release -&gt; mouseup

move -&gt; mousemove

cancel -&gt; mouseleave

over -&gt; mouseover

out -&gt; mouseout

enter -&gt; mouseenter

leave -&gt; mouseleave

总体看来如下图所示:

移动Web触控事件总结

在我们定义好标准时候就要考虑如何去实现,值得庆幸的是,事件的传播阶段并没有变化,这里要感谢微软不来添乱。盗一张图:

移动Web触控事件总结

我们先来看<code>toucmove</code>,单看名字容易让人想当然的认为它与mousemove对应,然后上文说过了,当触控点在不同元素上移动时,并不会触发<code>mouseover</code>/<code>mouseout</code> <code>mouseenter</code>/<code>mouseleave</code>等事件,为了实现上面所说的<code>over</code>, <code>out</code>, <code>enter</code>, <code>leave</code>我们首先要能够在<code>touchmove</code>中拿到当前位置的dom元素。

浏览器为我们提供了<code>elementfrompoint</code>方法,这个函数根据<code>clientx</code>/<code>clienty</code>来选中最上层的dom元素,这为我们在touchmove中实时获取最近的dom元素提供了保障。当触控点从一个元素移动到另一个元素上时,对移出元素触发<code>mytouchout</code>事件对移入元素触发<code>mytouchover</code>事件,同时对与触摸元素当触控点在其上移动时触发<code>mytouchmove</code>事件。

移动Web触控事件总结

关于自定义事件,当然是使用createevent, initevent, dispatchevent三个函数,这三个函数并不是本文重点,请大家自行查阅《javascript高级程序设计第三版》13章中关于自定义事件的内容。

如此一来,我们的move、over、out等事件就有了着落,而press也非常简单,只需要绑定touchstart即可,同样cancel也只需要绑定touchcancel即可。

对于release我们不能简单的绑定touchend。因为上文已经说过,touchend中touch的target属性对应的是最初触控的元素,并不会随着触控点位置而改变。即是最终在元素b上拿开手指,touchend仍然会发生在元素a上。所以我们需要在touchend时,利用<code>elementfrompoint</code>获取最后触摸元素,在它身上触发<code>mytouchend</code>事件来模拟release。

根据事件传播的三个阶段,最适合做这些事的阶段应位于冒泡阶段,代码如下:

首先定义事件绑定与发射函数:

然后模拟mouse事件,分别在document上添加<code>touchstart</code>, <code>touchmove</code>, <code>touchend</code>的事件处理:

到目前为止标准化事件基本完成,剩下的就是enter与leave事件。这两个事件与over、out类似,区别就是enter与leave在touch进入或者离开子元素时并不冒泡到父元素上,而over与out会冒泡到父元素。所以我们只要在over与out上稍加变通即可:如果<code>evt.relatedtarget</code>是子元素则父元素不触发事件,核心函数如下:

综上,我们的标准化事件过程就全部完成了:

在最初移动web刚出现时,用户双击时网页会自动放大,所以为了区分双击缩放与click事件,浏览器设置了一个间隔时间300ms。如果300ms内连续点击2次则认为是双击缩放,否则是单击click,浏览器内部实现原理如下所示

移动Web触控事件总结

在实际应用中发现,300ms并不是绝对发生,当用户设置了viewport并禁止缩放时,大部分浏览器会禁止300ms延迟,但在低版本安卓以及微信、qq等应用的内嵌webview中仍然会发生300ms延迟问题。

在现今分秒必争的移动端,如果网页在100ms之内没有反应就会给用户迟钝的感觉,更何况300ms,根据上文我们可以简单的使用<code>press</code>事件来解决问题。与click相比,press的间隔时间明显缩短。但这也带来了移动端另一个经典问题:点透!

点透的经典例子是:在遮罩层下有一个button或者文本框,在遮罩层上绑定press事件,当press发生时,事件函数中清除遮罩层。这样业务场景下,当press时,遮罩层会消失,这是正常的,但是300ms后,遮罩层下方的元素触发了click事件。

发生这件事的原因在于,press发生后遮罩层被清除,300ms后,浏览器找到当前最上层元素,触发click事件,过程原理如下:

如果我们全部依赖press而不去绑定click事件,是否可行呢?答案是否定的,因为press只对应<code>touchstart</code>,如果用户一直按住不放,或者先按住在滑到别的元素上,这不能认为是一次click事件。那么我们是否可以像自定义<code>mytouch*</code>等事件那样来定义自己的<code>click</code>事件呢?答案是可行的!

我们可以认为当触控点击开始并且在结束时所经过的事件不超过300ms而且移动位置不超过4px,则这次事件就是一次完整的click事件。

这个过程涉及touchstart、touchmove和touchend三个事件,首先绑定document的touchstart事件:

整个过程核心逻辑在于dofastclick函数中:

现在我们添加了自定义的click事件,那么问题来了在我们的自定义click中不会存在300ms延迟,但是现在浏览器存在两个click事件,一个是我们定义的,一个是原生的click事件。原生的click事件仍然会在300ms后执行,当你对一个元素绑定click事件时,一次click通常会触发两次click事件,这也是另一个经典的<code>鬼点击问题</code>。所以我们需要将原生的click事件彻底禁止掉。根据事件的三个处理阶段,最合适的处理地方在于捕获阶段,阻止原生click的继续传播和默认行为。

现在鬼点击的问题解决了,但是实践发现

移动浏览器仍然保留<code>mousedown</code>与<code>mouseup</code>事件,这两个事件仍然存在300ms延迟的问题!!!当遮罩层的下方是一个文本框时,300ms后mousedown发生,键盘就是在<code>mousedown</code>的时候弹出的!所以我们需要把mousedown事件一起禁掉。

那么事情结束了么?然并卵,如果将mousedown禁掉,你的input文本框永远不会再弹出键盘!!!所以我们需要做一下判断,如果是文本框不能<code>preventdefault</code>:

总结一下,目前我还没有发现完美的解决方案,也就是说如果你的移动浏览器没有禁用300ms延迟,如果你的遮罩层下方是个文本框,如果你的业务刚好满足点透的业务场景。。。貌似没有完美的方式阻止键盘弹出。或者可以使用缓动动画,过渡300ms。

继续阅读