2024 年 1 月 27 日

指针事件

指针事件是处理各种指向设备(如鼠标、触控笔/手写笔、触摸屏等)输入的现代方式。

简要历史

我们来做一个简要概述,以便你了解指针事件在其他事件类型中的总体情况和位置。

  • 很久以前,过去,只有鼠标事件。

    然后触摸设备变得普遍,尤其是手机和平板电脑。为了让现有脚本工作,它们生成了(并且仍然生成)鼠标事件。例如,轻触触摸屏会生成 mousedown。因此,触摸设备可以很好地与网页配合使用。

    但触摸设备比鼠标有更多的功能。例如,可以同时触摸多个点(“多点触控”)。不过,鼠标事件没有处理此类多点触控所需的属性。

  • 因此引入了触摸事件,例如 touchstarttouchendtouchmove,它们具有特定于触摸的属性(我们在此处不详细介绍,因为指针事件更好)。

    不过,这还不够,因为还有许多其他设备,例如笔,它们有自己的功能。此外,编写同时侦听触摸和鼠标事件的代码很麻烦。

  • 为了解决这些问题,引入了新的标准指针事件。它为所有类型的指针设备提供了一组事件。

截至目前,所有主流浏览器都支持 指针事件 2 级 规范,而较新的 指针事件 3 级 正在开发中,并且与指针事件 2 级基本兼容。

除非你为旧浏览器(例如 Internet Explorer 10 或 Safari 12 或更低版本)开发,否则没有必要再使用鼠标或触摸事件 - 我们可以切换到指针事件。

然后,你的代码将可以很好地与触摸和鼠标设备配合使用。

也就是说,有一些重要的特性,人们应该知道才能正确使用指针事件并避免意外。我们将在本文中记下它们。

指针事件类型

指针事件的命名与鼠标事件类似

指针事件 类似的鼠标事件
pointerdown mousedown
pointerup mouseup
pointermove mousemove
pointerover mouseover
pointerout mouseout
pointerenter mouseenter
pointerleave mouseleave
pointercancel -
gotpointercapture -
lostpointercapture -

正如我们所看到的,对于每个 mouse<event>,都有一个 pointer<event> 扮演类似的角色。还有 3 个额外的指针事件没有对应的 mouse... 对应项,我们很快就会解释它们。

在我们的代码中用 pointer<event> 替换 mouse<event>

我们可以在我们的代码中用 pointer<event> 事件替换 mouse<event> 事件,并期望事情继续与鼠标配合良好。

对触摸设备的支持也将“神奇地”得到改善。不过,我们可能需要在 CSS 中的某些地方添加 touch-action: none。我们将在下面关于 pointercancel 的部分中介绍它。

指针事件属性

指针事件具有与鼠标事件相同的属性,例如 clientX/Ytarget 等,还有一些其他属性

  • pointerId – 导致事件的指针的唯一标识符。

    浏览器生成。允许我们处理多个指针,例如带手写笔和多点触控的触摸屏(后面会有示例)。

  • pointerType – 指针设备类型。必须是字符串,如下之一:“mouse”、“pen”或“touch”。

    我们可以使用此属性对各种指针类型做出不同的反应。

  • isPrimary – 对于主指针(多点触控中的第一个手指),为true

一些指针设备测量接触面积和压力,例如对于触摸屏上的手指,为此还有其他属性

  • width – 指针(例如手指)接触设备的区域的宽度。在不受支持的情况下,例如对于鼠标,它始终为1
  • height – 指针接触设备的区域的高度。在不受支持的情况下,它始终为1
  • pressure – 指针尖端的压力,范围为 0 到 1。对于不支持压力的设备,必须为0.5(按下)或0
  • tangentialPressure – 归一化的切向压力。
  • tiltXtiltYtwist – 特定于笔的属性,描述了笔相对于表面的位置。

大多数设备不支持这些属性,因此很少使用。如果需要,可以在 规范 中找到有关它们的详细信息。

多点触控

鼠标事件完全不支持的一件事是多点触控:用户可以在手机或平板电脑上同时在多个地方触摸,或执行特殊手势。

指针事件允许借助pointerIdisPrimary属性处理多点触控。

