2024 年 1 月 27 日

指標事件

指標事件是一種處理各種指標裝置輸入的現代方式,例如滑鼠、觸控筆/手寫筆、觸控螢幕等。

簡要歷史

讓我們做個簡要的概述,以便你了解指標事件在其他事件類型中的概況和位置。

  • 很久以前,過去只有滑鼠事件。

    然後觸控裝置變得廣泛,特別是手機和平板電腦。為了讓現有的腳本運作,它們產生(而且仍然產生)滑鼠事件。例如,輕觸觸控螢幕會產生 mousedown。因此,觸控裝置可以順利與網頁搭配使用。

    但是,觸控裝置比滑鼠有更多功能。例如,可以同時觸控多個點(「多點觸控」)。儘管如此,滑鼠事件沒有必要的屬性來處理此類多點觸控。

  • 因此,引入了觸控事件,例如 touchstarttouchendtouchmove,它們具有觸控專屬屬性(我們在此不詳細說明,因為指標事件甚至更好)。

    儘管如此,這還不夠,因為還有許多其他裝置,例如觸控筆,它們有自己的功能。此外,撰寫同時偵聽觸控和滑鼠事件的程式碼很麻煩。

  • 為了解決這些問題,引入了新的指標事件標準。它為所有類型的指標裝置提供一組單一事件。

截至目前,所有主要瀏覽器都支援 指標事件 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... 對應項,我們將很快說明它們。

在我們的程式碼中將 mouse<event> 替換為 pointer<event>

我們可以在程式碼中將 mouse<event> 事件替換為 pointer<event>,並預期滑鼠可以繼續正常運作。

對觸控裝置的支援也會「神奇地」改善。儘管如此,我們可能需要在 CSS 中的某些地方加入 touch-action: none。我們將在關於 pointercancel 的部分中說明。

指標事件屬性

指標事件具有與滑鼠事件相同的屬性,例如 clientX/Ytarget 等,另外還有其他一些屬性

  • pointerId – 導致事件的指標的唯一識別碼。

    瀏覽器產生。允許我們處理多個指標,例如具有觸控筆和多點觸控功能的觸控螢幕(後面將提供範例)。

  • pointerType – 指標裝置類型。必須為字串,下列其中之一:「滑鼠」、「觸控筆」或「觸控」。

    我們可以使用此屬性對各種指標類型做出不同的反應。

  • isPrimary – 對於主要指標(多點觸控中的第一個手指)為 true

有些指標裝置會測量接觸面積和壓力,例如觸控螢幕上的手指,因此有額外的屬性

  • width – 指標(例如手指)觸控裝置的區域寬度。如果不受支援,例如滑鼠,則始終為 1
  • height – 指標觸控裝置的區域高度。如果不受支援,則始終為 1
  • pressure – 指標尖端的壓力,範圍從 0 到 1。對於不支援壓力的裝置,必須為 0.5(按壓)或 0
  • tangentialPressure – 標準化的切向壓力。
  • tiltXtiltYtwist – 專門針對觸控筆的屬性,用來描述觸控筆相對於表面的定位方式。

這些屬性不受大多數裝置支援,因此很少使用。如有需要,您可以在 規格 中找到相關詳細資訊。

多點觸控

滑鼠事件完全不支援的一件事就是多點觸控:使用者可以在手機或平板電腦上同時在多個地方觸控,或執行特殊手勢。

指標事件允許在 pointerIdisPrimary 屬性的協助下處理多點觸控。

以下是使用者在觸控螢幕上的一個地方觸控,然後在螢幕上的其他地方放上另一個手指時會發生的事

  1. 在第一個手指觸控時
    • pointerdown,其中 isPrimary=true 且具有某個 pointerId
  2. 對於第二個手指和更多手指(假設第一個手指仍在觸控)
    • pointerdown 搭配 isPrimary=false 與每個手指不同的 pointerId

請注意:pointerId 不是指定給整個裝置,而是給每個觸控的手指。如果我們用 5 根手指同時觸控螢幕,我們會有 5 個 pointerdown 事件,每個事件都有各自的座標和不同的 pointerId

與第一根手指相關的事件總是會有 isPrimary=true

我們可以使用 pointerId 追蹤多個觸控的手指。當使用者移動然後移除手指時,我們會得到 pointermovepointerup 事件,其 pointerId 與我們在 pointerdown 中的相同。

以下示範會記錄 pointerdownpointerup 事件

請注意:你必須使用觸控螢幕裝置,例如手機或平板電腦,才能實際看到 pointerId/isPrimary 的差異。對於單點觸控裝置,例如滑鼠,所有指標事件的 pointerId 會始終相同,且 isPrimary=true

事件:pointercancel

當進行指標互動時,pointercancel 事件會觸發,然後發生某些事情導致互動中止,因此不會再產生指標事件。

這些原因包括

  • 指標裝置硬體被實體停用。
  • 裝置方向改變(平板電腦旋轉)。
  • 瀏覽器決定自行處理互動,將其視為滑鼠手勢或縮放和平移動作或其他動作。

我們將在實際範例中示範 pointercancel,看看它如何影響我們。

假設我們要為一個球實作拖放,就像本文開頭的 使用滑鼠事件拖放

