指针事件是处理各种指向设备(如鼠标、触控笔/手写笔、触摸屏等)输入的现代方式。
简要历史
我们来做一个简要概述,以便你了解指针事件在其他事件类型中的总体情况和位置。
-
很久以前,过去,只有鼠标事件。
然后触摸设备变得普遍,尤其是手机和平板电脑。为了让现有脚本工作,它们生成了(并且仍然生成)鼠标事件。例如,轻触触摸屏会生成
mousedown
。因此,触摸设备可以很好地与网页配合使用。但触摸设备比鼠标有更多的功能。例如,可以同时触摸多个点(“多点触控”)。不过,鼠标事件没有处理此类多点触控所需的属性。
-
因此引入了触摸事件,例如
touchstart
、touchend
、touchmove
,它们具有特定于触摸的属性(我们在此处不详细介绍,因为指针事件更好)。不过,这还不够,因为还有许多其他设备,例如笔,它们有自己的功能。此外,编写同时侦听触摸和鼠标事件的代码很麻烦。
-
为了解决这些问题,引入了新的标准指针事件。它为所有类型的指针设备提供了一组事件。
截至目前,所有主流浏览器都支持 指针事件 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/Y
、target
等,还有一些其他属性
-
pointerId
– 导致事件的指针的唯一标识符。浏览器生成。允许我们处理多个指针,例如带手写笔和多点触控的触摸屏(后面会有示例)。
-
pointerType
– 指针设备类型。必须是字符串,如下之一:“mouse”、“pen”或“touch”。我们可以使用此属性对各种指针类型做出不同的反应。
-
isPrimary
– 对于主指针(多点触控中的第一个手指),为true
。
一些指针设备测量接触面积和压力,例如对于触摸屏上的手指,为此还有其他属性
width
– 指针(例如手指)接触设备的区域的宽度。在不受支持的情况下,例如对于鼠标,它始终为1
。height
– 指针接触设备的区域的高度。在不受支持的情况下,它始终为1
。pressure
– 指针尖端的压力,范围为 0 到 1。对于不支持压力的设备,必须为0.5
(按下)或0
。tangentialPressure
– 归一化的切向压力。tiltX
、tiltY
、twist
– 特定于笔的属性,描述了笔相对于表面的位置。
大多数设备不支持这些属性,因此很少使用。如果需要,可以在 规范 中找到有关它们的详细信息。
多点触控
鼠标事件完全不支持的一件事是多点触控:用户可以在手机或平板电脑上同时在多个地方触摸,或执行特殊手势。
指针事件允许借助pointerId
和isPrimary
属性处理多点触控。
当用户在一个地方触摸触摸屏,然后将另一个手指放在其他地方时,会发生以下情况
- 在第一个手指触摸时
pointerdown
,其中isPrimary=true
,并且有一些pointerId
。
- 对于第二个手指和更多的手指(假设第一个手指仍在触摸)
pointerdown
,其中isPrimary=false
,并且每个手指都有不同的pointerId
。
请注意:pointerId
不是分配给整个设备,而是分配给每个触摸的手指。如果我们用 5 个手指同时触摸屏幕,我们将有 5 个pointerdown
事件,每个事件都有各自的坐标和不同的pointerId
。
与第一个手指关联的事件始终具有isPrimary=true
。
我们可以使用它们的pointerId
跟踪多个触摸手指。当用户移动然后移除手指时,我们会得到与pointerdown
中相同的pointerId
的pointermove
和pointerup
事件。
下面是记录pointerdown
和pointerup
事件的演示
请注意:您必须使用触摸屏设备(例如手机或平板电脑)才能真正看到pointerId/isPrimary
的差异。对于单点触控设备(例如鼠标),所有指针事件的pointerId
始终相同,isPrimary=true
。
事件:pointercancel
当正在进行指针交互时,pointercancel
事件触发,然后发生某些事情导致其中止,因此不会再生成更多指针事件。
此类原因包括
- 指针设备硬件已物理禁用。
- 设备方向已更改(平板电脑已旋转)。
- 浏览器决定自行处理交互,将其视为鼠标手势或缩放和平移操作或其他操作。
我们将在实际示例中演示pointercancel
,以了解它如何影响我们。
假设我们正在为球实现拖放,就像文章开头所述使用鼠标事件进行拖放。
以下是用户操作流程和相应事件
- 用户按图像以开始拖动
pointerdown
事件触发
- 然后他们开始移动指针(从而拖动图像)
pointermove
触发,可能多次触发
- 然后惊喜发生了!浏览器具有对图像的原生拖放支持,它会启动并接管拖放过程,从而生成
pointercancel
事件。- 浏览器现在自行处理图像的拖放。用户甚至可以将球图像从浏览器拖到其邮件程序或文件管理器中。
- 我们不会再收到
pointermove
事件。
因此,问题在于浏览器“劫持”了交互:pointercancel
在“拖放”过程的开始触发,并且不再生成pointermove
事件。
以下是拖放演示,其中记录了指针事件(仅up/down
、move
和cancel
)在textarea
中
我们希望自行实现拖放,因此让我们告诉浏览器不要接管它。
防止默认浏览器操作以避免pointercancel
。
我们需要做两件事
- 防止发生原生拖放
- 我们可以通过设置
ball.ondragstart = () => false
来实现此目的,正如文章使用鼠标事件进行拖放中所述。 - 这对于鼠标事件非常有效。
- 我们可以通过设置
- 对于触摸设备,还有其他与触摸相关的浏览器操作(除了拖放)。为了避免出现问题
- 通过在 CSS 中设置
#ball { touch-action: none }
来阻止它们。 - 然后,我们的代码将开始在触摸设备上运行。
- 通过在 CSS 中设置
在执行此操作后,事件将按预期工作,浏览器不会劫持该进程,也不会发出 pointercancel
。
此演示添加了这些行
如你所见,不再有 pointercancel
。
现在,我们可以添加代码来实际移动球,我们的拖放功能将适用于鼠标设备和触摸设备。
指针捕获
指针捕获是指针事件的一项特殊功能。
这个想法非常简单,但乍一看可能显得相当奇怪,因为对于任何其他事件类型都不存在类似的功能。
主要方法是
elem.setPointerCapture(pointerId)
– 将具有给定pointerId
的事件绑定到elem
。在调用之后,具有相同pointerId
的所有指针事件都将以elem
为目标(就像发生在elem
上一样),无论它们在文档中的实际发生位置如何。
换句话说,elem.setPointerCapture(pointerId)
将具有给定 pointerId
的所有后续事件重新定位到 elem
。
在以下情况下,绑定将被移除
- 当发生
pointerup
或pointercancel
事件时,自动移除 - 当从文档中移除
elem
时,自动移除 - 当调用
elem.releasePointerCapture(pointerId)
时,自动移除
现在它有什么好处?是时候看一个真实的例子了。
指针捕获可用于简化拖放之类的交互。
让我们回顾一下如何实现自定义滑块,如 使用鼠标事件进行拖放 中所述。
我们可以创建一个 slider
元素来表示其中的条带和“滑块”(thumb
)
<div class="slider">
<div class="thumb"></div>
</div>
使用样式,它看起来像这样
以下是工作逻辑,如所述,在用类似的指针事件替换鼠标事件之后
- 用户按下滑块
thumb
– 触发pointerdown
。 - 然后,他们移动指针 – 触发
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
,拖动现在没有副作用。
最后,指针捕获为我们带来了两个好处
- 代码变得更简洁,因为我们不再需要在整个
document
上添加/移除处理程序。绑定会自动释放。 - 如果文档中还有其他指针事件处理程序,当用户拖动滑块时,它们不会被指针意外触发。
指针捕获事件
为了完整起见,这里还有一件事需要提及。
有两个事件与指针捕获相关
- 当元素使用
setPointerCapture
启用捕获时,将触发gotpointercapture
。 - 当捕获被释放时,将触发
lostpointercapture
:要么通过releasePointerCapture
调用显式释放,要么在pointerup
/pointercancel
上自动释放。
总结
指针事件允许使用单段代码同时处理鼠标、触摸和笔事件。
指针事件扩展了鼠标事件。我们可以在事件名称中用pointer
替换mouse
,并期望我们的代码继续适用于鼠标,同时更好地支持其他设备类型。
对于浏览器可能决定劫持并自行处理的拖放和复杂触摸交互,请记住取消事件上的默认操作,并为我们参与的元素在 CSS 中设置 touch-action: none
。
指针事件的其他功能是
- 使用
pointerId
和isPrimary
的多点触控支持。 - 特定于设备的属性,例如
pressure
、width/height
等。 - 指针捕获:我们可以将所有指针事件重新定位到特定元素,直到
pointerup
/pointercancel
。
截至目前,所有主流浏览器都支持指针事件,因此我们可以安全地切换到它们,特别是如果不需要 IE10 和 Safari 12。即使使用这些浏览器,也有支持指针事件的 polyfill。
评论
<code>
标记,对于多行代码,请用<pre>
标记将其包装起来,对于超过 10 行的代码,请使用沙盒 (plnkr、jsbin、codepen…)