25

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

 3 years ago
source link: https://www.yinchengli.com/2020/12/27/pointer-events/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

问题:mousedown和touchstart会一起触发

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

div.addEventListener('mousedown', handlePressDown);
div.addEventListener('touchstart', handlePressDown);

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

3eIzmeR.png!mobile

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

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

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

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

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

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

div.addEventListener('touchstart', event => {
  event.preventDefault();
  handlePressDown();
});

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

f2M3Ar.png!mobile

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

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

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

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

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

div.addEventListener('mousedown', handlePressDown);

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

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

umEbauj.png!mobile

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

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

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

let ignoreNextMouseDown = false;
 
div.addEventListener('touchstart', () => {
  handlePressDown();
  ignoreNextMouseDown = true;
});
 
div.addEventListener('mousedown', () => {
  if (ignoreNextMouseDown) {
    ignoreNextMouseDown = false;
    return;
  }
  handlePressDown();
});

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

y6R7BrY.png!mobile

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

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

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

实际使用如下代码所示:

div.addEventListener('pointerdown', event => {
  handlePressDown();
});

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

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

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

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

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

<div touch-action="none"></div>

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

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

2uyuEn2.png!mobile

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

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

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

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

// iOS 13以下没有pointer
let hasPointer = typeof window.PointerEvent !== 'undefined';
const pointerdown = hasPointer ? 'pointerdown' : 'touchstart';
div.addEventListener(pointerdown, event => {
  let clientX = event.touches ? event.touches[0].clientX : event.clientX;
});

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

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

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

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

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

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

vYRNNru.png!mobile

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

小结

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

Post Views: 4


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK