触摸事件

这篇翻译不完整。请帮忙从英语翻译这篇文章

为了给触摸界面提供有力支持, 触摸事件提供了响应用户对触摸屏或者触摸板上操作的能力.

定义

平面
对触摸敏感的平面.可能是屏幕或者触控板.
触摸点
平面上的一个接触点. 有可能是手指 (或者 肘部, 耳朵, 鼻子, 或任何东西, 不过大多数情况下是手指) 或者触摸笔.

接口

TouchEvent
代表当触摸行为在平面上变化的时候发生的事件.
Touch
代表用户与触摸平面间的一个接触点.
TouchList
代表一系列的Touch; 一般在用户多个手指同时接触触控平面时使用这个接口.
DocumentTouch
包含了一些创建 Touch对象与TouchList对象的便捷方法.

例子

这个例子可跟踪多点同时触控,允许用户用多指触摸的方式在 <canvas> 元素上画图. 这个例子只会在支持触摸事件的浏览器下生效.

注意: 我们用“手指”表示用户与触摸平面进行交互,除此以外也可以是触摸笔或者其他方式.

 

创建 canvas

<canvas id="canvas" width="600" height="600" style="border:solid black 1px;">
  Your browser does not support canvas element.
</canvas>
<br>
<button onclick="startup()">Initialize</button>
<br>
Log: <pre id="log" style="border: 1px solid #ccc;"></pre>

设置事件处理器

当页面加载时,下面的startup()函数通过我们在<body> 元素上设置的onload 属性而被触发.

function startup() {
  var el = document.getElementsByTagName("canvas")[0];
  el.addEventListener("touchstart", handleStart, false);
  el.addEventListener("touchend", handleEnd, false);
  el.addEventListener("touchcancel", handleCancel, false);
  el.addEventListener("touchleave", handleLeave, false);
  el.addEventListener("touchmove", handleMove, false);
}

这里给我们的 <canvas>元素设置了所有触摸相关的事件监听器,因此当事件触发时我们就可以处理它们.

跟踪新的触摸行为

当一个 touchstart 事件被触发, 代表在触摸板上一个发生了一个新的触摸行为,下面的 handleStart()函数会被调用.

function handleStart(evt) {
  evt.preventDefault();
  var el = document.getElementsByTagName("canvas")[0];
  var ctx = el.getContext("2d");
  var touches = evt.changedTouches;
        
  for (var i=0; i<touches.length; i++) {
    ongoingTouches.push(touches[i]);
    var color = colorForTouch(touches[i]);
    ctx.fillStyle = color;
    ctx.fillRect(touches[i].pageX-2, touches[i].pageY-2, 4, 4);
  }
}

 event.preventDefault()阻止了浏览器继续处理触摸事件 (这同样也阻止了鼠标事件的传递). 而后我们拿到事件上下文,从事件的TouchEvent.changedTouches 属性中拿到改变中的触摸点列表.

我们遍历上述的点列表Touch 并把这些点压入一个代表当前活动的触摸点组成的数组中,以这些点为起点画矩形; 我们设置线条宽度为四像素,所以最终我们画出来的是一个四乘四的正方形。

当触摸移动时绘制

每当一根或者几根手指在触摸平面上移动时, touchmove 事件被触发, 随之handleMove()函数被调用.此例子中,这个函数更新了上面保存过的触摸点信息,之后,从触摸点之前的位置到现在的位置之间绘制直线,且对每个点都进行这样的操作.

function handleMove(evt) {
  evt.preventDefault();
  var el = document.getElementsByTagName("canvas")[0];
  var ctx = el.getContext("2d");
  var touches = evt.changedTouches;
  
  ctx.lineWidth = 4;
        
  for (var i=0; i<touches.length; i++) {
    var color = colorForTouch(touches[i]);
    var idx = ongoingTouchIndexById(touches[i].identifier);

    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.moveTo(ongoingTouches[idx].pageX, ongoingTouches[idx].pageY);
    ctx.lineTo(touches[i].pageX, touches[i].pageY);
    ctx.closePath();
    ctx.stroke();
    ongoingTouches.splice(idx, 1, touches[i]);  // swap in the new touch record
  }
}

这里同样遍历了所有被改变的触摸点,但为了决定每次新触摸要绘制的线段的起点,它也查询了我们先前缓存的触摸信息数组。这是通过查找每个触摸的  Touch.identifier 属性来做到的.这个属性是个整数,每次触摸都不同,在触摸事件期间手指一直接触表面,这个属性保持不变。

这样我们就可以拿到先前每个触摸的坐标点,之后以适当的上下文方法将两点连接起来,并绘制线段。

当这条线绘制完毕后我们调用 Array.splice(),把ongoingTouches数组中之前的触摸点信息用现在的信息来代替.

对触摸行为的结束进行处理

当用户从触摸表面抬起手指时,touchend 事件被触发. 类似的当手指移除canvas区域外,我们会得到touchleave 事件. 我们利用相同的方式来处理这两种情况,即调用下面的handleEnd()函数.这个函数的作用是给每个已经结束的触摸绘制最后一段线段,同时把这个触摸点从进行中的触摸列表数组中移除.

function handleEnd(evt) {
  evt.preventDefault();
  var el = document.getElementsByTagName("canvas")[0];
  var ctx = el.getContext("2d");
  var touches = evt.changedTouches;
  
  ctx.lineWidth = 4;
        
  for (var i=0; i<touches.length; i++) {
    var color = colorForTouch(touches[i]);
    var idx = ongoingTouchIndexById(touches[i].identifier);
    
    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.moveTo(ongoingTouches[i].pageX, ongoingTouches[i].pageY);
    ctx.lineTo(touches[i].pageX, touches[i].pageY);
    ongoingTouches.splice(i, 1);  // remove it; we're done
  }
}

