为移动Web应用创建快速响应按钮

英文原文出自Google Deveploers《Creating Fast Buttons for Mobile Web Applications》,由TracyYih翻译,并首发于EsoftMobile.com。如需转载,请注明译者及出处信息。

背景

在Google,我们不断地突破移动Web应用能够达到的效果,HTML5这类技术让原生应用和Web应用的界线开始变得模糊。为了这个目标,我们开发了一种新技术让纯HTML按钮能够有更快的响应。这之前,我们可能只是为按钮或者其他可以点击的元素增加点击处理,如:

1
<button onclick='signUp()'>Sign Up!</button>

使用这种方法存在一个问题就是,移动浏览器会在你点击按钮后300ms才触发事件,原因是浏览器需要区分你是否是要执行双击。但是对于大多数按钮,我们并不需要它处理双击事件,所以300ms的延时时间对于只是想执行点击事件的用户来说太长了。我们最早开发这种技术是在Google Voice mobile web app中,我们希望能够更加迅速的调用拨号事件。

处理触摸事件

该技术涉及一点JavaScript的东西来让按钮响应触摸(Touch)事件而不是点击(Click)事件。触摸事件响应不会有延时所以感受会比点击事件快很多,但是我们也需要考虑以下几个问题:

  1. 如果用户是点击屏幕上其他的元素而触发了按钮的触摸事件,这种情况我们不应该去执行按钮的事件。
  2. 如果用户按下按钮然后拖到屏幕其他位置而触发了触摸事件,我们也不应该执行按钮的事件。
  3. 我们希望按钮在按下的时间能够高亮来表示点击状态。

我们能够通过检测touchstart和touchmove事件来解决前两个问题,只需要考虑一个一开始在按钮上就是touchstart状态的触摸事件,而如果touchstart后存在了一个touchmove,那我们就不应该处理touchend事件。

我们可以通过同时给按钮添加onclick处理来解决第三个问题,这样浏览器就会把它当成按钮并在点击时出现高亮。我们touchend处理也能保证按钮仍然很快响应,同时添加onclick可以在不支持触摸事件的浏览器上做为比较可靠的备用方案。

破坏讨厌的点击

在添加触摸事件的同时添加onclick事件又会带来另一个讨厌的问题,当你点击按钮时,点击(click)事件仍然会在300ms后执行,我们可以通过在touchstart事件里调用preventDefault来解决。在touchstart中调用preventDefault方法会会导致点击和滑动失效,但我们又想用户能够滑动视图即使一开始是点在按钮上,所以我们仍不能接受这种解决方案。接来下我们想出了一个被我们叫做点击破坏者(Click buster)的方案,我们给body添加一个点击事件监听(listener),当监听事件被触发,我们会区分点击是否由我们已经处理的tap事件导致的,如果这样,我们才调用preventDefault和stopPropagation。

Fast Button Code

下面我们会提供一些我们实现这个想法的代码。

通过标签和事件创建一个FastButton

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
google.ui.FastButton = function(element, handler) {
this.element = element;
this.handler = handler;
element.addEventListener('touchstart', this, false);
element.addEventListener('click', this, false);
};
google.ui.FastButton.prototype.handleEvent = function(event) {
switch(event.type) {
case 'touchstart': this.onTouchStart(event); break;
case 'touchmove': this.onTouchMove(event); break;
case 'touchend': this.onClick(event); break;
case 'click': this.onClick(event); break;
}
};

保存touchstart坐标,并开始监听touchmove和touchend事件,调用stopPropagation来保证其他操作不会触发点击事件。

1
2
3
4
5
6
7
8
9
google.ui.FastButton.prototype.onTouchStart = function(event) {
event.stopPropagation();
this.element.addEventListener('touchend', this, false);
document.body.element.addEventListener('touchmove', this, false);
this.startX = event.touches[0].clientX;
this.startY = event.touches[0].clientY;
};

当touchmove事件触发时,检查用户是否拖动超过10px。

1
2
3
4
5
6
google.ui.FastButton.prototype.onTouchMove = function(event) {
if (Math.abs(event.touches[0].clientX - this.startX) > 10 ||
Math.abs(event.touches[0].clientY - this.startY) > 10) {
this.reset();
}
};

执行真正的点击事件并在touchend事件时避免讨厌的click。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
google.ui.FastButton.prototype.onClick = function(event) {
event.stopPropagation();
this.reset();
this.handler(event);
if (event.type == 'touchend') {
google.clickbuster.preventGhostClick(this.startX, this.startY);
}
};
google.ui.FastButton.prototype.reset = function() {
this.element.removeEventListener('touchend', this, false);
document.body.removeEventListener('touchmove', this, false);
}

调用preventGhostClick来破坏在接下来2.5s内x,移动距离在25px内的点击事件。

1
2
3
4
5
6
7
8
google.clickbuster.preventGhostClick = function(x, y) {
google.clickbuster.coordinates.push(x ,y);
window.setTimeout(google.clickbuster.pop, 2500);
};
google.clickbuster.pop = function() {
google.clickbuster.coordinates.splice(0, 2);
};

如果我们在给定的半径和时间内捕获到了点击事件,调用stopPropagation和preventDefault。

1
2
3
4
5
6
7
8
9
10
11
12
13
google.clickbuster.onClick = function(event) {
for (var i = 0; i < google.clickbuster.coordinates.length; i += 2) {
var x = google.clickbuster.coordinates[i];
var y = google.clickbuster.coordinates[i + 1];
if (Math.abs(event.clientX - x) < 25 && Math.abs(event.clientY - y) < 25) {
event.stopPropagation();
event.preventDefault();
}
}
};
document.addEventListener('click', google.clickbuster.onClick, true);
google.clickbuster.coordinates = [];

总结

到这里你应该能够创建快速响应的按钮了,花一点心思,你可以让它们看起来更像你所面向平台的原生按钮。现在已经有一些JavaScript库也提供了这种问题的解决方案,但是到目前为止我们没有发现有提供可靠备选方案和讨厌的点击问题。我们希望浏览器开发者能够在能够解决在不能缩放的页面上快速响应的问题,事实上我们已经在Gingerbread的浏览器上实现了。

测试代码FastButtonDemo已上传github。