以下是使用者動作和對應事件的流程

  1. 使用者按壓圖片,開始拖曳
    • 觸發 pointerdown 事件
  2. 然後他們開始移動指標(因此拖曳圖片)
    • 觸發 pointermove,可能多次
  3. 然後驚喜發生了!瀏覽器有原生支援圖片的拖放功能,它會介入並接管拖放程序,因此產生 pointercancel 事件。
    • 瀏覽器現在自行處理影像的拖放。使用者甚至可以將球體影像從瀏覽器拖曳到他們的郵件程式或檔案總管。
    • 我們不再有 pointermove 事件了。

因此,問題在於瀏覽器「劫持」了互動:pointercancel 會在「拖放」程序開始時觸發,而且不會再產生 pointermove 事件。

以下是拖放示範,會在 textarea 中記錄指標事件(只有 up/downmovecancel)。

我們想要自行實作拖放,所以讓我們告訴瀏覽器不要接管。

防止瀏覽器預設動作,以避免 pointercancel

我們需要做兩件事

  1. 防止原生拖放發生
    • 我們可以透過設定 ball.ondragstart = () => false 來做到,就像在文章 使用滑鼠事件進行拖放 中所述。
    • 這對滑鼠事件很有效。
  2. 對於觸控裝置,還有其他與觸控相關的瀏覽器動作(除了拖放)。為了避免這些問題
    • 透過在 CSS 中設定 #ball { touch-action: none } 來防止它們。
    • 然後我們的程式碼將會開始在觸控裝置上運作。

在我們這麼做之後,事件將會按預期運作,瀏覽器不會劫持程序,也不會發出 pointercancel

此示範新增了這些列

正如你所見,不再有 pointercancel 了。

現在我們可以新增程式碼來實際移動球體,我們的拖放將會在滑鼠裝置和觸控裝置上運作。

指標擷取

指標擷取是指標事件的一項特殊功能。

這個概念很簡單,但一開始可能會覺得很奇怪,因為其他事件類型沒有類似的功能。

主要方法是

  • elem.setPointerCapture(pointerId) – 將具有指定 pointerId 的事件繫結到 elem。呼叫之後,所有具有相同 pointerId 的指標事件都會以 elem 為目標(就像發生在 elem 上一樣),無論它們在文件中實際發生在哪裡。

換句話說,elem.setPointerCapture(pointerId) 會將所有後續具有指定 pointerId 的事件重新設定目標為 elem

繫結會被移除

  • 當發生 pointeruppointercancel 事件時自動執行
  • elem 從文件中移除時自動執行
  • 當呼叫 elem.releasePointerCapture(pointerId) 時執行

現在它有什麼好處?是時候看一個實際範例了。

指標擷取可用於簡化拖放類型的互動。

讓我們回想一下如何實作自訂滑桿,如 使用滑鼠事件拖放 中所述。

我們可以製作一個 slider 元素來表示其中的條狀物和「滑塊」(thumb)

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

使用樣式,它看起來像這樣

以下是工作邏輯,如所述,在用類似的指標事件取代滑鼠事件後

  1. 使用者按下滑桿 thumb – 觸發 pointerdown
  2. 然後他們移動指標 – 觸發 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,拖曳現在沒有副作用。

最後,指標擷取為我們帶來兩個好處

  1. 由於我們不再需要在整個 document 上新增/移除處理常式,所以程式碼變得更簡潔。繫結會自動釋放。
  2. 如果文件中還有其他指標事件處理常式,在使用者拖曳滑桿時,這些處理常式不會意外地被指標觸發。

指標擷取事件

為了完整起見,這裡還要提一件事。

有兩個事件與指標擷取相關聯

  • 當元素使用 setPointerCapture 來啟用擷取時,會觸發 gotpointercapture
  • 當擷取被釋放時,會觸發 lostpointercapture:透過 releasePointerCapture 呼叫明確釋放,或在 pointerup/pointercancel 上自動釋放。

摘要

指標事件允許同時處理滑鼠、觸控和筆事件,只需一段程式碼。

指標事件擴充了滑鼠事件。我們可以在事件名稱中用 pointer 取代 mouse,並預期我們的程式碼能繼續用於滑鼠,同時更支援其他裝置類型。

對於瀏覽器可能決定劫持並自行處理的拖放和複雜觸控互動,請記得取消事件上的預設動作,並在 CSS 中為我們使用的元素設定 touch-action: none

指標事件的其他功能包括

  • 使用 pointerIdisPrimary 的多點觸控支援。
  • 裝置特定屬性,例如 pressurewidth/height 等。
  • 指標擷取:我們可以將所有指標事件重新設定目標到特定元素,直到 pointerup/pointercancel

目前,所有主要瀏覽器都支援指標事件,因此我們可以安全地切換到指標事件,特別是在不需要 IE10 和 Safari 12 的情況下。即使使用這些瀏覽器,也有多重載入,可以支援指標事件。

教學課程地圖

留言

留言前請先閱讀…
  • 如果您有改進建議,請 提交 GitHub 問題 或提出拉取請求,而不是留言。
  • 如果您無法理解文章中的某些內容,請詳細說明。
  • 若要插入少數幾個字的程式碼,請使用 <code> 標籤,若要插入多行程式碼,請將它們包在 <pre> 標籤中,若要插入超過 10 行的程式碼,請使用沙箱 (plnkrjsbincodepen…)