指针事件(Pointer Events)是一种用于处理来自各种输入设备(如:鼠标、触控笔、触摸屏等)的输入信息的现代化解决方案
一段简史
很早以前,只存在鼠标事件
后来,触屏设备开始普及,尤其是手机和平板电脑。为了使现有的脚本仍能正常工作,它们生成(现在仍生成)鼠标事件。例如,轻触屏幕就会生成 mousedown
事件。因此,触摸设备可以很好地与网页配合使用
但是,触摸设备比鼠标具有更多的功能。例如,我们可以同时触控多点(多点触控)。然而,鼠标事件并没有相关属性来处理这些
因此,引入了触摸事件,例如 touchstart
、touchend
和 touchmove
,它们具有特定于触摸的属性(这里不再赘述这些特性,因为指针事件更加完善)
不过这还是不够完美。因为很多其他输入设备(如触控笔)都有自己的特性。而且同时维护两份分别处理鼠标事件和触摸事件的代码,显得有些笨重了
为了解决这些问题,引入了全新的规范——指针事件。它为各种指针输入设备提供了一套统一的事件
目前,各大主流浏览器已经支持了 Pointer Events Level 2 标准,版本更新的 Pointer Events Level 3 已经发布,并且大多数情况下与 Pointer Events Level 2 兼容
因此,除非你写的代码需要兼容旧版本的浏览器,例如 IE 10 或 Safari 12 或更低的版本,否则无需继续使用鼠标事件或触摸事件,而是使用指针事件
这样,代码就可以在触摸设备和鼠标设备上都能正常工作了
话虽如此,指针事件仍然有一些重要的奇怪特性,应当对它们有所了解以正确使用指针事件,并避免一些意料之外的错误,下文会对它们进行介绍
指针事件
指针事件的命名方式和鼠标事件类似:
指针事件 | 类似的鼠标事件 |
---|---|
pointerdown | mousedown |
pointerup | mouseup |
pointermove | mousemove |
pointerover | mouseover |
pointerout | mouseout |
pointerenter | mouseenter |
pointerleave | mouseleave |
pointercancel | - |
gotpointercapture | - |
lostpointercapture | - |
不难发现,每一个 mouse<event>
都有与之相对应的 pointer<event>
用
pointer<event>
替换mouse<event>
可以把代码中的
mouse<event>
都替换成pointer<event>
,程序仍然正常兼容鼠标设备替换之后,程序对触屏设备的支持会“魔法般”地提升。但是,可能需要在 CSS 中的某些地方添加
touch-action: none
,见 指针中断
指针中断
pointercancel
:一个正处于活跃状态的指针交互由于某些原因被中断时触发。也就是在这个事件之后,该指针就不会继续触发更多事件了
导致指针中断的可能原因如下:
- 指针设备硬件在物理层面上被禁用
- 设备方向旋转
- 浏览器打算自行处理这一交互,比如将其看作是一个专门的鼠标手势或缩放操作等
例子:图片拖拽
- 用户按住了一张图片,开始拖拽 ——
pointerdown
事件触发 - 用户开始移动指针(从而拖动图片)——
pointermove
事件持续触发 - 然后意料之外的情况发生了!浏览器有自己原生的图片拖放操作,接管了之前的拖放过程,于是触发了
pointercancel
事件- 现在拖放图片的操作由浏览器自行实现
- 我们不会再得到
pointermove
事件了
解决:阻止浏览器的默认行为来防止 pointercancel
触发
- 阻止原生的拖放操作发生:
img.ondragstart = () => false
- 对于触屏设备,还有其他和触摸相关的浏览器行为(除了拖放)。为了避免它们所引发的问题:
- 通过在 CSS 中设置
touch-action: none
来阻止它们
- 通过在 CSS 中设置
指针捕获
elem.setPointerCapture(pointerId)
:将给定的 pointerId
绑定到 elem
。在调用之后,所有具有相同 pointerId
的指针事件都将 elem
作为目标(就像事件发生在 elem
上一样)
绑定会在以下情况下被移除:
- 当
pointerup
或pointercancel
事件出现时,绑定会被自动地移除 - 当
elem
被从文档中移除后,绑定会被自动地移除 - 当
elem.releasePointerCapture(pointerId)
被调用,绑定会被移除
相关事件:
gotpointercapture
:在一个元素使用setPointerCapture
来启用捕获后触发lostpointercapture
:在捕获被释放后触发(以上几种情况)
指针捕获可以被用于简化拖放类的交互,例如滑块:
实现逻辑:
- 用户按下滑动条的滑块
thumb
——pointerdown
事件触发 - 然后用户移动指针 ——
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
- 通过
pointerId
和isPrimary
即可处理多点触控
- 通过
有些指针设备会测量接触面积和点按压力,可以使用以下属性:
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]
度