Javascript 指針事件

2023-02-17 10:54 更新

指針事件(Pointer Events)是一種用于處理來(lái)自各種輸入設(shè)備(例如鼠標(biāo)、觸控筆和觸摸屏等)的輸入信息的現(xiàn)代化解決方案。

一段簡(jiǎn)史

讓我們先做一個(gè)簡(jiǎn)短的概覽,以便你對(duì)指針事件及其在其它事件類型中所處位置有個(gè)粗略認(rèn)識(shí)。

  • 很早以前,只存在鼠標(biāo)事件。
  • 后來(lái),觸屏設(shè)備開(kāi)始普及,尤其是手機(jī)和平板電腦。為了使現(xiàn)有的腳本仍能正常工作,它們生成(現(xiàn)在仍生成)鼠標(biāo)事件。例如,輕觸屏幕就會(huì)生成 mousedown 事件。因此,觸摸設(shè)備可以很好地與網(wǎng)頁(yè)配合使用。

    但是,觸摸設(shè)備比鼠標(biāo)具有更多的功能。例如,我們可以同時(shí)觸控多點(diǎn)(多點(diǎn)觸控)。然而,鼠標(biāo)事件并沒(méi)有相關(guān)屬性來(lái)處理這種多點(diǎn)觸控。

  • 因此,引入了觸摸事件,例如 touchstart、touchend 和 touchmove,它們具有特定于觸摸的屬性(這里不再贅述這些特性,因?yàn)橹羔樖录油晟疲?/li>

    不過(guò)這還是不夠完美。因?yàn)楹芏嗥渌斎朐O(shè)備(如觸控筆)都有自己的特性。而且同時(shí)維護(hù)兩份分別處理鼠標(biāo)事件和觸摸事件的代碼,顯得有些笨重了。

  • 為了解決這些問(wèn)題,人們引入了全新的規(guī)范「指針事件」。它為各種指針輸入設(shè)備提供了一套統(tǒng)一的事件。

目前,各大主流瀏覽器已經(jīng)支持了 Pointer Events Level 2 標(biāo)準(zhǔn),版本更新的 Pointer Events Level 3 已經(jīng)發(fā)布,并且大多數(shù)情況下與 Pointer Events Level 2 兼容。

因此,除非你寫(xiě)的代碼需要兼容舊版本的瀏覽器,例如 IE 10 或 Safari 12 或更低的版本,否則無(wú)需繼續(xù)使用鼠標(biāo)事件或觸摸事件 —— 我們可以使用指針事件。

這樣,你的代碼就可以在觸摸設(shè)備和鼠標(biāo)設(shè)備上都能正常工作了。

話雖如此,指針事件仍然有一些重要的奇怪特性,你應(yīng)當(dāng)對(duì)它們有所了解以正確使用指針事件,并避免一些意料之外的錯(cuò)誤。我們將在本文中對(duì)它們進(jìn)行介紹。

指針事件類型

指針事件的命名方式和鼠標(biāo)事件類似:

指針事件 類似的鼠標(biāo)事件
pointerdown mousedown
pointerup mouseup
pointermove mousemove
pointerover mouseover
pointerout mouseout
pointerenter mouseenter
pointerleave mouseleave
pointercancel -
gotpointercapture -
lostpointercapture -

不難發(fā)現(xiàn),每一個(gè) mouse<event> 都有與之相對(duì)應(yīng)的 pointer<event>。同時(shí)還有 3 個(gè)額外的事件沒(méi)有相應(yīng)的 mouse...,我們會(huì)在稍后詳細(xì)解釋它們。

在代碼中用 ?pointer<event>? 替換 ?mouse<event>?

我們可以把代碼中的 mouse<event> 都替換成 pointer<event>,程序仍然正常兼容鼠標(biāo)設(shè)備。

替換之后,程序?qū)τ|屏設(shè)備的支持會(huì)“魔法般”地提升。但是,我們可能需要在 CSS 中的某些地方添加 touch-action: none。我們會(huì)在下文的 pointercancel 一節(jié)中描述這里面的細(xì)節(jié)。

指針事件屬性

