使用指针事件处理多端设备“定点”输入问题

如果一套代码既要兼容PC端又要兼容移动端的话,经常会遇到这样一个痛点:需要同时处理mouse和touch事件,并且在移动端上点击时除了触发touch事件外还会触发mouse事件。另外,有些PC是可触屏的,它既可以用使用鼠标操作,还可以用手势触摸操作,这种也是需要同时支持mouse和touch事件。

如果我们只是处理纯点击click的话,问题不大,但如果需要区分按下和抬起,例如按下时做一个缩小动画,抬起时做一个放大动画,又或者需要处理移动时,那么问题就来了。

问题:mousedown和touchstart会一起触发

一般我们想到最简单的解决方法是同时监听mouse事件和touch事件,如下代码所示:

然而,在移动端上,mousedown和touchstart会一起触发,导致回调被执行两次。我们可以做一个简单的实验,给一个元素绑定所有的mouse和touch事件,然后在一台iOS设备上点击,观察事件的打印顺序,如下图所示:

我们看到,除了触发touch事件之外,还触发了mouse事件,从而导致上面的回调会执行两次。

这里有一个问题,为什么移动端上还要触发mouse事件呢?根据MDN的解释

现在绝大多数的web内容都是为鼠标操作而设计的。因此,即使浏览器支持触屏,也必须要模拟(emulate)鼠标事件,这样即使是那些只能接受鼠标输入的内容,也不需要进行额外修改就可以正常工作

也就是说,为了让PC的页面能够在触屏设备上正常使用,即使这些页面没有处理touch事件。同时文档也给出了解决方法,如果你不需要mouse事件,那么在touchstart回调里面调用preventDefault即可。

解决方法1:touchstart回调里调用preventDefault

这应该是成本最低的一种解决方式,如下代码所示:

加上preventDefault的调用之后,我们再做个实验,观察控制台输出,结果如下图所示:

可以看到,所有的mouse事件都不会触发了,达到了我们想要的状态。那么,在touchstart里面调用preventDefault会有什么副作用吗?

首先是元素所在区域无法滑动了,如果你按下的位置刚好是当前目标元素。其次是click事件也不会触发和冒泡了,这样造成的问题是,如果你有一些通用的处理,如一些通过容器监听click发送埋点的处理,就无法进行了。也就是说,你改变了点击的默认行为,那么多少会造成一些副面效果。当然,如果这两者都无需考虑的话,preventDefault应该是最简单的处理方式了。

如果你真的不想改变默认行为的话,那还可以怎么办呢?

解决方法2:只监听mousedown事件

我们注意到一个点,不管是触摸还是鼠标点击,都会触发mousedown,所以我们只监听mousedown不就可以了?

理论上是可行的,但是你在移动端上面处理mousedown事件,这种事情想来也是奇怪,会有什么问题吗?

有的,很明显,因为mousedown是有延迟的,是在touchend之后触发的,我们可以把各个事件相对于touchstart延迟的时间打印出来,如下图所示:

可以看到,mousedown事件大概延迟了100ms触发,所以这就造成了体验上的问题,因此,这个方法不太推荐。

解决方法3:touchstart事件之后,忽略掉下一次mousedown事件

这个方法的思想是,如果触发了touchstart,那么下一次mousedown就不要响应了,因为toustart会顺带着触发一次mousedown,所以我们把下一次的mousedown忽略了不就好了么?这个方法实现起来有点麻烦,如下代码所示:

这样在可触屏PC上,既可以使用鼠标操作,也可以用手势控制,两个互不冲突。但是当这种用来做为flag的变量用多了之后,如果管理不当的话,容易造成状态上的混乱。例如有一个场景是在触发屏上,手按住目标元素保持不动,然后隔一小会再抬起,这个时候是不会触发mouse事件的,如下图所示:

所以重置flag变量的时机需要修改。这个方法也不是很理想,需要很多额外的处理。

解决方法4:使用指针事件pointer events

CSS有一个pointer-events属性,JS也有一套pointer events。它的出现就是为了解决不同设备上“定点”输入不一致问题,提供一套统一的事件。常用的有pointerdown、pointermove和pointerup等,具体可查看MDN文档

实际使用如下代码所示:

这样我们就无需关心是touch触发的还是mouse触发的了,瞬间代码就清爽了。

但是这个事件有兼容性问题,主要是iOS 13以下不支持,这个是最大的阻力(就连IE 11都支持了),我们不能很愉快地直接就切到指针事件。不过网上有一些polyfill的库,如github star数很高的由jQuery官方推出的一个库pep.js。这个库我看了源代码,里面处理了不同浏览器兼容的问题,甚至处理了shadow DOM的元素事件的触发,以及很好地构造和还原PointerEvent里面的各个属性。

它的原理是通过监听原生的touch和mouse事件,然后手动去触发(fire)一个pointer事件,类似于fastclick。

但是在使用上发现了一些问题,主要是在触屏设备上有一些表现和原生的PointerEvent表现不一致,可以简单分析一下,包括有:

(1)触屏设备上需要给要使用指针事件的元素添加touch-action属性,如:

表明目标元素不会滑动、放大等,这样它才能放心地触发指针事件。并且这个添加需要在库初始之前,因为它是通过document.querySelectorAll(‘[touch-action]’)获取到所有需要处理的元素,所以后面动态添加的元素就不会处理了。它在非触屏PC上则是通过mouse事件冒泡到document处理的,所以如果事件被阻止冒泡的话就无法使用了,这一点在它的说明文档也有提示。

(2)它会给支持原生PointerEvent的设备上的元素添加一个touch-action的CSS属性:

这里的touch-action属性是在第(1)点的时候添加的,而不支持的设备则不会添加这段CSS代码。如上面代码,加上touch-action: none的后果是不能滑动了。所以如果你要垂直滑动的话,那么要改成touch-action: pan-y.

加上这段代码的目的应该是为了让支持原生的表现和polyfill之后的表现一致,这样就会有点奇怪,为了追求行为一致而迫使我们去改变原生的行为。

(3)event.isTrusted属性始终是false,无法区分是人为点的,还是代码触发的。

所以,没法通过加一个polyfill的库就一劳永逸了。这里我当前采用的方法是降级到touch事件,如果不支持PointerEvent的话,如下代码所示:

因为不支持指针事件的主要是iOS设备。

需要注意指针事件和mouse、touch事件的一些不一样的地方。

指针事件和常规事件的一些表现不一致地方

(1)在PC上鼠标中键、右键按下都会触发pointerdown事件,需要结合event.button和event.buttons属性区分是否是左键

(2)如果目标元素在一个可滚动的容器里面滑动的时候,touchstart、touchmove和touchend都会触发,但是指针事件只会触发pointerdown,而不会触发pointermove和pointerup.

(3)指针事件可以通过pointerType得到触发事件的类型:mouse、touch、pen,如果是触摸笔pen的话,还可以通过pressure得到按压力度:

touch事件通过touches[0].presssure也可以得到按压力度。

小结

综上,指针事件是一个处理多端设备“定点”输入的一套机制,使用上遇到的问题主要是iOS设备目前兼容性不太好,所以仍然需要一些额外的处理。指针事件在处理多端设备“定点”问题上是一个标准和趋势。

 

发表评论

邮箱地址不会被公开。