如何防止触摸事件的默认处理?

38

我正在尝试做类似于嵌入式 Google 地图的事情。我的组件应该忽略单指触摸(允许用户滚动页面)和在组件之外的捏合手势(允许用户缩放页面),但应该对双指触摸作出反应(允许用户在组件内导航),并在这种情况下禁止任何默认操作。

我如何阻止触摸事件的默认处理,但只在用户用两个手指与我的组件交互时?

我尝试过:

  1. 我尝试捕获 onTouchStartonTouchMoveonTouchEnd。结果发现,在 Firefox Android 上,当在组件上进行捏合手势时,第一个触发的事件是带有单指触摸的 onTouchStart,然后是带有两个触摸的 onTouchStart,然后是 onTouchMove。但在 onTouchMove 处理程序中调用 event.preventDefault()event.stopPropagation() 并不能(总是)停止页面缩放/滚动。在第一次调用 onTouchStart 时阻止事件升级确实有帮助——不幸的是,那时我还不知道是否会是多点触控,所以我不能使用这个方法。

  2. 第二种方法是在 document.body 上设置 touch-action: none。这在 Chrome Android 上可以工作,但我只能在 Firefox Android 上将其设置在所有元素上(除了我的组件)。因此,虽然这是可行的,但它似乎可能会产生不需要的副作用和性能问题。编辑:进一步测试发现,如果在触摸开始前设置 CSS,则此方法仅适用于 Chrome。换句话说,如果我在检测到 2 根手指时注入 CSS 样式,则 touch-action 将被忽略。因此,这在 Chrome 上没有用处。

  3. 我还尝试在组件挂载时添加一个新的事件监听器:

    document.body.addEventListener("touchmove", ev => {
      ev.preventDefault();
      ev.stopImmediatePropagation();
    }, true);
    

    (对于touchstart同样适用)。在Firefox Android中这样做可以工作,但在Chrome Android上则没有任何效果。

我已经想不出更好的办法了。是否有一种可靠的跨浏览器方法来实现Google显然所做的事情,或者他们使用了多个技巧并在每个浏览器上进行了大量测试才使其正常工作?如果有人指出我的方法存在错误或提出新的方法,我将不胜感激。

4个回答

57
TL;DR: 我在注册事件处理程序时漏了{ passive: false }
我在Chrome中使用preventDefault()时遇到的问题是由于他们的scrolling "干预"(即:破坏Web IE风格)。简而言之,因为不调用preventDefault()的处理程序可以更快地处理,所以addEventListener添加了一个名为passive的新选项。如果设置为true,则事件处理程序保证不会调用preventDefault(如果调用,则将被忽略)。然而,Chrome决定更进一步,使{passive:true}成为默认值(自版本56起)。
解决方案是显式将事件侦听器调用passive设置为false
window.addEventListener('touchmove', ev => {
  if (weShouldStopDefaultScrollAndZoom) {
    ev.preventDefault();
    ev.stopImmediatePropagation();
  };
}, { passive: false });

请注意,这会对性能产生负面影响。
顺便提一下,我似乎误解了CSS的touch-action属性,但我仍然无法使用它,因为它需要在触摸序列开始之前设置。但如果不是这种情况,它可能更加高效,并且似乎支持所有适用平台(Safari在Mac上不支持它,但在iOS上支持)。这篇文章最好地阐述了这一点:

对于您的情况,您可能希望将您的文本区域(或其他内容)标记为'touch-action:none',以禁用滚动/缩放而不禁用所有其他行为。

CSS属性应该在组件上设置,而不是在document上设置,就像我所做的那样:
<div style="touch-action: none;">
  ... my component ...
</div>

在我的情况下,我仍然需要使用“被动”事件处理程序,但知道有哪些选项是很好的……希望能对某些人有所帮助。

我真的很想从中得到一个更简洁的答案。目前,我实现了一个额外的 touchmove 事件并向组件添加了 CSS。这两个是否都是必需的,或者我们可以不用在组件上使用 CSS? - T.Woody
1
只需要其中之一。CSS 的 touch-action 选项性能更佳(且更好),但只有在您事先设置它(在 touchstart 事件之前)时才能使用。在我的情况下,这不是一个选择,因为我需要根据手指数量实现不同的行为,所以我必须等待 touchstart 事件。 - johndodo
如果使用指针事件(pointerdownpointermove),则 preventDefaultstopImmediatePropagation 对默认触摸操作没有任何影响。必须使用 touchstarttouchmove 显式地防止默认操作。 - The Conspiracy

1

尝试使用if语句来判断是否有多个触摸点:

document.body.addEventListener("touchmove", ev => {
  if (ev.touches.length > 1) {
    ev.preventDefault();
    ev.stopImmediatePropagation();
  }
}, true);

谢谢!我已经尝试过了(请参见我的问题中的第3点)。然而,这并不能防止在Android Chrome上滚动/缩放。请注意,我有困难防止默认操作(在检测到2个手指后),而不是弄清楚是否需要防止它。 - johndodo

1
这是我的想法:
我使用一个具有opacity: 0div来覆盖map,并且该divz-index大于mapz-index
然后我会在covered div中检测触摸事件。如果我检测到2 fingers在此covered div上触摸,则会将此divdisplay: none以允许用户在此地图上使用两个手指。
否则,在document touchEnd中,我将使用display: block恢复此covered div,以确保我们可以滚动。

-1
在我的情况下,我已经用以下方法解决了它。
@HostListener('touchmove', ['$event'])
    public onTouch(event: any): void {
    event.stopPropagation();
    console.log('onTouch', event);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接