这个函数跟之前的函数很类似,唯一的区别是我们调用Array.splice(), 在正在进行的触摸列表中,我们仅仅将一个触摸的标识移除,而不再添加这个触摸新的信息。结果就是我们停止跟踪这个触摸点。

处理取消触摸事件

如果用户的手指滑出触摸区域,滑入浏览器界面时,或者触摸需要取消时,touchcancel 事件会被传递,下面的 handleCancel() 函数会被触发.

function handleCancel(evt) {
  evt.preventDefault();
  var touches = evt.changedTouches;
  
  for (var i=0; i<touches.length; i++) {
    ongoingTouches.splice(i, 1);  // remove it; we're done
  }
}

因为我们的目的是立刻结束触摸,所以我们直接从正在进行的触摸列表中删除,不会绘制最后一部分线段。

便捷函数

这个例子使用了两个很方便的函数,有必要简单了解下这两个函数,会有助于更加清楚的理解代码剩余的部分。

为每次触摸选择一个颜色

为了让每次触摸绘制的内容看起来不相同,colorForTouch()函数用来根据每一次触摸所独有的标识来取颜色 . 这个标识的范围通常是0到所有活动触摸对象的数量-1. 而基本不可能会有人用多于16根手指去使用这个demo,我们直接把这种情况转为灰色。

function colorForTouch(touch) {
  var id = touch.identifier;
  id = id.toString(16); // make it a hex digit
  return "#" + id + id + id;
}

这个函数返回一个字符串,可以用在 <canvas> 函数中用来设置绘制颜色. 举例来说,若触摸的标识符Touch.identifier为10, 转换后的字符串为 "#aaa".

查询正在进行的触摸行为

下面的ongoingTouchIndexById() 函数通过遍历查找数组 ongoingTouches 来找到与给定标识相匹配的触摸行为,之后返回这个触摸行为在数组中的下标。

function ongoingTouchIndexById(idToFind) {
  for (var i=0; i<ongoingTouches.length; i++) {
    var id = ongoingTouches[i].identifier;
    
    if (id == idToFind) {
      return i;
    }
  }
  return -1;    // not found
}

查看在线演示

附加小贴士

这部分提供了一些如何在web应用中处理触摸事件的小贴士。

处理点击

当处理一系列触摸操作时,为了阻止鼠标事件的触发,可以touchstart 或第一个 touchmove中调用preventDefault(),更适合的做法是在touchmove中调用preventDefault()

这样做的话,鼠标事件仍然会被触发,相关的如链接等就可以继续工作。有些框架采取了一个替代方案,使用触摸事件代替鼠标事件来达到相同的目的。 (下面这个例子过于简单,也可能产生奇怪的行为。这里仅仅作为一个引导).

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

  var newEvt = document.createEvent("MouseEvents");
  var type = null;
  var touch = null;
  switch (event.type) {
    case "touchstart": type = "mousedown"; touch = event.changedTouches[[0];
    case "touchmove":  type = "mousemove"; touch = event.changedTouches[[0];
    case "touchend":   type = "mouseup"; touch = event.changedTouches[0];
  }
  newEvt.initMouseEvent(type, true, true, event.originalTarget.ownerDocument.defaultView, 0,
    touch.screenX, touch.screenY, touch.clientX, touch.clientY,
    evt.ctrlKey, evt.altKey, evt.shirtKey, evt.metaKey, 0, null);
  event.originalTarget.dispatchEvent(newEvt);
}

只在第二次触摸时调用 preventDefault() 

有一种技术可以在触摸时,阻止页面上发生pinchZoom(捏拉缩放)之类操作,他是通过在一系列触摸操作中,对第二个触摸调用preventDefault()函数. 这个行为未在标准中被定义,不同浏览器的表现也不同(IOS会阻止缩放但扔允许双指平移,Android会允许缩放,但阻止平移。这两种行为在Opera与Firefox中均被阻止。).因此现在不建议使用此方式, 而是通过meta viewport 来阻止页面缩放.

浏览器兼容性

Feature Chrome Firefox (Gecko) Internet Explorer Opera Safari
Basic support 22.0 18.0 (18.0) 未实现 未实现 未实现
Feature Android Chrome for Android Firefox Mobile (Gecko) IE Mobile Opera Mobile Safari Mobile
Basic support (Yes) ? 6.0 (6.0) ? ? (Yes)

Gecko notes

偏好设置 dom.w3c_touch_events.enabled 可以用于打开或者禁用标准触摸事件。默认设置为打开。

Gecko 12.0 note
(Firefox 12.0 / Thunderbird 12.0 / SeaMonkey 2.9)

早于Gecko 12.0 (Firefox 12.0 / Thunderbird 12.0 / SeaMonkey 2.9)内核版本的浏览器不支持多点触控事件,每次触摸事件发生是仅有一个触控点会报告。

Note: 早于 Gecko 6.0 (Firefox 6.0 / Thunderbird 6.0 / SeaMonkey 2.3)内核版本的浏览器提供了 proprietary touch event API,但是该API 已经不建议使用。

文档标签和贡献者

向此页面作出贡献: genie88, ziyunfei, fscholz, ReyCG_sub, xcffl, jtyjty99999
最后编辑者: genie88,