[翻译]整合鼠标、触摸 和触控笔事件的Html5 Pointer Event Api

4,930 阅读9分钟

原文链接

mobiforge.com/design-deve…


(本翻译未完全按照原文进行,因为老外太多废话!)
Pointer Events API 是Hmtl5的事件规范之一,它主要目的是用来将鼠标(Mouse)、触摸(touch)和触控笔(pen)三种事件整合为统一的API。

相比Touch Events API,虽然目前除了Apple的 Safari浏览器,其他浏览器都在实现对该事件类型的支持,但是情况并不是很好。
本篇文章忽略浏览器的兼容问题,只讨论其基本使用方法。更多内容可以参考:Pointer Events 背景资料

Pointer Events

和 Touch Events API 对应于触摸事件类似,Pointer Events API则对应于Pointer事件,那么什么是Pointer呢?

Pointer 是指可以在屏幕上反馈一个指定坐标的输入设备。

Pointer Events继承并扩展了Mouse Event,所以它拥有Mouse Event的常用属性,比如 clientX, clientY等等,同时也增加了一些新的属性,比如tiltX, tiltY, 和 pressure等等。我们对Pointer的如下属性更感兴趣:






























































属性说明
pointerId唯一数值类型标识符
screenX相对于用户屏幕的x坐标
screenY相对于用户屏幕的y坐标
clientX相对于当前窗口的x坐标,不包含滚动条的滚动距离
clientY相对当前窗口的y坐标,不包含滚动条的滚动距离
pageX相对于页面的x坐标, 包含滚动条的滚动距离
pageY相对于页面的y坐标, 包含滚动条的滚动距离
widthpointer在屏幕上接触范围的宽度
heightpointer在屏幕上接触范围的高度
tiltX触控介质沿Z轴方向与x轴的偏转角度, x,y坐标轴构成的平面为屏幕表面
tiltY触控介质沿Z轴方向与y轴的偏转角度, x,y坐标轴构成的平面为屏幕表面
pressure按压值
pointerTypePointer 类别: mouse, pen, 或者touch
isPrimary是否是主Pointer设备

这里有几点需要注意的地方:

. pointerId:代表每一个独立的Pointer。根据id,我们可以很轻松的实现多点触控应用。\
. width/height:Mouse Event 在屏幕上只能覆盖一个点的位置,但是一个Pointer可能覆盖一个更大的区域。\
. isPrimary:当有多个Pointer被检测到的时候(比如多点触摸),对每一种类型的Pointer会存在一个Primary Poiter。只有Primary Poiter会产生与之对应的Mouse Event。稍后会讨论更多与此有关的内容。\
. pressure/tilt/width/height: :这些特性,使程序支持更复杂的操作成为可能。


下面是PointerEvent Api 定义的核心事件:














































事件类型触发时机
pointeroverpointer移动到一个元素所在区域的时候
pointerenterpointer 移动到一个元素或者其后代元素所在区域的时候.
pointerdown激活按钮状态 被赋值为非0值: 对于触摸或触控笔是指和屏幕产生接触的时候; 对于鼠标是指一个按键被按下的时候
pointermovepointer 改变了所在坐标, 或者 按压, 倾斜时,或者触发了没有在规范中定义的其他类型事件
pointerupactive buttons state 值为 left: 触控笔或者手指离开屏幕, 或者释放鼠标按键
pointercancel检测到当前pointer结束操作的时候, 比如改变方向, 触控点太多等意外输入
pointeroutpointer移出一个元素所在区域.在不支持hover的设备上当 pointerup 事件和pointercancel事件被触发后触发
pointerleavepointer 离开元素或者起后代元素后
gotpointercapture当一个元素成为pointer的目标元素的时候
lostpointercapture当元素不在成为pointer的目标元素的时候

Mouse events, pointer events, 和touch events 对照表










































Mouse eventTouch eventPointer event
mousedowntouchstartpointerdown
mouseenterpointerenter
mouseleavepointerleave
mousemovetouchmovepointermove
mouseoutpointerout
mouseoverpointerover
mouseuptouchendpointerup

Mouse Event 和Point Event做一个对等关系很容易,但是Touch Event就没那么乐观了。但是上面的表格只是一个粗略的对照关系,相对应的事件在具体实现和含义上并不完全相同。这意味着你不能使用同一个处理函数来处理不同类型的事件,除非你明确的知道你在干什么,因为这些事件的运作方式不同。例如touchmove 事件的目标元素是touch began 时的元素,即使move的过程中触点不在该元素区域内,touchemove的目标元素仍然不会改变;但是mousemove 和 pointermove的目标元素是位于触点下方的元素,离开该元素区域,目标元素就会改变。

