觸控事件

為了高質量地支援基於觸控的使用者介面,觸控事件提供瞭解釋觸控式螢幕或觸控板上手指(或觸控筆)活動的能力。

觸控事件介面是相對底層的 API,可用於支援特定於應用程式的多點觸控互動,例如雙指手勢。當手指(或觸控筆)首次接觸接觸面時,多點觸控互動開始。之後,其他手指可能會接觸表面,並可選擇地在觸控表面上移動。當手指離開表面時,互動結束。在此互動過程中,應用程式會在開始、移動和結束階段接收觸控事件。

觸控事件類似於滑鼠事件,但它們支援同時在觸控表面的不同位置進行觸控。TouchEvent 介面封裝了當前活動的所有觸控點。Touch 介面代表單個觸控點,包含有關觸控點相對於瀏覽器視口的位置等資訊。

定義

表面

支援觸控的表面。這可能是螢幕或觸控板。

觸控點

與表面的接觸點。這可能是手指(或肘部、耳朵、鼻子,隨便什麼,但通常是手指)或觸控筆。

介面

TouchEvent

表示當表面上的觸控狀態發生變化時發生的事件。

Touch

表示使用者與觸控表面之間的單個接觸點。

TouchList

表示一組觸控;當用戶同時有多根手指放在表面上時,會使用此選項。

示例

此示例一次跟蹤多個觸控點,允許使用者同時使用多根手指在 <canvas> 中繪圖。它僅在支援觸控事件的瀏覽器上有效。

注意: 下面的文字在描述與表面的接觸時使用了“手指”一詞,但當然也可以是觸控筆或其他接觸方式。

建立畫布

html
<canvas id="canvas" width="600" height="600">
  Your browser does not support canvas element.
</canvas>
<br />
Log:
<pre id="log"></pre>
css
#canvas {
  border: 1px solid black;
}

#log {
  height: 200px;
  width: 600px;
  overflow: scroll;
  border: 1px solid #cccccc;
}

設定事件處理程式

程式碼為我們的 <canvas> 元素設定了所有事件監聽器,以便我們能夠處理發生的觸控事件。

js
const el = document.getElementById("canvas");
el.addEventListener("touchstart", handleStart);
el.addEventListener("touchend", handleEnd);
el.addEventListener("touchcancel", handleCancel);
el.addEventListener("touchmove", handleMove);

跟蹤新的觸控

我們將跟蹤正在進行的觸控。

js
const ongoingTouches = [];

當發生 touchstart 事件(表示在表面上發生了新的觸控)時,會呼叫下面的 handleStart() 函式。

js
function handleStart(evt) {
  evt.preventDefault();
  log("touchstart.");
  const el = document.getElementById("canvas");
  const ctx = el.getContext("2d");
  const touches = evt.changedTouches;

  for (let i = 0; i < touches.length; i++) {
    const touch = touches[i];
    log(`touchstart: ${i}.`);
    ongoingTouches.push(copyTouch(touch));
    const color = colorForTouch(touch);
    log(`color of touch with id ${touch.identifier} = ${color}`);
    ctx.beginPath();
    ctx.arc(touch.pageX, touch.pageY, 4, 0, 2 * Math.PI, false); // a circle at the start
    ctx.fillStyle = color;
    ctx.fill();
  }
}

這會呼叫 event.preventDefault() 來阻止瀏覽器繼續處理觸控事件(這也防止了滑鼠事件也被傳遞)。然後我們獲取上下文,並從事件的 TouchEvent.changedTouches 屬性中提取已更改的觸控點列表。

之後,我們遍歷列表中的所有 Touch 物件,將它們推入一個活動觸控點陣列,並繪製繪製的起始點作為一個小圓圈;我們使用的是 4 畫素寬的線條,因此 4 畫素半徑的圓圈會清晰顯示。

觸控移動時的繪圖

每當一根或多根手指移動時,都會傳遞一個 touchmove 事件,從而呼叫我們的 handleMove() 函式。在此示例中,它的職責是更新快取的觸控資訊,並繪製從每個觸控點的上一個位置到當前位置的線條。