指針事件具備和鼠標(biāo)事件完全相同的屬性,包括 clientX/Y 和 target 等,以及一些其他屬性:

  • ?pointerId? —— 觸發(fā)當(dāng)前事件的指針唯一標(biāo)識(shí)符。
  • 瀏覽器生成的。使我們能夠處理多指針的情況,例如帶有觸控筆和多點(diǎn)觸控功能的觸摸屏(下文會(huì)有相關(guān)示例)。

  • ?pointerType? —— 指針的設(shè)備類型。必須為字符串,可以是:“mouse”、“pen” 或 “touch”。
  • 我們可以使用這個(gè)屬性來(lái)針對(duì)不同類型的指針輸入做出不同響應(yīng)。

  • ?isPrimary? —— 當(dāng)指針為首要指針(多點(diǎn)觸控時(shí)按下的第一根手指)時(shí)為 ?true?。

有些指針設(shè)備會(huì)測(cè)量接觸面積和點(diǎn)按壓力(例如一根手指壓在觸屏上),對(duì)于這種情況可以使用以下屬性:

  • ?width? —— 指針(例如手指)接觸設(shè)備的區(qū)域的寬度。對(duì)于不支持的設(shè)備(如鼠標(biāo)),這個(gè)值總是 ?1?。
  • ?height? —— 指針(例如手指)接觸設(shè)備的區(qū)域的長(zhǎng)度。對(duì)于不支持的設(shè)備,這個(gè)值總是 ?1?。
  • ?pressure? —— 觸摸壓力,是一個(gè)介于 0 到 1 之間的浮點(diǎn)數(shù)。對(duì)于不支持壓力檢測(cè)的設(shè)備,這個(gè)值總是 ?0.5?(按下時(shí))或 ?0?。
  • ?tangentialPressure? —— 歸一化后的切向壓力(tangential pressure)。
  • ?tiltX, tiltY, twist? —— 針對(duì)觸摸筆的幾個(gè)屬性,用于描述筆和屏幕表面的相對(duì)位置。

大多數(shù)設(shè)備都不支持這些屬性,因此它們很少被使用。如果你需要使用它們,可以在 規(guī)范文檔 中查看更多有關(guān)它們的詳細(xì)信息。

多點(diǎn)觸控

多點(diǎn)觸控(用戶在手機(jī)或平板上同時(shí)點(diǎn)擊若干個(gè)位置,或執(zhí)行特殊手勢(shì))是鼠標(biāo)事件完全不支持的功能之一。

指針事件使我們能夠通過(guò) pointerId 和 isPrimary 屬性的幫助,能夠處理多點(diǎn)觸控。

當(dāng)用戶用一根手指觸摸觸摸屏的某個(gè)位置,然后將另一根手指放在該觸摸屏的其他位置時(shí),會(huì)發(fā)生以下情況:

  1. 第一個(gè)手指觸摸:
    • ?pointerdown? 事件觸發(fā),?isPrimary=true?,并且被指派了一個(gè) ?pointerId?。
  2. 第二個(gè)和后續(xù)的更多個(gè)手指觸摸(假設(shè)第一個(gè)手指仍在觸摸):
    • ?pointerdown? 事件觸發(fā),?isPrimary=false?,并且每一個(gè)觸摸都被指派了不同的 ?pointerId?。

請(qǐng)注意:pointerId 不是分配給整個(gè)設(shè)備的,而是分配給每一個(gè)觸摸的。如果 5 根手指同時(shí)觸摸屏幕,我們會(huì)得到 5 個(gè) pointerdown 事件和相應(yīng)的坐標(biāo)以及 5 個(gè)不同的 pointerId

和第一個(gè)觸摸相關(guān)聯(lián)的事件總有 isPrimary=true。

利用 pointerId,我們可以追蹤多根正在觸摸屏幕的手指。當(dāng)用戶移動(dòng)或抬起某根手指時(shí),我們會(huì)得到和 pointerdown 事件具有相同 pointerId 的 pointermove 或 pointerup 事件。

這是一個(gè)記錄 pointerdown 和 pointerup 事件的演示:

示例代碼

