多點觸控互動

觸控事件介面支援應用程式特定的單點和多點觸控互動。然而,對於程式設計師來說,這些介面可能有點棘手,因為觸控事件與滑鼠事件等其他 DOM 輸入事件非常不同。本指南中描述的應用程式展示瞭如何使用觸控事件進行簡單的單點和多點觸控互動,這是構建應用程式特定手勢所需的基礎。

此應用程式的即時版本可在 GitHub 上找到。原始碼可在 GitHub 上找到,歡迎提交拉取請求和Bug 報告

示例

本示例演示瞭如何使用 touchstarttouchmovetouchcanceltouchend) 觸控事件來處理以下手勢:單點觸控、兩點(同時)觸控、多於兩點同時觸控、單指滑動以及兩指移動/捏合/滑動。

定義觸控目標

該應用程式使用 <div> 元素來表示四個觸控區域。

css
div {
  margin: 0em;
  padding: 2em;
}
#target1 {
  background: white;
  border: 1px solid black;
}
#target2 {
  background: white;
  border: 1px solid black;
}
#target3 {
  background: white;
  border: 1px solid black;
}
#target4 {
  background: white;
  border: 1px solid black;
}

全域性狀態

tpCache 用於在觸控點觸發的事件之外快取觸控點以供處理。

js
// Log events flag
let logEvents = false;

// Touch Point cache
const tpCache = [];

註冊事件處理程式

為所有四種觸控事件型別註冊了事件處理程式。touchendtouchcancel 事件型別使用相同的處理程式。

js
function setHandlers(name) {
  // Install event handlers for the given element
  const el = document.getElementById(name);
  el.addEventListener("touchstart", startHandler);
  el.addEventListener("touchmove", moveHandler);
  // Use same handler for touchcancel and touchend
  el.addEventListener("touchcancel", endHandler);
  el.addEventListener("touchend", endHandler);
}

function init() {
  setHandlers("target1");
  setHandlers("target2");
  setHandlers("target3");
  setHandlers("target4");
}

移動/捏合/縮放處理程式

此函式提供了對兩點水平移動/捏合/縮放處理非常基礎的支援。程式碼不包含錯誤處理或垂直移動。請注意,捏合和縮放移動檢測的閾值是應用程式特定的(且依賴於裝置)的。

js
// This is a very basic 2-touch move/pinch/zoom handler that does not include
// error handling, only handles horizontal moves, etc.
function handlePinchZoom(ev) {
  if (ev.targetTouches.length === 2 && ev.changedTouches.length === 2) {
    // Check if the two target touches are the same ones that started
    // the 2-touch
    const point1 = tpCache.findLastIndex(
      (tp) => tp.identifier === ev.targetTouches[0].identifier,
    );
    const point2 = tpCache.findLastIndex(
      (tp) => tp.identifier === ev.targetTouches[1].identifier,
    );

    if (point1 >= 0 && point2 >= 0) {
      // Calculate the difference between the start and move coordinates
      const diff1 = Math.abs(
        tpCache[point1].clientX - ev.targetTouches[0].clientX,
      );
      const diff2 = Math.abs(
        tpCache[point2].clientX - ev.targetTouches[1].clientX,
      );

      // This threshold is device dependent as well as application specific
      const PINCH_THRESHOLD = ev.target.clientWidth / 10;
      if (diff1 >= PINCH_THRESHOLD && diff2 >= PINCH_THRESHOLD)
        ev.target.style.background = "green";
    } else {
      // empty tpCache
      tpCache.length = 0;
    }
  }
}

觸控開始處理程式

touchstart 事件處理程式會快取觸控點以支援兩點手勢。它還呼叫 preventDefault() 來阻止瀏覽器應用進一步的事件處理(例如,滑鼠事件模擬)。

js
function startHandler(ev) {
  // If the user makes simultaneous touches, the browser will fire a
  // separate touchstart event for each touch point. Thus if there are
  // three simultaneous touches, the first touchstart event will have
  // targetTouches length of one, the second event will have a length
  // of two, and so on.
  ev.preventDefault();
  // Cache the touch points for later processing of 2-touch pinch/zoom
  if (ev.targetTouches.length === 2) {
    for (const touch of ev.targetTouches) {
      tpCache.push(touch);
    }
  }
  if (logEvents) log("touchStart", ev, true);
  updateBackground(ev);
}

