Javascript 指針事件

2023-02-17 10:54 更新

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

一段簡史

讓我們先做一個簡短的概覽,以便你對指針事件及其在其它事件類型中所處位置有個粗略認識。

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

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

  • 因此,引入了觸摸事件,例如 touchstart、touchend 和 touchmove,它們具有特定于觸摸的屬性(這里不再贅述這些特性,因為指針事件更加完善)。
  • 不過這還是不夠完美。因為很多其他輸入設(shè)備(如觸控筆)都有自己的特性。而且同時維護兩份分別處理鼠標(biāo)事件和觸摸事件的代碼,顯得有些笨重了。

  • 為了解決這些問題,人們引入了全新的規(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 兼容。

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

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

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

指針事件類型

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

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

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

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

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

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

指針事件屬性

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

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

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

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

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

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

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

多點觸控

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

指針事件使我們能夠通過 pointerId 和 isPrimary 屬性的幫助,能夠處理多點觸控。

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

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

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

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

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

這是一個記錄 pointerdown 和 pointerup 事件的演示:

示例代碼

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

事件:pointercancel 

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

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

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

我們會用一個實際例子來闡釋 pointercancel 的影響。

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

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

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

這里的問題就在于瀏覽器”劫持“了這一個互動操作:在“拖放”過程開始時觸發(fā)了 pointercancel 事件,并且不再有 pointermove 事件會被生成。

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

示例代碼

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

阻止瀏覽器的默認行為來防止 pointercancel 觸發(fā)。

我們需要做兩件事:

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

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

這個演示增加了以下幾行:

示例代碼

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

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

指針捕獲

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

這個想法很簡單,但是乍一看可能感覺很奇怪,因為在其他任何事件類型中都沒有這種東西。

主要的方法是:

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

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

綁定會在以下情況下被移除:

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

那么,它有什么用?我們一起來看一個實際的例子吧。

指針捕獲可以被用于簡化拖放類的交互。

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

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

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

添加樣式后的效果如下:


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

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

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

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

這就是 setPointerCapture 適用的場景。

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

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

主要代碼如下:

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

  // 開始跟蹤指針的移動
  thumb.onpointermove = function(event) {
    // 移動滑動條:在 thumb 上監(jiān)聽即可,因為所有指針事件都被重定向到了 thumb
    let newLeft = event.clientX - slider.getBoundingClientRect().left;
    thumb.style.left = newLeft + 'px';
  };

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

// 注意:無需調(diào)用 thumb.releasePointerCapture,
// 它會在 pointerup 時被自動調(diào)用

完整示例

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

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

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

言而總之,指針捕獲為我們帶來了兩個好處:

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

指針捕獲事件

完整起見,這里還需要提及一個知識點。

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

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

總結(jié)

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

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

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

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

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

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


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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號