Mobile

Creating Fast Buttons for Mobile Web Applications

Ryan Fioravanti
January 2011


Background

Here at Google, we’re constantly pushing the boundaries of what’s possible in a mobile web application. Technologies like HTML5 allow us to blur the distinction between web apps and native apps on mobile devices. As part of that effort we’ve developed a technique for creating buttons that are far more responsive than plain HTML buttons. Previously for a button, or any clickable element, we might have just set up a click handler. For example:

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

The problem with this approach is that mobile browsers will wait approximately 300ms from the time that you tap the button to fire the click event. The reason for this is that the browser is waiting to see if you are actually performing a double tap. For most buttons we are developing we know that there is no double click behavior that we want to handle, so waiting this long to start acting on the click is time wasted for users. We first developed this technique for the Google Voice mobile web app since we wanted users to be able to dial phone numbers rapidly.

Handling Touch Events

The technique involves a bit of JavaScript that allows the button to respond to touchend events rather than click events. Touchend events are fired with no delay so this is significantly faster than click events, however there are a few problems to consider:

  1. If the user tapped somewhere else on the screen and then invokes a touchend on the button then we should not fire a click.
  2. If the user touches down on the button and then drags the screen a bit and then invokes a touchend on the button then we should not fire a click.
  3. We want to highlight the button when the user touches down to give it a pressed state.

We can solve the first two problems by monitoring touchstart and touchmove events as well. We should only consider a touchend on the button if there was previously a touchstart on the button. Also if there exists a touchmove anywhere that goes past a certain threshold from the touchstart then we should not handle the touchend as a click.

We can solve the third problem by adding an onclick handler to the button as well. Doing so will allow the browser to properly treat it as a button, and our touchend handler will ensure that the button is still fast. Also, the presence of an onclick handler serves as a good fallback for browsers that don’t support touch events.

Busting Ghost Clicks

Adding the onclick handler back to the button introduces one last ugly problem. When you tap the button, a click event will still be fired 300ms later. Now we run the risk of the click handler executing twice. This can be easily resolved by calling preventDefault on the touchstart event. Calling preventDefault on touchstart events will stop clicks and scrolling from occurring as a result of the current tap. We wanted users to be able to scroll even if they started their scroll on a button, so we didn’t consider this an acceptable solution. What we came up with to deal with these ghost clicks was called a click buster. What we do is simply add a click event listener to the body, listening on the capture phase. When our listener is invoked, we try to determine if the click was as a result of a tap that we already handled, and if so we call preventDefault and stopPropagation on it.

Fast Button Code

Here we will offer some code that implements the ideas we have just discussed.

Construct the FastButton with a reference to the element and click handler.

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;
  }
};

Save a reference to the touchstart coordinate and start listening to touchmove and touchend events. Calling stopPropagation guarantees that other behaviors don’t get a chance to handle the same click event.

google.ui.FastButton.prototype.onTouchStart = function(event) {
  event.stopPropagation();

  this.element.addEventListener('touchend', this, false);
  document.body.addEventListener('touchmove', this, false);

  this.startX = event.touches[0].clientX;
  this.startY = event.touches[0].clientY;
};

When a touchmove event is invoked, check if the user has dragged past the threshold of 10px.

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();
  }
};

Invoke the actual click handler and prevent ghost clicks if this was a touchend event.

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);
};

Call preventGhostClick to bust all click events that happen within 25px of the provided x, y coordinates in the next 2.5s.

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);
};

If we catch a click event inside the given radius and time threshold then we call stopPropagation and preventDefault. Calling preventDefault will stop links from being activated.

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 = [];

Conclusion

At this point you should be able to easily create fast buttons. With a bit of fancy styling you can make them look and feel like native buttons according to the platform you are developing for. There are already a few mobile JavaScript libraries available that solve this same problem, but we haven’t yet seen any that provide the fallback for click events or a solution to the ghost clicks problem. We hope that browser developers will solve this problem in future releases by firing click events immediately when zooming is disabled on the website (using the viewport meta tag). In fact, this is already the case with the Gingerbread Android Browser.

Authentifizierung erforderlich

Dazu müssen Sie in Google+ angemeldet sein.

Anmeldung...

Google Developers braucht hierfür Ihre Erlaubnis.