js
function handleMove(evt) {
  evt.preventDefault();
  const el = document.getElementById("canvas");
  const ctx = el.getContext("2d");
  const touches = evt.changedTouches;

  for (const touch of touches) {
    const color = colorForTouch(touch);
    const idx = ongoingTouchIndexById(touch.identifier);

    if (idx >= 0) {
      log(`continuing touch ${idx}`);
      ctx.beginPath();
      log(
        `ctx.moveTo( ${ongoingTouches[idx].pageX}, ${ongoingTouches[idx].pageY} );`,
      );
      ctx.moveTo(ongoingTouches[idx].pageX, ongoingTouches[idx].pageY);
      log(`ctx.lineTo( ${touch.pageX}, ${touch.pageY} );`);
      ctx.lineTo(touch.pageX, touch.pageY);
      ctx.lineWidth = 4;
      ctx.strokeStyle = color;
      ctx.stroke();

      ongoingTouches.splice(idx, 1, copyTouch(touch)); // swap in the new touch record
    } else {
      log("can't figure out which touch to continue");
    }
  }
}

這同樣會遍歷已更改的觸控,但它會檢視我們快取的觸控資訊陣列中關於每個觸控點的上一個資訊,以確定要繪製的每個觸控點的新線段的起點。這是透過檢視每個觸控點的 Touch.identifier 屬性來完成的。此屬性是每個觸控點的唯一整數,在每次事件中,在手指接觸表面的整個過程中都保持一致。

這使我們能夠獲取每個觸控點上一個位置的座標,並使用適當的上下文方法繪製連線這兩個位置的線段。

繪製線條後,我們呼叫 Array.splice() 來用 ongoingTouches 陣列中的當前資訊替換關於該觸控點的上一個資訊。

處理觸控結束

當用戶抬起手指離開表面時,會發送一個 touchend 事件。我們透過呼叫下面的 handleEnd() 函式來處理。它的工作是繪製每個結束觸控的最後一條線段,並從正在進行的觸控列表中刪除該觸控點。

js
function handleEnd(evt) {
  evt.preventDefault();
  log("touchend");
  const el = document.getElementById("canvas");
  const ctx = el.getContext("2d");
  const touches = evt.changedTouches;

  for (const touch of touches) {
    const color = colorForTouch(touch);
    let idx = ongoingTouchIndexById(touch.identifier);

    if (idx >= 0) {
      ctx.lineWidth = 4;
      ctx.fillStyle = color;
      ctx.beginPath();
      ctx.moveTo(ongoingTouches[idx].pageX, ongoingTouches[idx].pageY);
      ctx.lineTo(touch.pageX, touch.pageY);
      ctx.fillRect(touch.pageX - 4, touch.pageY - 4, 8, 8); // and a square at the end
      ongoingTouches.splice(idx, 1); // remove it; we're done
    } else {
      log("can't figure out which touch to end");
    }
  }
}

這與上一個函式非常相似;唯一的真正區別是,我們繪製一個小正方形來標記結束,並且當我們呼叫 Array.splice() 時,我們會從正在進行的觸控列表中刪除舊條目,而不新增更新後的資訊。結果是我們停止跟蹤該觸控點。

處理取消的觸控

如果使用者的手指意外移入瀏覽器 UI,或者觸控需要被取消,則會發送 touchcancel 事件,我們呼叫下面的 handleCancel() 函式。

js
function handleCancel(evt) {
  evt.preventDefault();
  log("touchcancel.");
  const touches = evt.changedTouches;

  for (const touch of touches) {
    let idx = ongoingTouchIndexById(touches[i].identifier);
    ongoingTouches.splice(idx, 1); // remove it; we're done
  }
}

由於目的是立即中止觸控,因此我們將其從正在進行的觸控列表中刪除,而不繪製最後的線段。

便利函式

此示例使用了兩個便利函式,應簡要檢視它們,以幫助使其餘程式碼更清晰。

為每個觸控選擇顏色

為了使每個觸控點的繪圖看起來不同,colorForTouch() 函式用於根據觸控點的唯一識別符號選擇顏色。此識別符號是一個不透明的數字,但我們可以依賴它來區分當前活動的觸控點。

js
function colorForTouch(touch) {
  let r = touch.identifier % 16;
  let g = Math.floor(touch.identifier / 3) % 16;
  let b = Math.floor(touch.identifier / 7) % 16;
  r = r.toString(16); // make it a hex digit
  g = g.toString(16); // make it a hex digit
  b = b.toString(16); // make it a hex digit
  const color = `#${r}${g}${b}`;
  return color;
}

