1 触控和手势事件
1.1 HTML5规范
随着电容屏幕的流行,触控操作变得前所未有的流行起来。时至今日,带有多点触控功能已经成为了移动设备的标准配置,基于触控的手势识别技术也获得巨大的发展,如使用两个手指来缩放应用的大小等。所以,在移动系统中,编程需要考虑的不是鼠标事件,而是触控和手势事件,这些事件对于改善用户体验起了非常大的作用。最早将触控和手势事件引入Web领域的是苹果公司,它在iOS2.0中加入了这种支持,随后Android系统也加入了这一阵营。
在介绍规范之前,有必要先理解一下触控、手势事件与浏览器默认行为的关系。图13-1描述了处理触控事件的可能情况,图中灰色圆圈表示的是一个触控点,当它向上移动的时候,浏览器已面临艰难选择,对于用户触发的触控事件,可能有两个地方需要使用到触控事件:第一是浏览器本身,浏览器可能希望利用这个事件完成翻页动作;另外一方面,该灰色圆圈的部分所对应的元素可能需要由自己来处理这些触控事件,而不是浏览器来处理。浏览器或者WebKit的具体处理逻辑我们在稍后会介绍到。
图13-1 浏览器处理的触控事件
目前,Web领域引入两种与触控相关的技术,其一是HTML5 Touch Events,它基本上已经成为了规范,得到了众多渲染引擎和浏览器的支持和认可。其二是Gesture Events,它是苹果公司设计并在Safari浏览器中实现的,但是没有得到其他更多浏览器的支持。下面分别来分析这两者。
首先是HTML5 Touch Events,它已经成为推荐的规范,而且事实上也得到了两家主流移动操作系统中浏览器的支持,可以说发展得非常好,该标准主要是定义如何将原始的触控事件以特定的方式传递给JavaScript引擎,然后再传递给注册的事件响应函数。这一规范在HTML5网页应用中已经比较成熟,网页开发者可以根据规范进行定义,其中最主要的接口是TouchEvent,定义在图13-2中上半部分,表示一次传递给JavaScript注册函数的事件。
图13-2 HTML5 Touch Events定义的TouchEvent接口
根据标准中的定义,TouchEvent分成4种类型:touchstart、touchmove、touchend和touchcancel。熟悉触控事件的读者可能很容易理解,它们分别表示触控点开始接触屏幕、触控点移动、触控点离开屏幕和触控点取消。最后一个类型理解起来比较困难,有时浏览器取消该触控点,可能因为其他一些原因,如它可能进入了其他的窗口等。TouchEvent当然还是继承自DOM的UIEvent,这表明它有同其他事件类似的处理方式,不同点在于这个事件有一些不同的属性。下面逐一来分析它们。
- “touches” :表示当前屏幕中包括的所有触控点,“touches”是一个列表,如果触控点大于1,表示这是一个支持多点触控的设备。
- “targetTouches” :表示的是当前所有起始于当前DOM元素的触控点,也就是如果一个触控点的“touchstart”事件发生的位置在该元素的区域内,那就会被包含在该列表中。
- “changedTouches” :表示发生变化的触控点。如果类型是“touchstart”,那就包含新的触控点。如果是“touchmove”,那就包含发生移动的触控点。而“touchend”就是指触控点移出了屏幕。
每个触控点都需要包含很多信息,也就是图13-2中的众多属性,主要是标记属性的唯一ID、触控的目标(也就是对于的DOM元素)、屏幕位置、视图中的位置等,看起来还是比较直观的。有了这些接口,JavaScript代码能够非常清楚地知道每个触控点的信息,就能够像本地代码一样使用它们来满足各种应用的需求。
使用的方法并不复杂,示例代码13-1展示了如何注册监听事件的处理函数,这同其他的DOM事件区别并不是特别大,而且也只能注册在特定的元素(称为Clickable Element)上,如“div”等。因为TouchEvent有四种类型,示例代码定义了其中三种类型触控事件的处理函数。以“touchstart”为例,它会接受一个事件,就是之前定义的TouchEvent接口,为了避免同浏览器行为的冲突,可以在最开始调用“preventDefault”,这在第5章也做过介绍。后面可以根据事件来做出相应的动作。
示例代码13-1 使用HTML5 Touch Events的JavaScript代码
var targetElement = document.getElementById("aTouchableElement");targetElement.addEventListener("touchstart", onTouchStartEvent, false);targetElement.addEventListener("touchmove", onTouchMoveEvent, false);targetElement.addEventListener("touchend", onTouchEndEvent,false);function onTouchStartEvent(event) {// 处理事件event.preventDefault();event.touches;event.targetTouches;event.changedTouches;}…
有了这些原始的触控事件,Web开发者可以在网页中使用JavaScript代码来识别这些原始触控事件并生成手势事件,如Long Press、Pinch、Swipe、Fling等手势事件。目前有很多库提供这样的实现,如jQuery Mobile、Sencha Touch等,这极大地方便了Web开发者。
除了原始的触控事件,苹果公司开发的Safari浏览器还支持向JavaScript代码提供Gesture Events,其含义是由浏览器来识别原始事件并将手势事件传递给JavaScript代码,当然它定义了一个新的GestureEvent接口,事件类型也分为gesturestart、gesturechange和gestureend。这里的手势事件并没有与上面定义的Pinch等采用同样的方式,而是将旋转角度和缩放大小数据传递给JavaScript,这更像是支持两个手指的触控事件。由于它的局限性和不够通用,所以并没有得到像原始触控事件一样比较广泛的支持,这里也不做过多的介绍。
1.2 工作原理
WebKit和Chromium是如何支持触控事件的呢?其实这是比较复杂的过程,特别是某些处理方式跟鼠标事件其实还是有不一样的地方。首先事件的派送机制依然是使用第5章介绍的捕获和冒泡机制,具体参看图5-18的过程。
图13-3描述WebKit处理触控事件所使用到的一些主要类和它们之间的关系。最下层的WebWidget和WebView是WebKit的Chromium移植提供的接口,同之前介绍的一样,它们也是被Chromium项目的代码所调用,当Chromium接收到事件之后会将其传给WebViewImpl这个非常重要的类来处理。这个类大家应该很熟悉了,因为已经见过很多次面了。因为事件有多种类型,WebViewImpl类借助于PageWidgetDelegate类来处理和区分这些输入事件。经过PageWidgetDelegate类处理后的事件会调用WebViewImpl类各个事件处理接口,而WebViewImpl类的这些接口基本上使用主框(Frame)的事件处理句柄EventHandler对象来处理事件。细心的读者可以发现,图中的EventHandler包含两个函数,第一个是处理原始触控事件的函数,第二则是处理手势事件的函数。为什么会这样呢?
图13-3 WebKit处理触控事件的基础设施
WebKit除了接收原始的触控事件之外,还需要它的移植或者说是浏览器提供手势事件,这些事件会触发WebKit的默认动作。例如“LongPress”事件,它表示手指在屏幕上长按一段时间,这需要浏览器将其识别成手势事件然后传递给WebKit,当WebKit接收到这个事件之后,触发自己的默认动作。这个事件同前面介绍的Safari提供的Gesture Events不是一回事,因为Safari只是提供了旋转和缩放的值给JavaScript,而这里的手势事件包括一个或者多个手指触发的动作,WebKit并不会将这里的手势事件传递给JavaScript代码,如“longPress”事件会触发浏览器弹出右键菜单的动作。
对于多框结构的网页,事件首先由WebKit交给主框处理,WebKit会检查该事件是否需要由子框处理,如果是的话,WebKit会将该事件派发给子Frame,依此类推。这是一个递归过程,请读者结合第三章介绍的框结构来理解该过程。
下面来分析一下在Chromium中浏览器是如何处理从系统传递过来的触控事件,并将它们转换成之后的手势事件的。这一过程稍显复杂,让我们来解释一下原因。当触控事件发生后,Chromium首先需要将触控事件保存,然后使用众多的手势识别器(Gesture Recognizers)来将其识别成手势事件。此时,如下面所描述的问题来了。
- Chromium是否需要将所有的原始触控事件传递给网页呢?答案是否定的。如一些网页并没有注册监听函数来处理它们,那么就会造成极大的浪费,因为这些事件的传递和处理是个稍长的过程。更为致命的是,一个简单的用户操作通常有非常多的事件,这会极大地浪费CPU等资源。
- 为什么需要Gesture Event传递给WebKit呢?因为是由浏览器识别并将识别出结果的事件传递给WebKit,这客观上能够有效减少很多事件的传递。
- 除了发送TouchEvent和GestureEvent之外,也可能会发送MouseEvent,这是为什么?原因很简单,因为目前还存在一些网页,它们需要监听鼠标相关的事件以完成特定的动作,如果Chromium不模拟这些事件,那么网页显然不能正常的工作。但是某些鼠标事件可以模拟,如MouseDown其实对应于TouchStart,MouseUp对应用TouchEnd等。但是MouseOver就比较麻烦,比如一些网页需要根据当前鼠标悬浮事件来显示一个菜单,这对于触控设备来说,的确是一个问题。
对于Chromium来说,事件的处理还是相当复杂的,因为需要三种类型的事件并将其传递给WebKit。由于触控事件最初是应用在移动设备上的,所以这里也主要以Chromium的Android版为例来介绍,而Chromium的桌面版对触控事件的支持目前还不是特别完善。
在Chromium的Android版中,所有的事件都是由Android系统传送过来的,这也意味着事件的处理首先是在Java层,当然是在Browser进程的主线程中,如图13-4所示为层次结构图和相关层次中的基础设施。Java层主要包含两个类。
图13-4 Chromium处理触控事件的基础设施
- ContentViewGestureHandler :它主要有几个任务。首先,它需要通过相应的设施来决定是否需要原始的触控事件,这其实依赖于WebKit,在每个“touchstart”事件开始的时候,需要进行HitTest检查,该动作检查当前触控点所对应的元素,然后检查该元素是否注册了监听事件的函数,如果是,需要将原始事件传送给WebKit。其次是各种手势事件的识别器,它们能够对WebKit所需要的各种手势进行识别并传递给WebKit,最后根据需要(如果有鼠标事件的监听函数)模拟鼠标事件。
- ContentViewCore类 :主要负责将C++中的功能桥接到Java层中,并将Java中处理好的事件等信息桥接到C++代码中,它对应C++中的类是ContentViewCoreImpl。
两个类主要负责Java层的事件处理和传递。
在Java层之下是著名的RenderWidgetHostView类,它表示一个网页的视图。虽然这是Browser进程中的代理类,表示的是Renderer进程中相应的网页视图,它被ContentViewCoreImpl用来将事件传递给Renderer进程。后面大家应该比较清楚了,Chromium通过IPC机制来完成传递,Browser进程中的基础类是ImmediateInputRouter,而Renderer进程中的基础类是RenderWidget类。
在Renderer进程中,RenderWidget在Chromium中表示网页的结构,它拥有前面WebKit定义的接口类WebWidget,这样,完整的过程就被这些类串联起来了,如图13-4所示。
1.3 启示和实践
示例代码13-1其实是一个非常典型的用法,只是对于鼠标、触控等类型的事件处理过程可能需要复杂一些的步骤。
网页除了可能需要自身处理触控事件以外,还有一个比较特别的问题,那就是对于一个为移动设备定制的网页,它可能不需要使用缩放网页(使用Pinch手势的浏览器默认行为来放大或者缩放网页)或者不需要翻滚网页(Fling手势的浏览器默认行为是滚动网页),因开发者已经考虑并设计出了适合移动设备网页阅读的网页了。那有没有办法帮助开发者和浏览器合力规避浏览器默认行为呢?
根据上面的描述,相信读者已经看出一些端倪了,那就是网页开发者可以注册事件的响应函数,并调用“preventDefault”函数来阻碍浏览器执行默认行为。问题是这一方法只是针对某个元素而已,而不是整个网页,只是当手指触控到该元素的时候才禁止默认行为。解决这一问题的方法很简单,那就是可以将函数注册到区域更大的元素,如示例代码13-2所示的使用“body”元素就可以解决这个问题。
示例代码13-2 使用触控事件的响应函数来禁止网页的方法和滚动的代码
function handleEvent(event) {event.preventDefault();…}document.body.addEventListener('touchstart', handleEvent, false);document.body.addEventListener('touchmove', handleEvent, false);document.body.addEventListener('touchend', handleEvent, false);
这个方法看起来还是需要一些代码,虽然只是短短的不到十行代码,但是除此以外还有一个更好更简单的办法,那就是使用“meta”标签。