多点触控交互

指针事件扩展了 DOM 输入事件,以支持各种指向输入设备,例如笔/触控笔和触摸屏,以及鼠标。指针是一个与硬件无关的设备,它可以定位到一组特定的屏幕坐标。使用单个事件模型来处理指针可以简化网站和应用程序的创建,并提供良好的用户体验,无论用户使用什么硬件。

指针事件与鼠标事件有很多相似之处,但它们支持多个同时指针,例如触摸屏上的多个手指。此附加功能可用于提供更丰富​​的用户交互模型,但代价是多点触控交互处理的复杂性增加。本文档通过示例代码演示了如何使用指针事件与不同的多点触控交互。

该应用程序的实时版本可在 GitHub 上找到。源代码在 GitHub 上可用;欢迎拉取请求和 错误报告

示例

此示例演示了使用指针事件的各种事件类型(pointerdownpointermovepointerup pointercancel 等)来处理不同的多点触控交互。

定义触摸目标

该应用程序使用 <div> 来定义三个不同的触摸目标区域。

html
<style>
  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;
  }
</style>

全局状态

为了支持多点触控交互,需要在各种事件阶段保留指针的事件状态。此应用程序使用三个数组来缓存事件状态,每个目标元素一个缓存。

js
// Log events flag
const logEvents = false;

// Event caches, one per touch target
const evCache1 = [];
const evCache2 = [];
const evCache3 = [];

注册事件处理程序

为以下指针事件注册事件处理程序:pointerdownpointermovepointeruppointerup 的处理程序用于 pointercancelpointeroutpointerleave 事件,因为这四个事件在此应用程序中具有相同的语义。

js
function setHandlers(name) {
  // Install event handlers for the given element
  const el = document.getElementById(name);
  el.onpointerdown = pointerdownHandler;
  el.onpointermove = pointermoveHandler;

  // Use same handler for pointer{up,cancel,out,leave} events since
  // the semantics for these events - in this app - are the same.
  el.onpointerup = pointerupHandler;
  el.onpointercancel = pointerupHandler;
  el.onpointerout = pointerupHandler;
  el.onpointerleave = pointerupHandler;
}

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

指针按下

当指针(鼠标、笔/触控笔或触摸屏上的触摸点)接触接触面时,会触发 pointerdown 事件。必须缓存事件状态,以防此按下事件是多点触控交互的一部分。

在此应用程序中,当指针放置在元素上时,元素的背景颜色会根据元素具有的活动触摸点数量而改变。有关颜色变化的更多详细信息,请参见 update_background 函数。

js
function pointerdownHandler(ev) {
  // The pointerdown event signals the start of a touch interaction.
  // Save this event for later processing (this could be part of a
  // multi-touch interaction) and update the background color
  pushEvent(ev);
  if (logEvents) {
    log(`pointerDown: name = ${ev.target.id}`, ev);
  }
  updateBackground(ev);
}

指针移动

当指针移动时,会调用 pointermove 处理程序。它可能会被多次调用(例如,如果用户移动了指针),然后才触发其他事件类型。

在此应用程序中,指针移动由将目标的边框设置为 dashed 来表示,以提供清晰的视觉指示,表明该元素已收到此事件。

js
function pointermoveHandler(ev) {
  // Note: if the user makes more than one "simultaneous" touch, most browsers
  // fire at least one pointermove event and some will fire several pointermoves.
  //
  // This function sets the target element's border to "dashed" to visually
  // indicate the target received a move event.
  if (logEvents) {
    log("pointerMove", ev);
  }
  updateBackground(ev);
  ev.target.style.border = "dashed";
}

指针抬起

当指针从接触面抬起时,会触发 pointerup 事件。发生这种情况时,事件将从关联的事件缓存中删除。

在此应用程序中,此处理程序还用于 pointercancelpointerleavepointerout 事件。

js
function pointerupHandler(ev) {
  if (logEvents) {
    log(ev.type, ev);
  }
  // Remove this touch point from the cache and reset the target's
  // background and border
  removeEvent(ev);
  updateBackground(ev);
  ev.target.style.border = "1px solid black";
}

应用程序 UI

该应用程序使用 <div> 元素作为触摸区域,并提供按钮来启用日志记录和清除日志。

为了防止浏览器的默认触摸行为覆盖此应用程序的指针处理,将 touch-action 属性应用于 <body> 元素。

html
<body onload="init();" style="touch-action:none">
  <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>

  <!-- UI for logging/debugging -->
  <button id="log" onclick="enableLog(event);">Start/Stop event logging</button>
  <button id="clearlog" onclick="clearLog(event);">Clear the log</button>
  <p></p>
  <output></output>
</body>

杂项函数

这些函数支持应用程序,但没有直接参与事件流。

缓存管理

这些函数管理全局事件缓存 evCache1evCache2evCache3

js
function getCache(ev) {
  // Return the cache for this event's target element
  switch (ev.target.id) {
    case "target1":
      return evCache1;
    case "target2":
      return evCache2;
    case "target3":
      return evCache3;
    default:
      log("Error with cache handling", ev);
  }
}

function pushEvent(ev) {
  // Save this event in the target's cache
  const evCache = getCache(ev);
  evCache.push(ev);
}

function removeEvent(ev) {
  // Remove this event from the target's cache
  const evCache = getCache(ev);
  const index = evCache.findIndex(
    (cachedEv) => cachedEv.pointerId === ev.pointerId,
  );
  evCache.splice(index, 1);
}

更新背景颜色

触摸区域的背景颜色将按如下方式更改:没有活动触摸为 白色;一个活动触摸为 黄色;两个同时触摸为 粉色;三个或更多同时触摸为 浅蓝色

js
function updateBackground(ev) {
  // Change background color based on the number of simultaneous touches/pointers
  // currently down:
  //   white - target element has no touch points i.e. no pointers down
  //   yellow - one pointer down
  //   pink - two pointers down
  //   lightblue - three or more pointers down
  const evCache = getCache(ev);
  switch (evCache.length) {
    case 0:
      // Target element has no touch points
      ev.target.style.background = "white";
      break;
    case 1:
      // Single touch point
      ev.target.style.background = "yellow";
      break;
    case 2:
      // Two simultaneous touch points
      ev.target.style.background = "pink";
      break;
    default:
      // Three or more simultaneous touches
      ev.target.style.background = "lightblue";
  }
}

事件日志记录

这些函数用于将事件活动发送到应用程序窗口(以支持调试和了解事件流)。

js
// Log events flag
let logEvents = false;

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

function log(name, ev) {
  const o = document.getElementsByTagName("output")[0];
  o.innerText += `${name}:
  pointerID   = ${ev.pointerId}
  pointerType = ${ev.pointerType}
  isPrimary   = ${ev.isPrimary}
`;
}

function clearLog(event) {
  const o = document.getElementsByTagName("output")[0];
  o.textContent = "";
}