Web应用程序中的多点触控
PUBLISHED
简介
随着多点触控智能手机的出现,在HTML 5中实现多点触摸API的需求也相应出现。 2011年,W3C开始着手实现触摸API,并在2013年10月发布了最终版本。 该API是非常简单的,我们将研究它的细节。
新属性
正如你所期望的那样,用手指或手写笔触摸屏幕会产生一个事件,该事件送由程序处理。 这样的事件携带关于触摸的附加信息。 它包含诸如type,altKey,ctrlKey,shiftKey,target,pageX,pageY等标准事件的属性。 还有三个属性: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点)。 在下面的图片中可以看到该应用程序的截图。
运行测试应用程序并观察使用手指触摸屏幕时,接触点是如何变化的,以及不同情况下触发了哪些事件。
示例应用程序
本文附件中附带了一个示例应用程序。 它展示了触摸事件的用法。 示例应用程序是一个简单的乒乓球游戏。 先不使用外接渲染引擎,它由一些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函数看起来很复杂,但它只是检查球是否撞击了墙壁或球拍。 撞墙或球拍会使球反弹。 可以通过翻转球的速度向量来实现。
总结
我希望在读完这片文章后,您更了解触摸事件了。并且您可以使用多点触控功能编写自己的应用程序。