当用户在一个地方触摸触摸屏,然后将另一个手指放在其他地方时,会发生以下情况

  1. 在第一个手指触摸时
    • pointerdown,其中isPrimary=true,并且有一些pointerId
  2. 对于第二个手指和更多的手指(假设第一个手指仍在触摸)
    • pointerdown,其中isPrimary=false,并且每个手指都有不同的pointerId

请注意:pointerId不是分配给整个设备,而是分配给每个触摸的手指。如果我们用 5 个手指同时触摸屏幕,我们将有 5 个pointerdown事件,每个事件都有各自的坐标和不同的pointerId

与第一个手指关联的事件始终具有isPrimary=true

我们可以使用它们的pointerId跟踪多个触摸手指。当用户移动然后移除手指时,我们会得到与pointerdown中相同的pointerIdpointermovepointerup事件。

下面是记录pointerdownpointerup事件的演示

请注意:您必须使用触摸屏设备(例如手机或平板电脑)才能真正看到pointerId/isPrimary的差异。对于单点触控设备(例如鼠标),所有指针事件的pointerId始终相同,isPrimary=true

事件:pointercancel

当正在进行指针交互时,pointercancel事件触发,然后发生某些事情导致其中止,因此不会再生成更多指针事件。

此类原因包括

  • 指针设备硬件已物理禁用。
  • 设备方向已更改(平板电脑已旋转)。
  • 浏览器决定自行处理交互,将其视为鼠标手势或缩放和平移操作或其他操作。

我们将在实际示例中演示pointercancel,以了解它如何影响我们。

假设我们正在为球实现拖放,就像文章开头所述使用鼠标事件进行拖放

以下是用户操作流程和相应事件

  1. 用户按图像以开始拖动
    • pointerdown事件触发
  2. 然后他们开始移动指针(从而拖动图像)
    • pointermove触发,可能多次触发
  3. 然后惊喜发生了!浏览器具有对图像的原生拖放支持,它会启动并接管拖放过程,从而生成pointercancel事件。
    • 浏览器现在自行处理图像的拖放。用户甚至可以将球图像从浏览器拖到其邮件程序或文件管理器中。
    • 我们不会再收到pointermove事件。

因此,问题在于浏览器“劫持”了交互:pointercancel在“拖放”过程的开始触发,并且不再生成pointermove事件。

以下是拖放演示,其中记录了指针事件(仅up/downmovecancel)在textarea

我们希望自行实现拖放,因此让我们告诉浏览器不要接管它。

防止默认浏览器操作以避免pointercancel

我们需要做两件事

  1. 防止发生原生拖放
    • 我们可以通过设置ball.ondragstart = () => false来实现此目的,正如文章使用鼠标事件进行拖放中所述。
    • 这对于鼠标事件非常有效。
  2. 对于触摸设备,还有其他与触摸相关的浏览器操作(除了拖放)。为了避免出现问题
    • 通过在 CSS 中设置 #ball { touch-action: none } 来阻止它们。
    • 然后,我们的代码将开始在触摸设备上运行。

在执行此操作后,事件将按预期工作,浏览器不会劫持该进程,也不会发出 pointercancel

此演示添加了这些行

如你所见,不再有 pointercancel

现在,我们可以添加代码来实际移动球,我们的拖放功能将适用于鼠标设备和触摸设备。

指针捕获

指针捕获是指针事件的一项特殊功能。

这个想法非常简单,但乍一看可能显得相当奇怪,因为对于任何其他事件类型都不存在类似的功能。

主要方法是

  • elem.setPointerCapture(pointerId) – 将具有给定 pointerId 的事件绑定到 elem。在调用之后,具有相同 pointerId 的所有指针事件都将以 elem 为目标(就像发生在 elem 上一样),无论它们在文档中的实际发生位置如何。

换句话说,elem.setPointerCapture(pointerId) 将具有给定 pointerId 的所有后续事件重新定位到 elem

在以下情况下,绑定将被移除

  • 当发生 pointeruppointercancel 事件时,自动移除
  • 当从文档中移除 elem 时,自动移除
  • 当调用 elem.releasePointerCapture(pointerId) 时,自动移除

现在它有什么好处?是时候看一个真实的例子了。

指针捕获可用于简化拖放之类的交互。

让我们回顾一下如何实现自定义滑块,如 使用鼠标事件进行拖放 中所述。

