手势触控事件
撰写时间:2025-08-13
修订时间:2025-08-22
搭建测试环境
上面代码,创建了一个canvas,宽度为视口的80%,高度为固定值350px,边框厚度为0px,水平居中此canvas。应用devicePixelRatio以获取高清图像。在render函数中,先平移特定的偏移值后再绘制图像。最后,打印出HTMLCanvasElement的信息。
上面在设置canvas的clientWidth及clientHeight属性时,若使用内联的CSS来设置较容易引发一些小问题,因此上面使用JavaScript来设置值。目前各浏览器对这些相应的属性值的设置,逻辑较混乱。具体参见Canvas 2D 概述。
因下面各节需考查canvas在未设置或设置边框厚度的不同情况,因此initCanvas函数带有一个可初始化其边框厚度的参数,默认值为字符串数值10px
。
在canvas未设置边框厚度时,为显式将其与父元素在视觉上区分开来,故用一种黑色来填充其背景。
隐藏细节,聚焦关注点
通过本站的 Web 组件,将众多辅助代码隐藏起来,让其在后台自动运行,前端代码只保留可能会变化的代码,以聚焦新的关注点。
mousemove事件
本文虽阐述手势触控事件,但其与鼠标事件有一些相同部分,故本节先考查mousemove事件。
7种坐标值
在鼠标移动事件中,回调函数中的参数的类型为MouseEvent,通过各个属性值存储了7种坐标值。
在canvas上移动鼠标,观察各种值的变化。鼠标坐标值的类型都是整数。
offsetX, offsetY
对于一个基于canvas的应用来讲,offsetX及offsetY是最方便的一组坐标值,它们是相对于canvas左上角顶点的偏移值,因此本应无需进行减法运算即可直接返回鼠标在canvas内的坐标值。
但,对于像基于canvas的应用来讲,往往需要精确到像素级别,此时仍有一些小问题需要注意。
下面主要考查两个问题。一是当canvas未设置边框厚度时,鼠标位置的问题。二是当canvas设置了边框厚度时的问题。
问题一:鼠标位置的问题
Safari的问题:参数evt的offsetY可以返回的值最小值为-1,这属于不正常的情况;而offsetX可以返回的值最小值为0,这属于正常的情况。从逻辑上讲,当鼠标移出canvas的上边框时,此时已超出canvas的监听范围,不应再返回一个-1的值。
Chrome的问题:根据参数所绘制出来的正方形,比鼠标箭头标识图像向右向下各偏移了1个像素。Safari也偏移了一点点,但不如Chrome明显。因此,上面代码在绘制正方形时将参数位置往左往上分别偏移了1个像素,从而在Safari及Chrome中均取得一致的效果。
问题二:canvas边框厚度的问题
当canvas的边框厚度大于0值,将鼠标移到左边或上边的边框线范围时,对于Chrome,参数evt的offsetX及offsetY均返回一个负值;左上角边框线范围外往下往右一个像素才返回0值。这使得上面代码的运行结果符合我们的期待。
但在Safari中,参数evt的offsetX及offsetY并未考虑边框线厚度的情况。当鼠标移至边框线左上角,offsetX及offsetY的值均为0;而在画布有效渲染区域内(上面黑色区域),offsetX及offsetY的值均为10。这样,我们原来正常运行的代码,当canvas有特定厚度值时,所绘制正方形将自动往下往右偏移了这个厚度值,从而导致光标箭头提示图像与所绘制的正方形不再保持一致。
如何解决这种在不同浏览器中出现不同情况的问题?一种方法是查询用户所用浏览器的类型,然后再分别对待。但这种方法需对所有主流浏览器都要进行测试。因此这种方法有点笨拙。第二种方法是改用参数evt的clientX及clientY属性来计算出实际偏移值。具体细节,详见下节。
clientX, clientY
clientX及clientY是控件相对于视口 (viewport)的一组坐标值。视口是浏览器用以显示整个网页内容的区域的一部分。
浏览器由地址栏、收藏栏、标签页、边栏、滚动条、状态栏,以及网页内容渲染区等构成。网页内容渲染区的大小可通过以下代码查看:
因此,网页内容渲染区通常也称为浏览器客户端区域。当某个网页的内容较多时,屏幕只能看到一部分的内容。在屏幕上所看到内容的区域称为视口,因此视口中的内容只是浏览器客户端区域的一部分。当用户上下翻页时,浏览器客户端区域中的其他内容则相应地滚动到视口中,从而能被用户阅读。
mousemove事件中参数evt的clientX及clientY属性值返回鼠标相对于视口的偏移值。
下面的代码,当鼠标在浏览器整个视口移动时,将显示鼠标在视口中的偏移位置值。
clientX及clientY的数据类型为整数。
在不改变浏览器大小的情况下,视口区域的大小是固定的,则mousemove事件参数evt中的clientX及clientY的值是在视口区域内的偏移值,因此它们与网页是否翻页无关。
使用鼠标滚轮上下滑动网页,网页的内容将上下滚动,但clientX及clientY的值总是保持不变。在滑动时观察鼠标的位置,您会发现它在滑动过程中不会动,也即它相对于视口区域的位置不变,因此clientX及clientY的值也不会变。
上下翻页不会改变视口区域的大小及位置,但翻页会改变各个具体控件在视口区域中的相对位置。同样使用鼠标滚轮上下滑动网页,鼠标位置不会变,但您会发现鼠标所指向的网页内容总在不同的变化。
也即说,随着网页的上下翻动,每个具体控件在视口中的相对位置会随之而改变。
下面代码让canvas元素监听鼠标移动事件并查看clientX及clientY的值的情况。滚动网页并在canvas内移动鼠标,观察clientY值的变化。
现在,我们根据clientX及clientY值,通过计算求出鼠标在canvas中的相对位置,然后在canvas中相应位置绘制一个小圆圈。
在HTMLCanvasElement的prototype链中,可调用Element的getBoundingClientRect方法来获取该元素在视口中的方框。
- HTMLCanvasElement
- HTMLElement
- Element: getBoundingClientRect()
- Node
- EventTarget
- Object
- EventTarget
- Node
- Element: getBoundingClientRect()
- HTMLElement
可以看出,其返回值的各个属性值均为浮点数。
使用clientX及clientY分别减去此方框左上角对应轴向上的坐标值,四舍五入后,再减去canvas的边框厚度值,即可得到鼠标在canvas中的偏移值。
pageX, pageY
pageX及pageY返回鼠标相对于整个浏览器客户端区域的偏移值。
offsetParent属性值(属于prototype链中的HTMLElement)返回特定子元素最近的可协助其定位的父元素。offsetLeft及offsetTop返回子元素相对于offsetParent父元素的偏移值。
由于canvas的offsetParent父元素是body,因此,offsetLeft及offsetTop属性值就代表了canvas在浏览器客户端区域中的偏移值。使用pageX及pageY分别减去它们,就可以得出鼠标相对于canvas的偏移值。再减去canvas的边框厚度,最终得到canvas的有效渲染区域内的坐标值。
调试基于触控事件的应用
在桌面浏览器中调试
Chrome
打开开发者工具面板,点击该面板的左上角第2个图标显示/隐藏设备工具栏