觸控移動處理程式

touchmove 處理程式出於與上述相同的原因呼叫 preventDefault(),並呼叫捏合/縮放處理程式。

js
function moveHandler(ev) {
  // Note: if the user makes more than one "simultaneous" touches, most browsers
  // fire at least one touchmove event and some will fire several touch moves.
  // Consequently, an application might want to "ignore" some touch moves.
  //
  // This function sets the target element's border to "dashed" to visually
  // indicate the target received a move event.
  //
  ev.preventDefault();
  if (logEvents) log("touchMove", ev, false);
  // To avoid too much color flashing many touchmove events are started,
  // don't update the background if two touch points are active
  if (!(ev.touches.length === 2 && ev.targetTouches.length === 2))
    updateBackground(ev);

  // Set the target element's border to dashed to give a clear visual
  // indication the element received a move event.
  ev.target.style.border = "dashed";

  // Check this event for 2-touch Move/Pinch/Zoom gesture
  handlePinchZoom(ev);
}

觸控結束處理程式

touchend 處理程式將事件目標的背景顏色恢復為其原始顏色。

js
function endHandler(ev) {
  ev.preventDefault();
  if (logEvents) log(ev.type, ev, false);
  if (ev.targetTouches.length === 0) {
    // Restore background and border to original values
    ev.target.style.background = "white";
    ev.target.style.border = "1px solid black";
  }
}

應用程式 UI

該應用程式使用 <div> 元素作為觸控區域,並提供按鈕來啟用日誌記錄和清除日誌。

html
<div id="target1">Tap, Hold or Swipe me 1</div>
<div id="target2">Tap, Hold or Swipe me 2</div>
<div id="target3">Tap, Hold or Swipe me 3</div>
<div id="target4">Tap, Hold or Swipe me 4</div>

<!-- UI for logging/debugging -->
<button id="toggle-log">Start/Stop event logging</button>
<button id="clear-log">Clear the log</button>
<output id="output"></output>

雜項函式

這些函式支援該應用程式,但與事件流沒有直接關係。

更新背景顏色

觸控區域的背景顏色將按如下方式更改:無觸控時為 white;單點觸控時為 yellow;兩點同時觸控時為 pink;三點或更多點同時觸控時為 lightblue。有關檢測到兩指移動/捏合/縮放時背景顏色變化的詳細資訊,請參閱觸控移動處理程式

js
function updateBackground(ev) {
  // Change background color based on the number simultaneous touches
  // in the event's targetTouches list:
  //   yellow - one tap (or hold)
  //   pink - two taps
  //   lightblue - more than two taps
  switch (ev.targetTouches.length) {
    case 1:
      // Single tap`
      ev.target.style.background = "yellow";
      break;
    case 2:
      // Two simultaneous touches
      ev.target.style.background = "pink";
      break;
    default:
      // More than two simultaneous touches
      ev.target.style.background = "lightblue";
  }
}

事件日誌記錄

這些函式用於將事件活動記錄到應用程式視窗,以支援除錯和了解事件流。

js
const output = document.getElementById("output");

function toggleLog(ev) {
  logEvents = !logEvents;
}

document.getElementById("toggle-log").addEventListener("click", toggleLog);

function log(name, ev, printTargetIds) {
  let s =
    `${name}: touches = ${ev.touches.length} ; ` +
    `targetTouches = ${ev.targetTouches.length} ; ` +
    `changedTouches = ${ev.changedTouches.length}`;
  output.innerText += `${s}\n`;

  if (printTargetIds) {
    s = "";
    for (const touch of ev.targetTouches) {
      s += `... id = ${touch.identifier}\n`;
    }
    output.innerText += s;
  }
}

function clearLog(event) {
  output.textContent = "";
}

document.getElementById("clear-log").addEventListener("click", clearLog);

另見