移动web开发技能之touch事件详解

手机APP/开发
341
0
0
2023-02-03
目录
  • 概述
  • touch事件
  • touch事件分类
  • touch事件对象
  • 移动web单击事件
  • iOS单击延迟
  • “单击穿透”问题

概述

单击事件是任何一个前端页面中最常用的交互行为之一,在传统的PC端大部分是使用click事件来实现用户单击交互的程序逻辑,而在移动Web端新增了touch事件来实现移动端更加敏感和复杂的触摸交互行为。本章将就移动端touch事件的使用以及它与PC端的click事件的区别进行深入探讨。

touch事件

在传统的PC端,用户的单击操作主要是由鼠标的左键或者右键来产生,它主要是指鼠标的按钮被按下,并且在很短的时间内(一般小于300ms)又被释放开,这就被称为单击操作(或称为一次点击操作)。

而对于移动Web端,同样也是如此,当手指触摸到屏幕时开始计算时间,并且在300ms内离开屏幕,这段时间手指不能移动,这就算是移动Web端的单击事件,手指触摸就被称为touch。

touch事件分类

移动Web端的touch触摸事件主要由屏幕和触摸点组成,其中屏幕可以是手机、平板或者触摸板,而触摸点可以通过手指、胳膊肘或触摸笔,甚至耳朵、鼻子都行,但一般是通过手指。根据touch触摸的类型可分为以下4种事件:

  • touchstart:当手指与屏幕接触时触发。
  • touchmove:当手指在屏幕上滑动时连续地触发。
  • touchend:当手指从屏幕上离开时触发。
  • touchcancel:当touch事件被迫终止,例如电话接入或者弹出信息时会触发,或者当触摸点太多,超过了支持的上限(自动取消早先的触摸点)时触发,一般不常用。

相比PC端,以上4种事件将用户的touch行为划分得更细,并且通过这些细化的事件可以实现移动Web端独有的用户交互行为,例如拖动swipe、长按longtap、双指缩放pinch,等等。

其中的touchstart、touchmove和touchend是最常用的3个事件,其中touchstart最先触发,touchend结束时触发,而touchmove是否触发取决于手指是否在触摸屏上移动。下面用代码来感受一下这3种事件的触发顺序,如下

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
  <title>touch事件</title>
  <script type="text/javascript">
    document.addEventListener("touchstart",function () {
        console.log("开始触摸");
    });
    document.addEventListener("touchmove",function () {
        console.log("移动手指");
    });
    document.addEventListener("touchend",function () {
        console.log("结束触摸")
    });
  </script>
</head>
<body>
</body>
</html>

在浏览器中运行这段代码,同时注意要启用Chrome中DevTools工具中的Device Mode功能,并使用鼠标模拟手指在屏幕上触发触摸事件,随后就会在Console控制台看到打印出对应的日志,从中可以看到一个简单的触摸操作是如何完成的。

touch事件对象

对于touch事件,每一次触发都可以得到一个事件对象,在JavaScript中这个对象叫作TouchEvent,利用TouchEvent可以获取touch事件触发时的坐标、元素以及到底有几个手指触发等,下面就来了解一下TouchEvent事件对象。

可以在Console控制台打印出当前触发touch时的TouchEvent对象,代码如下所示:

document.addEventListener("touchstart",function (e) {
    console.log(e);
});

打印的内容如图:

在上面的TouchEvent的属性中,经常使用的就是touches、targetTouches和changedTouches,它们的含义分别是:

  • touches:当前页面(屏幕)上所有的触摸点。
  • targetTouches:当前绑定事件的元素上的触摸点。
  • changedTouches:当前屏幕上刚刚接触的手指或者离开的手指的触摸点。

这3个属性返回的是TouchList对象,代表的是一个touch的集合数组,也就是说每一次touch触发,都会兼顾到多指触摸的场景,下面就分别以单指触摸的场景和多指触摸的场景来讲解这3个属性的区别。

首先是单指触摸的场景,我们来模拟用户一个手指触摸,如图。

外层的线框代表页面,里面的一个<div>元素绑定了touch事件,1号手指触摸了该<div>元素,这时touches、targetTouches以及changedTouches里面的触摸点都是指1号手指这个触摸点,应该很好理解。 对于多指触摸的场景,条件是手指触摸屏幕之后暂不离开,如图。

