触摸事件

为了为基于触摸的用户界面提供高质量的支持,触摸事件提供了解释触摸屏或触控板上手指(或手写笔)活动的能力。

触摸事件接口是相对低级的 API,可用于支持特定于应用程序的多点触控交互,例如双指手势。多点触控交互从手指(或手写笔)首次触碰接触面开始。其他手指随后可能会触碰表面,并可选地在触碰表面上移动。当手指从表面移开时,交互结束。在此交互过程中,应用程序会在开始、移动和结束阶段接收触摸事件。

触摸事件类似于鼠标事件,只是它们支持同时触摸和触碰表面上的不同位置。TouchEvent 接口封装了当前处于活动状态的所有触点。Touch 接口表示单个触点,它包含触点相对于浏览器视窗位置的信息。

定义

表面

触摸敏感的表面。这可能是屏幕或触控板。

触点

与表面的接触点。这可能是手指(或肘部、耳朵、鼻子,等等,但通常是手指)或手写笔。

接口

TouchEvent

表示表面上触摸状态发生变化时发生的事件。

触摸

表示用户与触摸表面之间单个接触点。

TouchList

表示一组触摸;当用户例如同时在表面上有多个手指时使用它。

示例

此示例一次跟踪多个触点,允许用户使用多个手指在<canvas>中绘图。它仅适用于支持触摸事件的浏览器。

注意:下面的文本在描述与表面的接触时使用术语“手指”,但它当然也可以是手写笔或其他接触方式。

创建画布

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

设置事件处理程序

页面加载时,将调用下面显示的startup() 函数。这为我们的<canvas> 元素设置了所有事件侦听器,以便我们可以处理发生时的触摸事件。

js
function startup() {
  const el = document.getElementById("canvas");
  el.addEventListener("touchstart", handleStart);
  el.addEventListener("touchend", handleEnd);
  el.addEventListener("touchcancel", handleCancel);
  el.addEventListener("touchmove", handleMove);
  log("Initialized.");
}

document.addEventListener("DOMContentLoaded", startup);

跟踪新的触摸

我们将跟踪正在进行的触摸。

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++) {
    log(`touchstart: ${i}.`);
    ongoingTouches.push(copyTouch(touches[i]));
    const color = colorForTouch(touches[i]);
    log(`color of touch with id ${touches[i].identifier} = ${color}`);
    ctx.beginPath();
    ctx.arc(touches[i].pageX, touches[i].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 (let i = 0; i < touches.length; i++) {
    const color = colorForTouch(touches[i]);
    const idx = ongoingTouchIndexById(touches[i].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( ${touches[i].pageX}, ${touches[i].pageY} );`);
      ctx.lineTo(touches[i].pageX, touches[i].pageY);
      ctx.lineWidth = 4;
      ctx.strokeStyle = color;
      ctx.stroke();

      ongoingTouches.splice(idx, 1, copyTouch(touches[i])); // 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 (let i = 0; i < touches.length; i++) {
    const color = colorForTouch(touches[i]);
    let idx = ongoingTouchIndexById(touches[i].identifier);

    if (idx >= 0) {
      ctx.lineWidth = 4;
      ctx.fillStyle = color;
      ctx.beginPath();
      ctx.moveTo(ongoingTouches[idx].pageX, ongoingTouches[idx].pageY);
      ctx.lineTo(touches[i].pageX, touches[i].pageY);
      ctx.fillRect(touches[i].pageX - 4, touches[i].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 (let i = 0; i < touches.length; i++) {
    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,生成的字符串为“#a31”。

复制触摸对象

一些浏览器(例如移动版 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}`;
}

结果

您可以在移动设备上测试此示例,方法是触摸下面的框。

注意:更普遍地说,此示例将在提供触摸事件的平台上运行。您可以在可以模拟此类事件的桌面平台上测试它。

  • 在 Firefox 中,在响应式设计模式(您可能需要重新加载页面)中启用“触摸模拟”。
  • 在 Chrome 中,使用设备模式并将设备类型设置为发送触摸事件的类型。

其他提示

本节提供有关如何在 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 目前阻止所有平移和缩放。)目前,不建议依赖此情况下的任何特定行为,而是依赖元视口来阻止缩放。

规范

规范
触摸事件

浏览器兼容性

触摸事件通常在配备触摸屏的设备上可用,但许多浏览器在所有桌面设备上都禁用了触摸事件 API,即使这些设备配备了触摸屏。

造成这种情况的原因是,一些网站使用触摸事件 API 部分的可用性来判断浏览器是否在移动设备上运行。如果触摸事件 API 可用,这些网站会认为是移动设备并提供移动优化内容。这可能会给配备触摸屏的桌面设备用户带来糟糕的体验。

为了在所有类型的设备上同时支持触摸和鼠标,请使用 指针事件

api.Touch

BCD 表格仅在浏览器中加载

api.TouchEvent

BCD 表格仅在浏览器中加载

api.TouchList

BCD 表格仅在浏览器中加载