Mouse 兼容事件

Poiter API的强大之处在于它对Mouse Event的兼容,使得基于Mouse Event的站点可以很好的运行。当然这是有意为之,为了达到这个目的,当Pointer Event被触发之后,会再次触发一个对应的Mouse Event。当然只有被认定为主Pointer(primary Pointer)的Pointer才会继续触发Mouse Event。某种程度上,你可以认为在同一时间只有一个鼠标输入。基于Mouse Event 的网站,原有的处理逻辑无需改动,只需要添加新的针对Touch Event的处理逻辑即可。

Pointer API 的好处

Poiter API 整合了鼠标、触摸和触控笔的输入,使得我们无需对各种类型的事件区分对待。

目前不论是web还是本地应用都被设计成跨终端(手机,平板,PC)应用,鼠标多数应用在桌面应用,触摸则出现在各种设备上。过去开发人员必须针对不同的输入设备写不同的代码,或者写一个polyfill 来封装不同的逻辑。Pointer Events 改变了这种状况:

统一事件监听,不用再分别处理\
不用为获取不同事件的坐标值写不同的代码\
如果输入设备支持,可以获取压力、宽、高、倾斜角度等参数\
如果需要的话可以区别对待不同是事件类型

下面是各种事件Api的对比。




















































 Mouse EventsTouch EventsPointer Events
支持鼠标YPY
支持单点触控PYY
支持多点触控NYY
支持 笔, Kinect, 其他输入设备PNY
提供对 over/out/enter/leave events 和 hover 的支持YNY
异步 panning/zooming 硬件加速NNY
W3C 标准YYY

Pointer Events 示例

在本篇文章中,我们只展示Pointer Event Api的基本使用。第一件要做的事情是检测浏览器是否支持Pointer Event。

浏览器支持校验

if (window.PointerEvent) { 
  // Pointer events are supported. 
}

事件监听

第一个demo,我们创建Pointer Event的事件监听程序,打印输入点的坐标值。我们创建两个div,一个用来捕获Pointer事件,另一个用来展现坐标值。

<div id="coords"></div>
  <div id="pointerzone"></div>

接下来添加事件监听的代码:

  function init() {
    // Get a reference to our pointer div
    var pointerzone = document.getElementById("pointerzone");
    // Add an event handler for the pointerdown event
    pointerzone.addEventListener("pointerdown", pointerHandler, false);
}

在pointerHandler函数中,获取并展现pointer事件的坐标值:

  function pointerHandler(event) {
    // Get a reference to our coordinates div
    var coords = document.getElementById("coords");
    // Write the coordinates of the pointer to the div
    coords.innerHTML = 'x: ' + event.pageX + ', y: ' + event.pageY;
  }

我们确保在页面加载完成后执行init函数。

   <body onload="init()">
  ...
  </body>
  }

现在可以在浏览器打开页面了,如果你的浏览器支持pointer event,单击鼠标,就可以在页面看到输出的坐标值了。

pointermove event

和使用touch api的touchmove事件一样,我们可以使用pointermove事件来处理移动事件。

下面我们设计我们的demo:当捕获一个pointerdown 事件的时候,我们开始追踪pointer的移动轨迹。所以我们首先要监听pointerdown事件,然后在pointerdown事件的处理函数中添加对pointermove事件的监听。

 canvas.addEventListener("pointerdown", function() {
    canvas.addEventListener("mousemove", drawpointermove, false);
  }
  , false);

在drawpointermove函数中,我们根据前后两个点的坐标,来连续绘制轨迹。

function draw(e) {
  ctx.beginPath();
  // Start at previous point
  ctx.moveTo(lastPt.x, lastPt.y);
  // Line to latest point
  ctx.lineTo(e.pageX, e.pageY);
  // Draw it!
  ctx.stroke();

  //Store latest pointer
  lastPt = {x:e.pageX, y:e.pageY};
}

当pointer路径结束的时候——用户移开了手指或者笔尖,松开了鼠标按钮——我们需要停止绘图。所以我们需要监听pointerup事件,并添加一个endPointer处理函数。

 canvas.addEventListener("pointerup", endPointer, false);

  function endPointer(e) {
    //Stop tracking the pointermove event
    canvas.removeEventListener("pointermove", drawpointermove, false); 

    //Set last point to null to end our pointer path
    lastPt = null;
  }

运行结果:

下面给出这个demo的完整代码:

<html>
      <head>
        <style>
          /* Disable intrinsic user agent touch behaviors (such as panning or zooming) */
          canvas {
            touch-action: none;
          }
        </style>


      <script type='text/javascript'>
        var lastPt = null;
        var canvas;
        var ctx;

        function init() {
          canvas = document.getElementById("mycanvas");
          ctx = canvas.getContext("2d");
          var offset  = getOffset(canvas);
          if(window.PointerEvent) {
            canvas.addEventListener("pointerdown", function() {
              canvas.addEventListener("pointermove", draw, false);
            }
            , false);
            canvas.addEventListener("pointerup", endPointer, false);
          }
          else {
            //Provide fallback for user agents that do not support Pointer Events
            canvas.addEventListener("mousedown", function() {
              canvas.addEventListener("mousemove", draw, false);
            }
            , false);
            canvas.addEventListener("mouseup", endPointer, false);
          }
        }

        // Event handler called for each pointerdown event:
        function draw(e) {
          if(lastPt!=null) {
            ctx.beginPath();
            // Start at previous point
            ctx.moveTo(lastPt.x, lastPt.y);
            // Line to latest point
            ctx.lineTo(e.pageX, e.pageY);
            // Draw it!
            ctx.stroke();
          }
          //Store latest pointer
          lastPt = {x:e.pageX, y:e.pageY};
        }

        function getOffset(obj) {
          //...
        }

        function endPointer(e) {
          //Stop tracking the pointermove (and mousemove) events
          canvas.removeEventListener("pointermove", draw, false); 
          canvas.removeEventListener("mousemove", draw, false); 

          //Set last point to null to end our pointer path
          lastPt = null;
        }

      </script>
    </head>
    <body onload="init()">
      <canvas id="mycanvas" width="500" height="500" style="border:1px solid black;"></canvas>
    </body>
  </html>

多点触控

这个例子中,我们扩展上面的pointmove事件的代码,来实现对多点触控的支持。

首先我们初始一个多个颜色的数组,用来追踪不同的pointer。

var colours = ['red', 'green', 'blue', 'yellow','black'];

画线的时候通过pointer的id来取色。

 //Key the colour based on the id of the Pointer
  multitouchctx.strokeStyle = colours[id%5];
  multitouchctx.lineWidth = 3;

在这个demo中,我们要追踪每一个pointer,所以需要分别保存每一个pointer的相关坐标点。这里我们使用关联数组来存储数据,每个数据使用pointerId做key,我们使用一个Object对象作为关联数组,用如下方法添加数据:

// This will be our associative array
var multiLastPt=new Object();
...
// Get the id of the pointer associated with the event
var id = e.pointerId;
...
// Store coords
multiLastPt[id] = {x:e.pageX, y:e.pageY};

结束画线的时候,需要删除相关数据。

  delete multiLastPt[id];

运行结果如下:

完整代码如下:

<!DOCTYPE html>
<html>
  <head>
  <title>HTML5 multi-touch</title>
    <style>
     canvas {
       touch-action: none;
     }
    </style>
    <script>
    var canvas;
    var ctx;
    var lastPt = new Object();
    var colours = ['red', 'green', 'blue', 'yellow', 'black'];

    function init() {
      canvas = document.getElementById('mycanvas');
      ctx = canvas.getContext("2d");

          if(window.PointerEvent) {
            canvas.addEventListener("pointerdown", function() {
              canvas.addEventListener("pointermove", draw, false);
            }
            , false);
            canvas.addEventListener("pointerup", endPointer, false);
          }
          else {
            //Provide fallback for user agents that do not support Pointer Events
            canvas.addEventListener("mousedown", function() {
              canvas.addEventListener("mousemove", draw, false);
            }
            , false);
            canvas.addEventListener("mouseup", endPointer, false);
          }
    }

    function draw(e) {
      var id = e.pointerId;   
      if(lastPt[id]) {
        ctx.beginPath();
        ctx.moveTo(lastPt[id].x, lastPt[id].y);
        ctx.lineTo(e.pageX, e.pageY);
        ctx.strokeStyle = colours[id%5];
        ctx.stroke();
      }
      // Store last point
      lastPt[id] = {x:e.pageX, y:e.pageY};
    }

    function endPointer(e) {
      var id = e.pointerId;

      canvas.removeEventListener("mousemove", draw, false); 
      // Terminate this touch
      delete lastPt[id];
    }  

  </script>
  </head>
  <body onload="init()">
    <canvas id="mycanvas" width="500" height="500">
      Canvas element not supported.
    </canvas>
  </body>
</html>

小结

本文只是简单介绍了Pointer Event的使用,虽然目前浏览器的支持情况并不完美,但是作为w3c的标准,会被支持的越来越好。

如果你在开发中使用Pointer Event Api,一定要注意它和touch事件的区别,处理touch相关操作的时候要谨慎。


欢迎关注玄魂工作室--订阅号回复“html5”,更多前端开发知识