可切换进入响应式设计模式 (responsive design mode)。
Chrome的优点是,在响应式设计模式下,可用鼠标来模拟手势触控。
Safari
按下⌃Control+⌘Command+R,进入响应式设计模式。在此模式下,也可通过iOS设备模拟器来访问。
Safari的不足是,不能使用鼠标来模拟手势触控(桌面版的Safari未实现TouchEvent对象);并且iOS设备模拟器不能直接查看终端输出。
在移动设备上调试
在有Wifi的环境下,可在移动设备上直接访问桌面服务器。
- 找出本地的ip地址
在 Mac OSX 系统的终端输入:
ifconfig | grep "inet " | grep -v 127.0.0.1 这将直接列出本地网址。设若为:
192.168.5.15。 - 在移动设备中访问本地服务器应用
现在,若要访问桌面服务器上的:
localhost/demos/touch-events.html 则在移动设备的浏览器地址栏输入:
192.168.5.15/demos/touch-events.html
若像笔者一样,习惯于使用 Safari 来开发、调试 Web 应用,这应该是最方便的真机调试方式了。
手势触控事件
手势触控事件种类
手势触控事件与鼠标事件很相似,共有4种,下面是鼠标事件与手势触控事件的对应关系表:
| 鼠标移动事件 | 手势触控事件 |
|---|---|
| mousedown | touchstart |
| mousemove | touchmove |
| mouseup | touchend |
| N/A | touchcancel |
touchstart:手指或触控笔接触到设备面板上时被触发。此时将产生1或多个触控点 (touch point)。
touchmove:触控点在设备面板上移动时被触发。
touchend:手指或触控笔抬离设备面板时被触发。
touchcancel:当手势触控事件被取消时被触发。例如,当手指或触控笔从浏览器渲染区域滑动至浏览器菜单栏等。此时,因没有手指或触控笔抬离设备面板的动作,不能触发touchend事件,故而引入touchcancel事件以区别对待。
touchstart事件
下面代码,需在Chrome的响应式设计模式下、或在移动设备上运行才能看到结果。
当按下手指时,将触发一个touchstart事件。
代码效果
在移动设备上,本例子的效果就像是按手印,使用1 - 4个手指同时按触屏幕,则canvas将如实地绘制出相应多个触控点。
如果按下2个手指时不是完全同步,将先后触发2个touchstart事件,每个事件中只有一个手指印。
在canvas中点按并滑动手指时,默认情况下,触控事件也会其它事件,例如,上下滚动等。因此在事件处理器代码中加入代码:
可避免这种副作用。
代码详析
TouchEvent
在touchstart事件监听处理器中,参数evt的类型是TouchEvent。在鼠标事件中,我们可以直接从该参数中获取鼠标位置。但在触控事件中,因可能有多个触控点且多个触控点的位置、触控、离开屏幕的时间可能均不一样,因此鼠标位置信息较为复杂。TouchEvent封装了这些信息。
每个触控点的信息都封装在一个Touch对象中。参数evt有3个属性可返回多个Touch对象集合。
touches:在屏幕上的一组触控点。
targetTouches:在触控事件监听目标上的一组触控点。
changedTouches:从targetTouches所记录的所有触控点中,再筛选出与不同触控事件相关的一组触控点。
- 在touchstart事件中,当前激活的一组触控点。
- 在touchmove事件中,当前移动的一组触控点。
- 在touchend事件中,抬离屏幕或被接触元素的一组触控点。
因为changedTouches将众多的Touch对象进行了精细的分类,因此在不同的触控事件监听器中,我们一般使用evt的changedTouches来获取相应的一组触控点。
Touch
Touch封装了每个触控点的信息。它有几个重要的属性。
identifier:用于标识每个触控点的标识数字。Chrome中使用从0开始的数值来标识;而Safari使用很大的数字诸如1677645881等来标识。无论如何,对于每个不同的触控点,它们的identifier值是唯一的。
touchType:触控点类型。Safari中有此属性,其值为字符串direct
;而Chrome无此属性。
3组鼠标坐标值属性。分别为clientX及clientY,pageX及pageY,screenX及screenY。上面已阐述如何根据clientX及clientY、或者根据pageX及pageY来获取鼠标相对于canvas的偏移值以在canvas内相应的位置绘制图形。一般情况下,可优先使用clientX及clientY属性值。
访问不同的触控点集合
同时按下2个手指,其中一个按在canvas内部,另一手指按在canvas外部,则可看到,evt的touches记录了所有2个触控点,而targetTouches只记录了在监听目标canvas上产生的1个触控点。
而changedTouches则从targetTouches中再进行进一步的筛选:在targetTouches所有1个触控点中,是哪些触控点导致了touchstart事件被触发?在这里是那个按在canvas上面的1个触控点。
再设想这样的场景:在canvas内部同时按下2个手指,然后,一个手指保持不动,仅移动一个手指。则touchstart事件将记录2个触控点,而touchmove事件仅记录移动的1个触控点。
代码结构
我们总追求高效的代码。canvas的边框宽度是相对固定的,加载网页完毕后一般不再改变,因此calcCanvasBorderWidth在处理触控事件之前调用一次就够了。但canvas在视口中的位置随着用户上下滚动网页而随时发生改变,因此getComputedOffset须在每一次的触控事件中调用。
touchmove事件
获取触控点信息
当只有1个手指压在canvas上并移动时,4个分支上的touch的数量均为1。
| touchstart | touchmove | |
|---|---|---|
| targetTouches | 1 | 1 |
| changedTouches | 1 | 1 |
当2个手指压在canvas上,但只移动1个手指,则touchmove事件中的changedTouches中的touch的数量为1,其余均为2。
| touchstart | touchmove | |
|---|---|---|
| targetTouches | 2 | 2 |
| changedTouches | 2 | 1 |
当2个手指压在canvas上并同时移动时2个手指时,4个分支上的touch的数量均为2。
| touchstart | touchmove | |
|---|---|---|
| targetTouches | 2 | 2 |
| changedTouches | 2 | 2 |
计算缩放因子
在本节,当使用两个手指进行缩放时,计算出其缩放因子。
总体思路是,在任一或多个触控点移动时,更新所移动的触控点的位置。然后计算两个触控点的距离,减去上一次两个触控点的距离,从而得出缩放因子。
我们需要跟踪两个触控点的位置,因此引入变量touchLocs。重点在于当按下、抬起时,总会产生新的触控点、消除相应触控点,触控点总处于一种频繁变换的状态。因此,当我们引入第三方变量来跟踪触控点时,须同时在touchstart, touchmove及touchend事件中均要更新第三方变量的状态。
而要判断当触控点在触控面板上移动时是否有且仅有两个触控点时,需在touchmove事件中根据evt的targetTouches来进行判断。
在连续的移动事件中,事件响应频率各浏览器均有所差异,但均会及时、迅速。因此前后两次事件响应的状态变化值均很小,故每次计算出的缩放因子值都较小,这符合我们的日常体验。