外层的线框代表页面,里面的一个<div>元素绑定了touch事件,首先1号手指第一个触摸了该<div>元素,然后2号手指第二个也触摸了该<div>元素,最后3号手指第三个触摸了div外面的区域,这时touches涵盖的触摸点的集合数组包括1号、2号、3号手指,而targetTouches涵盖的触摸点的集合数组包括1号和2号手指,而changedTouches涵盖的触摸点的集合数组包括2号和3号手指。

当手指都离开屏幕之后,touches和targetTouches中将不会再有值,changedTouches还会有一个值,此值为最后一个离开屏幕的手指的接触点。这就是touches、targetTouches和changedTouches这3个属性对于单指触摸的场景和多指触摸的场景下的区别,总结如下:

单指触摸的场景:

  • touches:1号手指
  • targetTouches:1号手指
  • changedTouches:1号手指

多指触摸的场景:

  • touches:1,2,3号手指
  • targetTouches:1,2号手指
  • changedTouches:2,3号手指

对于单指触摸的场景来说,它们并无区别,主要区别在于多指触摸的场景,所以在使用时可以根据具体的程序逻辑来选择使用合适的属性。

对于涵盖触摸点的集合数组TouchList而言,里面每个元素都是一个touch对象,通过这个对象可以获取当前触摸的位置,如图。

其中,主要用到了offsetX/Y、pageX/Y和clientX/Y这3个属性,它们的区别和含义分别是:

  • offsetX/Y:触摸位置相当于事件源元素的位置坐标,以当前<div>元素盒子模型的内容区域的左上角为原点。
  • pageX/Y:触摸位置相当于整个页面内容区域的位置坐标,当页面过长时,包括滚动隐藏的部分内容,以页面完整内容区域的左上角为原点。
  • clientX/Y:触摸位置相当于浏览器视区(屏幕)区域的位置坐标,以相对于页面的可见部分内容区域的左上角为原点。

具体的位置和距离可以参考下图,外层表示页面的所有内容,中间框表示浏览器的视区,其中有一个<div>元素绑定了touch事件,黑点表示触摸点的位置。

移动web单击事件

在了解了touch事件之后,我们知道移动Web端的单击事件完全可以由touchstart、touchmove和touchend来组合实现,移动Web端同时也提供了原生的click事件,它和传统的PC端的click事件一样,在用户完成一次完整的手指单击屏幕之后触发。在移动Web端使用click绑定单击事件,代码如下:

document.addEventListener("click",function (e) {
    console.log(e);
});

一切看似都很顺利,在需要使用单击时就用click事件,在需要使用touch时(拖动,长按等)就使用touch对应的事件。但是,对于移动Web端而言,处于iOS系统或Android系统时,采用click实现单击事件却有着不同的表现。

iOS单击延迟

这要追溯至2007年初,苹果公司在发布首款iPhone前遇到了一个问题:当时的网站都是为大屏幕设备所设计的,于是提出了视区(Viewport)的概念,其中一项即是用户在浏览网页时,可以在页面的任何地方通过双击操作将页面放大(Double Tap to Zoom)。这个交互功能提升了用户浏览网页时的体验,于是Android和iOS的移动端浏览器纷纷支持了这个功能,但是对于双击这个操作而言,其实是包括了两次单击操作,当第一次单击完成后,系统需要有一段时间来监听是否有第二次单击,如果有则表明此次操作是一个双击操作,而这段时间间隔大概有300毫秒(ms)。

因此,哪怕是只想要单击这个事件,也都会经过双击放大这个判断逻辑,导致要等到300毫秒之后才能收到单击事件程序逻辑的反馈,这就是300毫秒的单击延迟问题。

对于Android系统的浏览器而言,可以通过给视区设置user-scalable=no来禁止用户进行缩放,随后就可以正常地使用原生的click事件而没有延迟;对于iOS系统而言,浏览器对user-scalable支持度存在Bug(漏洞),导致了无法通过简单的设置来达到正常使用原生click事件的目的。代码如下:

<meta name="viewport" content=" initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />

所以,在iOS移动端,如果想要实现真正的单击事件而没有300毫秒延迟问题,就不能采用原生的click事件,可以通过touch(touchstart、touchmove和touchend)事件来模拟一次单击操作。好在当前业界已有比较流行的方案,例如Zepto.js中的tap事件和FastClick.js库可用来解决这个问题,在这里主要介绍一下FastClick.js库。

FastClick.js是FT Labs团队结合touch事件专门为解决移动端浏览器的300毫秒单击延迟问题所开发的一个轻量级的库。正常情况下,在移动Web端,当用户单击屏幕时,会依次触发touchstart、touchmove(0 次或多次)、touchend、click(原生)这些事件。touchmove事件只有当手指在屏幕上移动时才会触发。Touchstart、touchmove或者touchend 事件的任意一个调用event.preventDefault()方法,都会直接阻止原生click事件的触发。

FastClick的实现原理是在检测到touchend事件触发时,把浏览器在300毫秒之后原生的click事件阻止掉,然后通过DOM自定义事件立即发出一个模拟的click事件,这样就消除了300毫秒的延迟,提供了一个快速响应的“单击”事件。如下代码演示了FastClick的使用。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
  <title>FastClick.js</title>
  <script type="text/javascript" src="./fastclick.js"></script>
</head>
<body>
  <button id="click">点我</button>
  <script type="text/javascript">
    // 页面加载完成后,使用FastClick,一般传递最外层的body元素即可
    document.addEventListener('DOMContentLoaded', function(){
        FastClick.attach(document.body);// 在实际的项目中,需判断在iOS移动端才需要此程序逻辑
    }, false);
    document.getElementById("click").addEventListener("click",function(){
        alert("单击触发! ");
    },false)
  </script>
</body>
</html>

需要注意的是,在不修改<meta>标签中的user-scalable属性的情况下,300毫秒单击延迟的问题只会出现在iOS系统的浏览器中,并且解决方案只需要针对iOS端,上文也提到了这个问题的产生是由于对user-scalable支持度存在Bug,之后苹果公司也意识到了这个问题的严重性,于是在iOS 9.3版本时,提供了一个基于新的内核WKWebView的浏览器,并将其应用在Safari浏览器上,由此解决了这个问题(存在300毫秒单击延迟问题的浏览器是UIWebView,这个内核已经不再维护了),并且后续使用iOS 9.3版本系统的浏览器在访问页面时,会默认使用WKWebView浏览器。

至此,移动Web端的300毫秒单击延迟问题得到了彻底的改善。

“单击穿透”问题

在移动Web端,有一个很常见的应用场景,单击一个按钮会出现一个蒙层,此蒙层是全屏遮盖,并且有最高层级,当单击蒙层时,蒙层消失。此场景和交互操作看似并没有什么问题,但是假如页面中有一个绑定了单击事件的<div>元素被蒙层遮盖,而单击蒙层关闭时的位置刚好和该<div>元素重合,那么蒙层关闭后会同时触发该<div>元素的单击事件,对于用户来说,这个操作并不是要单击该<div>元素,这就是所谓的“单击穿透”问题,如图。

出现“单击穿透”问题需要有个条件,即蒙层是通过绑定的touch事件来实现隐藏,而其遮盖的<div>元素绑定的是原生click事件,这样就形成了touch事件触发之后,蒙层隐藏了,300毫秒后当前这个触摸点的click事件又触发了,就形成“单击穿透”。

移动Web端的“单击穿透”问题出现的原因其实和300毫秒单击延迟问题脱不了关系,但是“单击穿透”出现的场景比较单一,并且也比较好解决。

解决“单击穿透”问题可以从问题出现的原因上来着手,主要有以下两种解决方案:

  • 不要同时混用touch事件和click事件,要么给蒙层和<div>元素同时绑定touch事件,要么同时绑定click事件,在iOS 9.3版本之后,只用click事件即可,此方案体验最好。
  • 延迟蒙层消失的时间,例如在touch事件触发后,在350毫秒后再让蒙层消失,这样后面的<div>元素就不会触发click事件了,此方案会导致蒙层消失的响应慢,体验差,并且有时会触发两次消失逻辑,故不推荐使用。

无论是300毫秒单击延迟问题,还是“单击穿透”问题,这些都是移动Web端特有的问题,也在一定程度上反映出移动Web端环境的复杂性,需要注意支持度和兼容性问题的地方很多,所以大家在进行移动Web端开发时,要有意识地去关注这些问题。