請(qǐng)注意:你使用的必須是一個(gè)多點(diǎn)觸控設(shè)備(如平板或手機(jī))才能在 pointerId/isPrimary 中看到區(qū)別。對(duì)于使用鼠標(biāo)這樣的單點(diǎn)觸控設(shè)備,所有指針事件都會(huì)具有相同的 pointerId 和 isPrimary=true 屬性。

事件:pointercancel 

pointercancel 事件將會(huì)在一個(gè)正處于活躍狀態(tài)的指針交互由于某些原因被中斷時(shí)觸發(fā)。也就是在這個(gè)事件之后,該指針就不會(huì)繼續(xù)觸發(fā)更多事件了。

導(dǎo)致指針中斷的可能原因如下:

  • 指針設(shè)備硬件在物理層面上被禁用。
  • 設(shè)備方向旋轉(zhuǎn)(例如給平板轉(zhuǎn)了個(gè)方向)。
  • 瀏覽器打算自行處理這一交互,比如將其看作是一個(gè)專門的鼠標(biāo)手勢(shì)或縮放操作等。

我們會(huì)用一個(gè)實(shí)際例子來(lái)闡釋 pointercancel 的影響。

例如,我們想要實(shí)現(xiàn)一個(gè)像 鼠標(biāo)拖放事件 中開(kāi)頭提到的那樣的一個(gè)對(duì)球的拖放操作。

用戶的操作流和對(duì)應(yīng)的事件如下:

  1. 用戶按住了一張圖片,開(kāi)始拖拽
    • ?pointerdown? 事件觸發(fā)
  2. 用戶開(kāi)始移動(dòng)指針(從而拖動(dòng)圖片)
    • ?pointermove? 事件觸發(fā),可能觸發(fā)多次
  3. 然后意料之外的情況發(fā)生了!瀏覽器有自己原生的圖片拖放操作,接管了之前的拖放過(guò)程,于是觸發(fā)了 ?pointercancel? 事件。
    • 現(xiàn)在拖放圖片的操作由瀏覽器自行實(shí)現(xiàn)。用戶甚至可能會(huì)把圖片拖出瀏覽器,放進(jìn)他們的郵件程序或文件管理器。
    • 我們不會(huì)再得到 ?pointermove? 事件了。

這里的問(wèn)題就在于瀏覽器”劫持“了這一個(gè)互動(dòng)操作:在“拖放”過(guò)程開(kāi)始時(shí)觸發(fā)了 pointercancel 事件,并且不再有 pointermove 事件會(huì)被生成。

這里是拖放示例的演示,并且在拖放過(guò)程中,指針事件(只包含 up/down、move 和 cancel)的觸發(fā)會(huì)被記錄在 textarea 中:

示例代碼

我們想要實(shí)現(xiàn)自己的拖放操作,所以讓我們來(lái)看看如何告訴瀏覽器不要接管拖放操作。

阻止瀏覽器的默認(rèn)行為來(lái)防止 pointercancel 觸發(fā)。

我們需要做兩件事:

  1. 阻止原生的拖放操作發(fā)生:
    • 正如我們?cè)?nbsp;鼠標(biāo)拖放事件 中描述的那樣,我們可以通過(guò)設(shè)置 ?ball.ondragstart = () => false? 來(lái)實(shí)現(xiàn)這一需求。
    • 這種方式也適用于鼠標(biāo)事件。
  2. 對(duì)于觸屏設(shè)備,還有其他和觸摸相關(guān)的瀏覽器行為(除了拖放)。為了避免它們所引發(fā)的問(wèn)題:
    • 我們可以通過(guò)在 CSS 中設(shè)置 ?#ball { touch-action: none }? 來(lái)阻止它們。
    • 之后我們的代碼便可以在觸屏設(shè)備中正常工作了。

經(jīng)過(guò)上述操作,事件將會(huì)按照我們預(yù)期的方式觸發(fā),瀏覽器不會(huì)劫持拖放過(guò)程,也不會(huì)觸發(fā) pointercancel 事件。

這個(gè)演示增加了以下幾行:

示例代碼

可以看到,pointercancel 事件不再被觸發(fā)。

現(xiàn)在我們就可以添加讓球的位置移動(dòng)的代碼了,并且我們的代碼對(duì)鼠標(biāo)和觸控設(shè)備都有效。

指針捕獲