此函式的返回值為一個字串,可用於呼叫 <canvas> 函式設定繪圖顏色。例如,對於 Touch.identifier 值為 10,結果字串為“#aa3311”。

複製觸控物件

一些瀏覽器(例如移動版 Safari)會在事件之間重用觸控物件,因此最好複製您關心的屬性,而不是引用整個物件。

js
function copyTouch({ identifier, pageX, pageY }) {
  return { identifier, pageX, pageY };
}

查詢正在進行的觸控

下面的 ongoingTouchIndexById() 函式會掃描 ongoingTouches 陣列以查詢與給定識別符號匹配的觸控,然後返回該觸控在陣列中的索引。

js
function ongoingTouchIndexById(idToFind) {
  for (let i = 0; i < ongoingTouches.length; i++) {
    const id = ongoingTouches[i].identifier;

    if (id === idToFind) {
      return i;
    }
  }
  return -1; // not found
}

顯示正在發生的事情

js
function log(msg) {
  const container = document.getElementById("log");
  container.textContent = `${msg} \n${container.textContent}`;
}

結果

您可以透過觸控下面的框在移動裝置上測試此示例。

注意: 更普遍地說,該示例將在提供觸控事件的平臺上執行。您可以在可以模擬此類事件的桌面平臺上進行測試。

附加提示

本節提供了有關如何在 Web 應用程式中處理觸控事件的其他提示。

處理點選

由於在 touchstart 或一系列 touchmove 事件的第一個事件上呼叫 preventDefault() 會阻止相應的滑鼠事件觸發,因此通常會在 touchmove 而不是 touchstart 上呼叫 preventDefault()。這樣,滑鼠事件仍然可以觸發,連結等內容也可以正常工作。或者,一些框架為了同樣的目的,已開始重新觸發觸控事件作為滑鼠事件。(本示例過於簡化,可能導致奇怪的行為。它僅作為指南。)

js
function onTouch(evt) {
  evt.preventDefault();
  if (
    evt.touches.length > 1 ||
    (evt.type === "touchend" && evt.touches.length > 0)
  )
    return;

  const newEvt = document.createEvent("MouseEvents");
  let type = null;
  let touch = null;

  switch (evt.type) {
    case "touchstart":
      type = "mousedown";
      touch = evt.changedTouches[0];
      break;
    case "touchmove":
      type = "mousemove";
      touch = evt.changedTouches[0];
      break;
    case "touchend":
      type = "mouseup";
      touch = evt.changedTouches[0];
      break;
  }

  newEvt.initMouseEvent(
    type,
    true,
    true,
    evt.originalTarget.ownerDocument.defaultView,
    0,
    touch.screenX,
    touch.screenY,
    touch.clientX,
    touch.clientY,
    evt.ctrlKey,
    evt.altKey,
    evt.shiftKey,
    evt.metaKey,
    0,
    null,
  );
  evt.originalTarget.dispatchEvent(newEvt);
}

僅在第二次觸控時呼叫 preventDefault()

一種防止頁面出現 pinchZoom 等行為的技術是在一系列觸控中的第二次觸控時呼叫 preventDefault()。這種行為在觸控事件規範中沒有明確定義,並且會導致不同瀏覽器出現不同的行為(例如,iOS 會阻止縮放但仍允許雙指平移;Android 會允許縮放但阻止平移;Opera 和 Firefox 目前會阻止所有平移和縮放)。目前,不建議在這種情況下依賴任何特定行為,而應依賴 meta viewport 來阻止縮放。

規範

規範
觸控事件

瀏覽器相容性

觸控事件通常在具有觸控式螢幕的裝置上可用,但許多瀏覽器在所有桌面裝置上都停用了觸控事件 API,即使是那些帶有觸控式螢幕的裝置。

這樣做的原因是,一些網站將觸控事件 API 部分可用性作為瀏覽器在移動裝置上執行的指示器。如果觸控事件 API 可用,這些網站將假定是移動裝置,並提供移動最佳化內容。這可能會為具有觸控式螢幕的桌面裝置使用者提供糟糕的體驗。

為了支援所有型別的裝置上的觸控和滑鼠,請改用 指標事件

api.Touch

api.TouchEvent

api.TouchList