Web应用程序中的多点触控

简介

随着多点触控智能手机的出现,在HTML 5中实现多点触摸API的需求也相应出现。 2011年,W3C开始着手实现触摸API,并在2013年10月发布了最终版本。 该API是非常简单的,我们将研究它的细节。

新属性

正如你所期望的那样,用手指或手写笔触摸屏幕会产生一个事件,该事件送由程序处理。 这样的事件携带关于触摸的附加信息。 它包含诸如type,altKeyctrlKeyshiftKeytargetpageXpageY标准事件的属性。 还有三个:touches,targetTouches和changedTouches。 让我们来讨论它们:

  • touches - 在当前屏幕上被触摸的所有接触点的列表,包括添加了事件监听器的元件外的接触点。
  • targetTouches - 在当前屏幕上被触摸的接触点的列表,只包括添加了事件监听器的元件里的接触点。 当触摸点从添加了事件侦听器的元件内部开始,然后被移到该元件以外,它仍然会被跟踪,并且点的位置信息会被保存在targetTouches列表中。
  • changedTouches - 所有位置或状态改变了的接触点列表,包括添加了事件监听器的元件外的点。

每一个接触点包括存储在几个属性中的X和Y坐标:

  • clientX和clientY - 触摸点相对于视窗的坐标(不包括滚动偏移),
  • pageX和pageY - 触摸点相对于视窗的坐标(包括滚动偏移),
  • screenX和screenY - 触摸点相对于屏幕的坐标。

触摸事件

使用触摸API,我们得新的事件:touchstart,touchmove,touchend,touchcancel,这些是标准的。 让我们来看看每一个事件:

  • touchstart - 当手指(或其他触控元件)已经接触屏幕时触发,
  • touchmove - 当手指(或其他触摸元件)已沿着屏的表面移动时触发,
  • touchend - 当手指(或其他触摸元件)已被从屏幕的表面上移开时触发。
  • touchcancel - 在特定情况下触发,不同浏览器和不同平台会有差异。 在Tizen操作系统中,当用户长按一个DOM元素,如文本或图像时触发它。

有些浏览器或JavaScript库可以外两个事件touchenter和touchleave,但它们不是标准的一部分。

用法

我们要做的第一件事情是为元件添加事件监听器,用于接收事件。 我们使用addEventListener函数来实现。

var canvas = document.getElementById('canvas');
canvas.addEventListener('touchstart', function (e) {
    e.preventDefault();
}, false);

第一个参数是事件名称,第二个是当事件发生时将被执行的事件处理程序。 第三个参数是一个标志,用来指示特定的元件是否应当接收这种事件类型(优先于它下面的DOM树中的任何其他元件)。 处理函数的第一个参数是事件对象,其包含了触摸点的列表。

如果我们要处理一个触摸事件,我们绝对应该做的一件事是阻止默认行为。 如果我们不这样做的话,当我们用手指触摸时,可能会得到意外行为,比如滚动或缩放视窗。

我们可以设计自己的触摸手势。例如,当我们要重新创建收缩和缩放手势时,我们需要检查我们是否使用两个手指触摸了屏幕,是否两个手指之间的距离增大了。 示例代码应该是这样的:

var prevDistance = 0;
var handler = function (e) {
    e.preventDefault();
    
    if (e.touches.length === 2) {
        var a = {
            x: e.touches[0].pageX,
            y: e.touches[0].pageY
        };
        var b = {
            x: e.touches[1].pageX,
            y: e.touches[1].pageY
        };
        var currDistance = Math.sqrt(Math.pow(b.y - a.y, 2) + Math.pow(b.x - a.x, 2));
        var zoomIn = prevDistance < currDistance;
        prevDistance = currDistance;
        document.body.innerHTML = currDistance + ' ' + (zoomIn ? 'ZOOM IN' : 'ZOOM OUT');
    }
};
window.addEventListener('touchstart', handler, false);
window.addEventListener('touchmove', handler, false);
window.addEventListener('touchend', handler, false);

正如你所看到的,我们必须保存先前计算的两个手指之间的距离(prevDinstance)。 我们为touchstart,touchmove和touchend事件添加了事件侦听器。 事件处理程序检查我们是否使用了两个接触点,并存储这些点,用于后面的计算。 我们使用标准的数学公式计算两点之间的距离,我们会检查相对于之前测量的距离,新的距离是变大了还是缩小了。 这是最简单的方法,若要获得更准确的数据,需要计算两个触摸点和中间点之间的角度。 可以将中间点设定为锚点,根据它来放大或缩小。 如果你想旋转视口,可以使用角度。