指針捕獲(Pointer capturing)是針對(duì)指針事件的一個(gè)特性。

這個(gè)想法很簡(jiǎn)單,但是乍一看可能感覺(jué)很奇怪,因?yàn)樵谄渌魏问录愋椭卸紱](méi)有這種東西。

主要的方法是:

  • ?elem.setPointerCapture(pointerId)? —— 將給定的 ?pointerId? 綁定到 ?elem?。在調(diào)用之后,所有具有相同 ?pointerId? 的指針事件都將 ?elem? 作為目標(biāo)(就像事件發(fā)生在 ?elem? 上一樣),無(wú)論這些 ?elem? 在文檔中的實(shí)際位置是什么。

換句話說(shuō),elem.setPointerCapture(pointerId) 將所有具有給定 pointerId 的后續(xù)事件重新定位到 elem

綁定會(huì)在以下情況下被移除:

  • 當(dāng) ?pointerup? 或 ?pointercancel? 事件出現(xiàn)時(shí),綁定會(huì)被自動(dòng)地移除。
  • 當(dāng) ?elem? 被從文檔中移除后,綁定會(huì)被自動(dòng)地移除。
  • 當(dāng) ?elem.releasePointerCapture(pointerId)? 被調(diào)用,綁定會(huì)被移除。

那么,它有什么用?我們一起來(lái)看一個(gè)實(shí)際的例子吧。

指針捕獲可以被用于簡(jiǎn)化拖放類的交互。

讓我們來(lái)回憶一下在 鼠標(biāo)拖放事件 中提到的,如何實(shí)現(xiàn)一個(gè)自定義滑動(dòng)條。

我們可以創(chuàng)建一個(gè)帶有條形圖的、并且內(nèi)部有一個(gè)“滑塊”(thumb)的滑動(dòng)條元素(slider):

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

添加樣式后的效果如下:


用指針事件替換鼠標(biāo)事件后的實(shí)現(xiàn)邏輯:

  1. 用戶按下滑動(dòng)條的滑塊 ?thumb? —— ?pointerdown? 事件被觸發(fā)。
  2. 然后用戶移動(dòng)指針 —— ?pointermove? 事件被觸發(fā),我們讓移動(dòng)事件只作用在 ?thumb? 上。
    • ……在指針的移動(dòng)過(guò)程中,指針可能會(huì)離開(kāi)滑動(dòng)條的 ?thumb? 元素,移動(dòng)到 ?thumb? 之上或之下的位置。而 ?thumb? 應(yīng)該嚴(yán)格在水平方向上移動(dòng),并與指針保持對(duì)齊。

在基于鼠標(biāo)事件實(shí)現(xiàn)的方案中,要跟蹤指針的所有移動(dòng),包括指針移動(dòng)到 thumb 之上或之下的位置時(shí),我們必須在整個(gè)文檔 document 上分配 mousemove 事件處理程序。

不過(guò),這并不是一個(gè)沒(méi)有副作用的解決方案。其中的一個(gè)問(wèn)題就是,指針在文檔周圍的移動(dòng)可能會(huì)引起副作用,在其他元素上觸發(fā)事件處理程序(例如 mouseover)并調(diào)用其他元素上與滑動(dòng)條不相關(guān)的功能,這不是我們預(yù)期的效果。

這就是 setPointerCapture 適用的場(chǎng)景。

  • 我們可以在 ?pointerdown? 事件的處理程序中調(diào)用 ?thumb.setPointerCapture(event.pointerId)?,
  • 這樣接下來(lái)在 ?pointerup/cancel? 之前發(fā)生的所有指針事件都會(huì)被重定向到 ?thumb? 上。
  • 當(dāng) ?pointerup? 發(fā)生時(shí)(拖動(dòng)完成),綁定會(huì)被自動(dòng)移除,我們不需要關(guān)心它。

因此,即使用戶在整個(gè)文檔上移動(dòng)指針,事件處理程序也將僅在 thumb 上被調(diào)用。盡管如此,事件對(duì)象的坐標(biāo)屬性,例如 clientX/clientY 仍將是正確的 —— 捕獲僅影響 target/currentTarget

主要代碼如下:

thumb.onpointerdown = function(event) {
  // 把所有指針事件(pointerup 之前發(fā)生的)重定向到 thumb
  thumb.setPointerCapture(event.pointerId);

  // 開(kāi)始跟蹤指針的移動(dòng)
  thumb.onpointermove = function(event) {
    // 移動(dòng)滑動(dòng)條:在 thumb 上監(jiān)聽(tīng)即可,因?yàn)樗兄羔樖录急恢囟ㄏ虻搅?thumb
    let newLeft = event.clientX - slider.getBoundingClientRect().left;
    thumb.style.left = newLeft + 'px';
  };

  // 當(dāng)結(jié)束(pointerup)時(shí)取消對(duì)指針移動(dòng)的跟蹤
  thumb.onpointerup = function(event) {
    thumb.onpointermove = null;
    thumb.onpointerup = null;
    // ...這里還可以處理“拖動(dòng)結(jié)束”相關(guān)的邏輯
  };
};

// 注意:無(wú)需調(diào)用 thumb.releasePointerCapture,
// 它會(huì)在 pointerup 時(shí)被自動(dòng)調(diào)用

完整示例

在這個(gè) demo 中還有一個(gè)元素,當(dāng)它的 onmouseover 處理程序被觸發(fā)時(shí)會(huì)顯示當(dāng)前的時(shí)間。

請(qǐng)注意:當(dāng)你拖動(dòng)滑塊的時(shí)候,鼠標(biāo)可能會(huì)懸停在這個(gè)元素上,它的 onmouseover 處理程序不會(huì)被觸發(fā)。

借助于 setPointerCapture,現(xiàn)在拖動(dòng)滑塊不會(huì)再產(chǎn)生副作用了。

言而總之,指針捕獲為我們帶來(lái)了兩個(gè)好處:

  1. 代碼變得更加簡(jiǎn)潔,我們不再需要在整個(gè) ?document? 上添加/移除處理程序。綁定會(huì)被自動(dòng)釋放。
  2. 如果文檔中有其他指針事件處理程序,則在用戶拖動(dòng)滑動(dòng)條時(shí),它們不會(huì)因指針的移動(dòng)被意外地觸發(fā)。

指針捕獲事件

完整起見(jiàn),這里還需要提及一個(gè)知識(shí)點(diǎn)。

還有兩個(gè)與指針捕獲相關(guān)的事件:

  • ?gotpointercapture? 會(huì)在一個(gè)元素使用 ?setPointerCapture? 來(lái)啟用捕獲后觸發(fā)。
  • ?lostpointercapture? 會(huì)在捕獲被釋放后觸發(fā):其觸發(fā)可能是由于 ?releasePointerCapture? 的顯式調(diào)用,或是 ?pointerup/pointercancel? 事件觸發(fā)后的自動(dòng)調(diào)用。

總結(jié)

指針事件允許我們通過(guò)一份代碼,同時(shí)處理鼠標(biāo)、觸摸和觸控筆事件。

指針事件是鼠標(biāo)事件的拓展。我們可以在事件名稱中用 pointer 替換 mouse 來(lái)讓我們的代碼既能繼續(xù)支持鼠標(biāo),也能更好地支持其他類型的設(shè)備。

對(duì)于瀏覽器可能會(huì)決定進(jìn)行劫持并自行處理的拖放和復(fù)雜的觸控交互 —— 請(qǐng)記住取消事件的默認(rèn)操作,并在 CSS 中為涉及到的元素設(shè)置 touch-action: none

指針事件還額外具備以下能力:

  • 基于 ?pointerId? 和 ?isPrimary? 的多點(diǎn)觸控支持。
  • 針對(duì)特定設(shè)備的屬性,例如 ?pressure? 和 ?width/height? 等。
  • 指針捕獲:我們可以把 ?pointerup/pointercancel? 之前的所有指針事件重定向到一個(gè)特定的元素。

目前,指針事件已經(jīng)被各大主流瀏覽器支持,尤其是如果不需要支持 IE10 和 Safari 12 以下的版本,我們可以放心地使用它們。不過(guò)即便是針對(duì)這些老式瀏覽器,也可以通過(guò) polyfill 來(lái)讓它們支持指針事件。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)