指针事件(Pointer Events)是一种用于处理来自各种输入设备(如:鼠标、触控笔、触摸屏等)的输入信息的现代化解决方案

一段简史

很早以前,只存在鼠标事件

后来,触屏设备开始普及,尤其是手机和平板电脑。为了使现有的脚本仍能正常工作,它们生成(现在仍生成)鼠标事件。例如,轻触屏幕就会生成 mousedown 事件。因此,触摸设备可以很好地与网页配合使用

但是,触摸设备比鼠标具有更多的功能。例如,我们可以同时触控多点(多点触控)。然而,鼠标事件并没有相关属性来处理这些

因此,引入了触摸事件,例如 touchstarttouchend 和 touchmove,它们具有特定于触摸的属性(这里不再赘述这些特性,因为指针事件更加完善)

不过这还是不够完美。因为很多其他输入设备(如触控笔)都有自己的特性。而且同时维护两份分别处理鼠标事件和触摸事件的代码,显得有些笨重了

为了解决这些问题,引入了全新的规范——指针事件。它为各种指针输入设备提供了一套统一的事件

目前,各大主流浏览器已经支持了 Pointer Events Level 2 标准,版本更新的 Pointer Events Level 3 已经发布,并且大多数情况下与 Pointer Events Level 2 兼容

因此,除非你写的代码需要兼容旧版本的浏览器,例如 IE 10 或 Safari 12 或更低的版本,否则无需继续使用鼠标事件或触摸事件,而是使用指针事件

这样,代码就可以在触摸设备和鼠标设备上都能正常工作了

话虽如此,指针事件仍然有一些重要的奇怪特性,应当对它们有所了解以正确使用指针事件,并避免一些意料之外的错误,下文会对它们进行介绍

指针事件

指针事件的命名方式和鼠标事件类似:

指针事件类似的鼠标事件
pointerdownmousedown
pointerupmouseup
pointermovemousemove
pointerovermouseover
pointeroutmouseout
pointerentermouseenter
pointerleavemouseleave
pointercancel-
gotpointercapture-
lostpointercapture-

不难发现,每一个 mouse<event> 都有与之相对应的 pointer<event>

pointer<event> 替换 mouse<event>

可以把代码中的 mouse<event> 都替换成 pointer<event>,程序仍然正常兼容鼠标设备

替换之后,程序对触屏设备的支持会“魔法般”地提升。但是,可能需要在 CSS 中的某些地方添加 touch-action: none,见 指针中断

指针中断

pointercancel:一个正处于活跃状态的指针交互由于某些原因被中断时触发。也就是在这个事件之后,该指针就不会继续触发更多事件了

导致指针中断的可能原因如下:

  • 指针设备硬件在物理层面上被禁用
  • 设备方向旋转
  • 浏览器打算自行处理这一交互,比如将其看作是一个专门的鼠标手势或缩放操作等

例子:图片拖拽

  1. 用户按住了一张图片,开始拖拽 —— pointerdown 事件触发
  2. 用户开始移动指针(从而拖动图片)—— pointermove 事件持续触发
  3. 然后意料之外的情况发生了!浏览器有自己原生的图片拖放操作,接管了之前的拖放过程,于是触发了 pointercancel 事件
    • 现在拖放图片的操作由浏览器自行实现
    • 我们不会再得到 pointermove 事件了

解决:阻止浏览器的默认行为来防止 pointercancel 触发

  1. 阻止原生的拖放操作发生:img.ondragstart = () => false
  2. 对于触屏设备,还有其他和触摸相关的浏览器行为(除了拖放)。为了避免它们所引发的问题:
    • 通过在 CSS 中设置 touch-action: none 来阻止它们

指针捕获

elem.setPointerCapture(pointerId):将给定的 pointerId 绑定到 elem。在调用之后,所有具有相同 pointerId 的指针事件都将 elem 作为目标(就像事件发生在 elem 上一样)

绑定会在以下情况下被移除:

  • 当 pointerup 或 pointercancel 事件出现时,绑定会被自动地移除
  • 当 elem 被从文档中移除后,绑定会被自动地移除
  • 当 elem.releasePointerCapture(pointerId) 被调用,绑定会被移除

相关事件:

  • gotpointercapture:在一个元素使用 setPointerCapture 来启用捕获后触发
  • lostpointercapture:在捕获被释放后触发(以上几种情况)

指针捕获可以被用于简化拖放类的交互,例如滑块:

实现逻辑:

  1. 用户按下滑动条的滑块 thumb —— pointerdown 事件触发
  2. 然后用户移动指针 —— pointermove 事件触发
    • 让移动事件只作用在 thumb 上
    • 在指针的移动过程中,指针可能会离开滑动条的 thumb 元素,移动到 thumb 之上或之下的位置。而 thumb 应该严格在水平方向上移动,并与指针保持对齐
    • 所以必须在整个文档 document 上分配 mousemove 事件处理程序

不过,这并不是一个没有副作用的解决方案。问题是,指针在文档周围的移动可能会引起副作用:在其他元素上触发事件处理程序(例如 mouseover)并调用其他元素上与滑动条不相关的功能,这不是预期的效果

这就是指针捕获适用的场景:

  • 在 pointerdown 事件的处理程序中调用 thumb.setPointerCapture(event.pointerId)
  • 这样接下来在 pointerup/cancel 之前发生的所有指针事件都会被重定向到 thumb 上
  • 当 pointerup 发生时(拖动完成),绑定会被自动移除

因此,即使用户在整个文档上移动指针,事件处理程序也将仅在 thumb 上被调用。尽管如此,事件对象的坐标属性,例如 clientX/clientY 仍将是正确的,捕获仅影响 target/currentTarget

事件对象

PointerEvent 派生于 MouseEvent,拥有所有的 MouseEvent 特性,且增加了:

  • pointerType: string:指针的设备类型
    • mouse:鼠标
    • pen:触控笔
    • touch:触摸屏
  • pointerId: number:触发当前事件的指针唯一标识符,用于多点触控
  • isPrimary: boolean:当指针为首要指针(多点触控时按下的第一根手指)时为 true
    • 通过 pointerIdisPrimary 即可处理多点触控

有些指针设备会测量接触面积和点按压力,可以使用以下属性:

  • width/height: number:指针(例如手指)接触设备的区域的宽高,单位像素
    • 对于不支持的设备(如鼠标)为 1
  • pressure: number:归一化后的触摸压力,范围在 [0, 1]
    • 对于不支持压力检测的设备总是 0.5(按下时)或 0
  • tangentialPressure:归一化后的切向压力(也称为桶压或 cylinder stress),范围在 [-1, 1]
    • 0 表示控制设备中立状态时的值
  • tiltX:由输入设备(如手写笔)与 Y 轴构成的平面,和 Y-Z 平面之间的夹角,范围在 [-90, 90]
  • tiltY:由输入设备(如手写笔)与 X 轴构成的平面,和 X-Z 平面之间的夹角,范围在 [-90, 90]
  • twist:输入设备(如手写笔)围绕自身主轴顺时针旋转的角度,取值范围是 [0, 359]