捏合缩放手势

在应用程序中添加手势可以显著提高用户体验。手势有很多类型,从简单的单点触控滑动手势到更复杂的多点触控旋转手势,其中触点(也称为指针)朝不同方向移动。

此示例展示了如何检测捏合/缩放手势,它使用 指针事件 来检测用户是否将两个指针彼此靠近或远离。

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

示例

在此示例中,您使用 指针事件 来同时检测任何类型的两个指向设备,包括手指、鼠标和笔。捏合 (缩小) 手势,它将两个指针彼此靠近移动,会将目标元素的背景色更改为 lightblue。捏合 (放大) 手势,它将两个指针彼此远离移动,会将目标元素的背景色更改为 pink

定义触摸目标

应用程序使用 <div> 来定义指针的目标区域。

html
<style>
  div {
    margin: 0em;
    padding: 2em;
  }
  #target {
    background: white;
    border: 1px solid black;
  }
</style>

全局状态

支持双指针手势需要在各个事件阶段保存指针的事件状态。此应用程序使用两个全局变量来缓存事件状态。

js
// Global vars to cache event state
const evCache = [];
let prevDiff = -1;

注册事件处理程序

事件处理程序已针对以下指针事件注册:pointerdownpointermovepointerup。用于 pointerup 的处理程序用于 pointercancelpointeroutpointerleave 事件,因为在这四个事件中,它们在此应用程序中的语义相同。

js
function init() {
  // Install event handlers for the pointer target
  const el = document.getElementById("target");
  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;
}

指针按下

当指针(鼠标、笔/触控笔或触摸屏上的触摸点)与接触表面接触时,会触发 pointerdown 事件。在此应用程序中,必须缓存事件的状态,以防此按下事件是双指针捏合/缩放手势的一部分。

js
function pointerdownHandler(ev) {
  // The pointerdown event signals the start of a touch interaction.
  // This event is cached to support 2-finger gestures
  evCache.push(ev);
  log("pointerDown", ev);
}

指针移动

pointermove 事件处理程序检测用户是否正在调用双指针捏合/缩放手势。如果两个指针都已按下,并且指针之间的距离在增加(表示捏合或放大),则元素的背景色会更改为 pink,如果指针之间的距离在减小(表示捏合或缩小),则背景色会更改为 lightblue。在更复杂的应用程序中,捏合或捏合的确定可以用于应用特定于应用程序的语义。

处理此事件时,目标的边框将设置为 dashed,以提供一个清晰的视觉指示,表明元素已收到移动事件。

js
function pointermoveHandler(ev) {
  // This function implements a 2-pointer horizontal pinch/zoom gesture.
  //
  // If the distance between the two pointers has increased (zoom in),
  // the target element's background is changed to "pink" and if the
  // distance is decreasing (zoom out), the color is changed to "lightblue".
  //
  // This function sets the target element's border to "dashed" to visually
  // indicate the pointer's target received a move event.
  log("pointerMove", ev);
  ev.target.style.border = "dashed";

  // Find this event in the cache and update its record with this event
  const index = evCache.findIndex(
    (cachedEv) => cachedEv.pointerId === ev.pointerId,
  );
  evCache[index] = ev;

  // If two pointers are down, check for pinch gestures
  if (evCache.length === 2) {
    // Calculate the distance between the two pointers
    const curDiff = Math.abs(evCache[0].clientX - evCache[1].clientX);

    if (prevDiff > 0) {
      if (curDiff > prevDiff) {
        // The distance between the two pointers has increased
        log("Pinch moving OUT -> Zoom in", ev);
        ev.target.style.background = "pink";
      }
      if (curDiff < prevDiff) {
        // The distance between the two pointers has decreased
        log("Pinch moving IN -> Zoom out", ev);
        ev.target.style.background = "lightblue";
      }
    }

    // Cache the distance for the next move event
    prevDiff = curDiff;
  }
}

指针抬起

当指针从接触表面抬起时,会触发 pointerup 事件。发生这种情况时,事件将从事件缓存中删除,目标元素的背景色和边框将恢复为原始值。

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

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

  // If the number of pointers down is less than two then reset diff tracker
  if (evCache.length < 2) {
    prevDiff = -1;
  }
}

应用程序 UI

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

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

html
<body onload="init();" style="touch-action:none">
  <div id="target">
    Touch and Hold with 2 pointers, then pinch in or out.<br />
    The background color will change to pink if the pinch is opening (Zoom In)
    or changes to lightblue if the pinch is closing (Zoom out).
  </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>

其他功能

这些功能支持应用程序,但不会直接参与事件流。

缓存管理

此功能有助于管理全局事件缓存 evCache

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

事件日志记录

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

js
// Log events flag
let logEvents = false;

// Logging/debugging functions
function enableLog(ev) {
  logEvents = !logEvents;
}

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

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

另请参阅