我们可以创建一个 slider 元素来表示其中的条带和“滑块”(thumb

<div class="slider">
  <div class="thumb"></div>
</div>

使用样式,它看起来像这样

以下是工作逻辑,如所述,在用类似的指针事件替换鼠标事件之后

  1. 用户按下滑块 thumb – 触发 pointerdown
  2. 然后,他们移动指针 – 触发 pointermove,我们的代码会随之移动 thumb 元素。
    • …随着指针移动,它可能会离开滑块thumb元素,到其上方或下方。thumb应该严格水平移动,与指针保持对齐。

在基于鼠标事件的解决方案中,为了跟踪所有指针移动,包括当它到thumb上方/下方时,我们必须在整个document上分配mousemove事件处理程序。

但这并不是最干净的解决方案。其中一个问题是,当用户在文档周围移动指针时,它可能会触发其他一些元素上的事件处理程序(例如mouseover),调用完全不相关的 UI 功能,而我们不希望发生这种情况。

这是setPointerCapture发挥作用的地方。

  • 我们可以在pointerdown处理程序中调用thumb.setPointerCapture(event.pointerId)
  • 然后,直到pointerup/cancel的未来指针事件将重新定位到thumb
  • pointerup发生(拖动完成)时,绑定会自动移除,我们不必关心它。

因此,即使用户在整个文档中移动指针,事件处理程序也会在thumb上调用。不过,事件对象的坐标属性(例如clientX/clientY)仍然是正确的——捕获只影响target/currentTarget

以下是基本代码

thumb.onpointerdown = function(event) {
  // retarget all pointer events (until pointerup) to thumb
  thumb.setPointerCapture(event.pointerId);

  // start tracking pointer moves
  thumb.onpointermove = function(event) {
    // moving the slider: listen on the thumb, as all pointer events are retargeted to it
    let newLeft = event.clientX - slider.getBoundingClientRect().left;
    thumb.style.left = newLeft + 'px';
  };

  // on pointer up finish tracking pointer moves
  thumb.onpointerup = function(event) {
    thumb.onpointermove = null;
    thumb.onpointerup = null;
    // ...also process the "drag end" if needed
  };
};

// note: no need to call thumb.releasePointerCapture,
// it happens on pointerup automatically

完整演示

在演示中,还有一个带有onmouseover处理程序的附加元素,显示当前日期。

请注意:当您拖动拇指时,您可能会悬停在此元素上,并且其处理程序不会触发。

因此,由于setPointerCapture,拖动现在没有副作用。

最后,指针捕获为我们带来了两个好处

  1. 代码变得更简洁,因为我们不再需要在整个document上添加/移除处理程序。绑定会自动释放。
  2. 如果文档中还有其他指针事件处理程序,当用户拖动滑块时,它们不会被指针意外触发。

指针捕获事件

为了完整起见,这里还有一件事需要提及。

有两个事件与指针捕获相关

  • 当元素使用setPointerCapture启用捕获时,将触发gotpointercapture
  • 当捕获被释放时,将触发lostpointercapture:要么通过releasePointerCapture调用显式释放,要么在pointerup/pointercancel上自动释放。

总结

指针事件允许使用单段代码同时处理鼠标、触摸和笔事件。

指针事件扩展了鼠标事件。我们可以在事件名称中用pointer替换mouse,并期望我们的代码继续适用于鼠标,同时更好地支持其他设备类型。

对于浏览器可能决定劫持并自行处理的拖放和复杂触摸交互,请记住取消事件上的默认操作,并为我们参与的元素在 CSS 中设置 touch-action: none

指针事件的其他功能是

  • 使用 pointerIdisPrimary 的多点触控支持。
  • 特定于设备的属性,例如 pressurewidth/height 等。
  • 指针捕获:我们可以将所有指针事件重新定位到特定元素,直到 pointerup/pointercancel

截至目前,所有主流浏览器都支持指针事件,因此我们可以安全地切换到它们,特别是如果不需要 IE10 和 Safari 12。即使使用这些浏览器,也有支持指针事件的 polyfill。

教程地图

评论

在评论之前阅读此内容…
  • 如果你有改进建议,请 提交 GitHub 问题或提交拉取请求,而不是发表评论。
  • 如果你无法理解文章中的内容,请详细说明。
  • 要插入几行代码,请使用 <code> 标记,对于多行代码,请用 <pre> 标记将其包装起来,对于超过 10 行的代码,请使用沙盒 (plnkrjsbincodepen…)