触摸测试应用程序

了解事件是如何工作的最佳方法是运行测试应用程序。 该TouchTest应用程序分为两个部分:顶部和底部。 每个部分处理触控事件,并打印出发生相关事件区域的详细信息(touches,targetTouches和changedTouches点)。 在下面的图片中可以看到该应用程序的截图。

TouchTest应用程序截图
TouchTest应用程序截图

运行测试应用程序并观察使用手指触摸屏幕时,接触点是如何变化的,以及不同情况下触发了哪些事件。

示例应用程序

本文附件中附带了一个示例应用程序。 它展示了触摸事件的用法。 示例应用程序是一个简单的乒乓球游戏。 先不使用外接渲染引擎,它由一些DOM元素做成,而不是在画布上渲染它。 在下面的图片中,可以看到应用程序的截图。

示例应用程序截图
示例应用程序截图

运行游戏后,你必须点击屏幕中央的大播放按钮。 该游戏有两名玩家。 每个玩家都可以在他的区域内通过移动手指控制自己的球拍。 当有一个玩家得到9分时,游戏结束。 让我们研究下应用程序中最重要的部分。

处理触摸事件

我们通过在窗口对象添加事件侦听器来收集事件。

window.addEventListener('touchstart', touchHandle, false);
window.addEventListener('touchmove', touchHandle, false);
window.addEventListener('touchend', touchHandle, false);
window.addEventListener('touchcancel', touchHandle, false);

为方便起见,我们在touches量中保存触摸事件。 我们只收集来changeTouches点列表中的点。 当touchend或touchcancel事件后,touches数组将被清空。 很重要的是要防止默认行为。 这样我们在触摸屏幕时就不会出现滚动或缩放视口的现象。

var touches = [];
var touchHandle = function (e) {
    e.preventDefault();
    
    if (e.type === 'touchend' || e.type === 'touchcancel') {
        touches = [];
    } else {
        if (e.changedTouches.length > 0) {
            touches = [];
            for (var i = 0; i < Math.min(e.changedTouches.length, 2); i++) {
                touches.push({
                    x: e.changedTouches[i].pageX,
                    y: e.changedTouches[i].pageY
                });
            }
        }
    }
};

现在,我们有了接触点数组,我们必须于触摸事件发生的区域,将它们传递到合适的Player。 我们通过Player构造数中的step函数来实现该功能。 我们只需要检查触摸点的Y属性,并确保它不超过玩家区域的边缘。

// Calculate edges of the player's area.
var edges = {
    left: offset.left,
    top: offset.top,
    right: offset.left + $area.width(),
    bottom: offset.top + $area.height()
};

this.step = function (dt) {
    var self = this,
        touch, position;
    
    for (var i = 0; i < touches.length; i++) {
        touch = touches[i];
        if (touch.y >= edges.top && touch.y <= edges.bottom) {
            position = touch.x - (config.pad.width / 2);
            position = Math.max(0, Math.min(config.client.width - config.pad.width, position));
            self.setPosition(position);
        }
    }
};

游戏循环和碰撞

我们有三数:Player, Ball 和 Game。 两个玩家对象和一个球象在Game构造函数中创建。

var p1 = new Player('p1-area', 'p1-pad');
var p2 = new Player('p2-area', 'p2-pad');
var ball = new Ball('ball');

游戏的核心是一个游戏循环,该循环每秒被执行60次。 它只使用了最简单的方法,因为它不属于本文的讨论范围。 然而,对于产品级的用程序,你可能会使用requestAnimationFrame函数来确保动画按照每秒多少帧的速度执行,以提供最佳的用户体验。 循环秒执行60次Game.step函数。

this.init = function () {
    var self = this;
    
    timer = window.setInterval(function () {
        self.step(dt);
    }, dt * 1000);
};

Game.step数会执行每一个对象的step函数,检查碰撞并更新游戏的用户界面。

this.step = function (dt) {
    var self = this;
    
    ball.step(dt);
    p1.step(dt);
    p2.step(dt);
    collisionsDetection();
    
    update();
};

collisionsDetection数看起来很复杂,但它只是检查球是否撞击了墙壁或球拍。 撞墙或球拍会使球反弹。 可以通过翻转球的速度向量来实现。

总结

我希望在读完这片文章后,您更了解触摸事件了。并且您可以使用多点触控功能编写自己的应用程序。

文件附件: