移动设备网页中快速响应单击动作
下文中的内容主要是在iOS平台测试,参考资料也主要是Apple的关于移动平台Web页面开发的书籍。
现在主流的移动设备,例如智能手机、iPad、Android平板电脑等,都没有鼠标输入设备,只是通过手指触摸屏幕来实现用户交互。而传统的网页大都针对鼠标输入设备进行页面元素的事件绑定,例如:mouseover, mousemove, mousedown, mouseup, click。在移动设备上,要通过单指触摸来模拟鼠标事件。根据Apple的官方文档,单指点击的事件模拟过程如下图:
上图中,当单指触摸手指抬起之后,浏览器将判断你按下的元素是“可点击元素”还是“非可点击元素”。对于这两种类型的元素,浏览器将会进行不同的动作。这里出现了一个新的概念“可点击元素”。在移动Safari浏览器中,以下DOM元素会被识别为“可点击元素”:
- 超级链接
- 表单元素
- 带有区域映射的<img>标签
- 绑定了mousemove, mousedown, mouseup, click事件的元素,无论是直接使用onxxx=的方式绑定还是使用JavaScript后期绑定的
所以,通常来说,我们在页面中直接使用DOM元素的click事件就可以让页面在移动设备浏览器中也能够正常工作。但是,如果你仔细观察,你会发现页面中绑定了click事件的DOM元素,在手指点击之后,并不能够快速的响应click事件中的JavaScript代码。也就是说,从你的手指点击之后到click事件的代码执行,这之间存在一个大约300毫秒的延迟,这是因为浏览器尝试识别你是不是在进行Double Tap,即单指快速连击的动作。
如果你希望你的网页上的元素,通常是一个按钮,能够快速的响应用户的触摸动作,就像在原生应用中一样,你需要使用touch相关的事件:
- touchstart - 手指按下
- touchmove - 手指按下之后保持按下状态移动手指
- touchend - 手指按下之后抬起来,可能移动过手指,也可能没有移动
- touchcancel- 手指按下之后,抬起来之前,被系统通知打断了,例如来电、短信提醒、电量不足等
一般的页面中可以不关心touchcancel事件。但是如果你做的是针对移动设备的网页游戏,那么可能你需要关注一下touchcancel事件,避免游戏的逻辑和用户的实际操作出现不一致。
如果你为你的DOM元素绑定touchstart, touchmove, touchend事件,你会发现它对用户按下动作的响应就像原生应用一样,非常迅速,毫无延迟。
以一个按钮为例,通常我们会在touchstart事件中,改变按钮的底色,让用户知道自己点中了这个按钮,在touchend事件中,我们会真正执行实际的业务逻辑,例如通过Ajax获取后台数据等。
但是如果用户的实际意图是要滚动这个页面,只是不小心按在了这个按钮上,此时按钮的touchend依然会触发,你的代码逻辑依然会被执行,但是实际上这并不是用户预期的行为。在原生UI组件中,例如UIButton本身就带有TouchUpInside的事件,也就是说,如果你的手指按下和抬起的时候都处于这个元素的范围,才认为你是在点击这个按钮。
遗憾的是,在JavaScript并没有提供DOM元素的类似TouchUpInside事件,我们只能通过一点小小的处理来尽量去识别用户是否想点击这个按钮:
1. 在touchstart事件中,记录用户点击的位置,使用clientX和clientY坐标对儿
2. 在touchmove事件中,计算当前手指的位置和开始点击的位置的差值的绝对值是否超过了一个设定的值,如果超过了设定值,则认为用户不想按这个钮了,或者用户并不是真的想按这个钮。给这个DOM元素设置一个标志位data-moved="y"。data-开头的是自定义属性,这个是HTML5规范中建议的
3. 在touchend事件中,检测这个DOM元素的data-moved属性是否为y,如果为y则不去执行既定的业务逻辑,而只是恢复按钮的样式为未点击的状态
问题:为什么不在touchend事件中直接判断手指离开时的位置和手指按下时的位置的差值?因为touchend事件和touchcancel事件中,传入到事件绑定函数的参数touchEvent对象并不包含手指位置的信息。所以只能在touchmove中跟踪手指位置。
运行效果:
说明:第一个写有“Click”字样的DIV是为了让你观察click事件的延迟。
完整的代码如下,为了让页面出现滚动效果,特意加入了一些无意义的文字。你可以根据这个代码进行一下封装,以便于在真正的应用中使用它。此代码在iPhone 4S/iOS 6.1.3/Safari 测试通过。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<title>click vs touch</title>
<style type="text/css">
div {
font-size: 18px;
font-family: sans-serif;
line-height: 150%;
}
#c {
margin-top: 40px;
height: 60px;
line-height: 60px;
vertical-align: middle;
text-align: center;
background-color: lightgray;
-webkit-user-select:none;
}
#c.click {
background-color: yellow;
}
#t {
margin-top: 20px;
margin-bottom: 40px;
height: 60px;
line-height: 60px;
vertical-align: middle;
text-align: center;
background-color: lightgray;
-webkit-user-select:none;
}
#t.start {
background-color: yellow;
}
#t.move {
background-color: blue;
color: white;
}
</style>
<script type="text/javascript">
var MOVE_THRESHOLD = 10;
function pageOnLoad() {
var c = document.getElementById("c");
var t = document.getElementById("t");
c.addEventListener("click", function(e){
c.setAttribute("class", "click");
});
t.addEventListener("touchstart", function(e) {
t.setAttribute("class", "start");
t.textContent = "touchstart";
t.setAttribute("data-moved", "n");
t.setAttribute("data-startx", e.touches[0].clientX);
t.setAttribute("data-starty", e.touches[0].clientY);
});
t.addEventListener("touchmove", function(e){
t.setAttribute("class", "move");
var startx = parseInt(t.getAttribute("data-startx", 10));
var starty = parseInt(t.getAttribute("data-starty", 10));
var deltax = e.touches[0].clientX - startx;
var deltay = e.touches[0].clientY - starty;
if (Math.abs(deltax) > MOVE_THRESHOLD
|| Math.abs(deltay) > MOVE_THRESHOLD) {
t.setAttribute("data-moved", "y");
}
});
t.addEventListener("touchend", function(e){
t.setAttribute("class", "");
t.textContent = "touchend with moved = " + t.getAttribute("data-moved");
});
}
</script>
</head>
<body onload="pageOnLoad();">
<div>
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
</div>
<div id="c">Click</div>
<div id="t">Touch</div>
<div>
set eiusmod tempor incidunt et labore et dolore magna aliquam. Ut enim ad minim veniam, quis nostrud exerc. Irure dolor in reprehend incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse molestaie cillum.
Tia non ob ea soluad incom dereud facilis est er expedit distinct. Nam liber te conscient to factor tum poen legum odioque civiuda et tam. Neque pecun modut est neque nonor et imper ned libidig met, consectetur adipiscing elit, sed ut labore et dolore magna aliquam is nostrud exercitation ullam mmodo consequet.
At vver eos et accusam dignissum qui blandit est praesent. Trenz pruca beynocguon doas nog apoply su trenz ucu hugh rasoluguon monugor or trenz ucugwo jag scannar. Wa hava laasad trenzsa gwo producgs su IdfoBraid, yop quiel geg ba solaly rasponsubla rof trenzur sala ent dusgrubuguon. Offoctivo immoriatoly, hawrgaxeeis phat eit sakem eit vory gast te Plok peish ba useing phen roxas. Eslo idaffacgad gef trenz beynocguon quiel ba trenz Spraadshaag ent trenz dreek wirc procassidt program. Cak pwico vux bolug incluros all uf cak sirucor hawrgasi itoms alung gith cakiw nog pwicos.
Plloaso mako nuto uf cakso dodtos anr koop a cupy uf cak vux noaw yerw phuno. Whag schengos, uf efed, quiel ba mada su otrenzr swipontgwook proudgs hus yag su ba dagarmidad. Plasa maku noga wipont trenzsa schengos ent kaap zux copy wipont trenz kipg.
Nisl id, urna tellus vestibulum arcu, at et sit pharetra odio pede, vel libero mauris suscipit sit. Ligula dolor vel ipsum posuere consequat gravida, mauris at, in suscipit magna libero enim mauris a. Sed ut imperdiet ridiculus.
</div>
</body>
</html>