Fast Touch Event Handling: Eliminate Click Delay

Try the demo!

Fast Touch Event Handling: Eliminate Click Delay

Mobile WebKits attempt to synthesize touch events into into click events. These same browser engines also have a double-tap-to-zoom function. This brings up the issue of distinguishing between a tap and double tap. What is a WebKit to do?

The answer is pretty simple. WebKit adds a small delay of 300 milliseconds after a single tap to see if you will tap a second time. If no second tap happens within 300 milliseconds, the gesture is interpreted as a single tap which is subsequently translated into a “click” event.

Great!

You already know that you can disable pinch and tap zooming in your app, provided you have designed your app to have nice, large buttons and text. Unfortunately, even after you do this, WebKit maintains the 300 millisecond delay after a single tap. To make matters worse, the delay is only triggered on fast taps. If you touchstart, pause briefly, then touchend you will experience no delay. Most users will tap fast enough to experience the delay some, if not most, of the time. This small delay can create a perceived slowness in your app, and therefore, it must be eliminated with extreme prejudice.

Libraries like fastclick will eliminate this delay for you, and will also take care of some tricky edge cases, but basic fast tap handling is pretty easy. The example code below demonstrates fast tap handling with the jQuery special event API, but the process will be similar with most other JavaScript event-handling libraries.

$.event.special.tap = {
  setup: function() {
    var self = this,
      $self = $(self);

    // Bind touch start
    $self.on('touchstart', function(startEvent) {
      // Save the target element of the start event
      var target = startEvent.target;

      // When a touch starts, bind a touch end handler exactly once,
      $self.one('touchend', function(endEvent) {
        // When the touch end event fires, check if the target of the
        // touch end is the same as the target of the start, and if
        // so, fire a click.
        if (target == endEvent.target) {
          $.event.simulate('tap', self, endEvent);
        }
      });
    });
  }
};

This is the basis for any sort of fast tap handling. In some cases, this may even be good enough. However, there are many ways to make this more robust.

Time Threshold

If a user starts a touch, holds, and then releases after one second, is that a tap? Maybe not, here’s how you can add a configurable time threshold to abort a tap event if the time between touchstart and touchend is too long.

$.event.special.tap = {
  // Abort tap if touch lasts longer than half a second
  timeThreshold: 500,
  setup: function() {
    var self = this,
      $self = $(self);

    // Bind touch start
    $self.on('touchstart', function(startEvent) {
      // Save the target element of the start event
      var target = startEvent.target,
        timeout;

      function removeTapHandler() {
        clearTimeout(timeout);
        $self.off('touchend', tapHandler);
      };

      function tapHandler(endEvent) {
        removeTapHandler();

        // When the touch end event fires, check if the target of the
        // touch end is the same as the target of the start, and if
        // so, fire a click.
        if (target == endEvent.target) {
          $.event.simulate('tap', self, endEvent);
        }
      };

      // Remove the tap handler if the timeout expires
      timeout = setTimeout(removeTapHandler, $.event.special.tap.timeThreshold);

      // When a touch starts, bind a touch end handler
      $self.on('touchend', tapHandler);
    });
  }
};

Distance Threshold

If the user starts a touch, moves 200 pixels to the left, then releases, is that a tap? Maybe not. Here’s how you can add a configurable distance threshold to abort a tap event if the user moves his or her finger too far.

$.event.special.tap = {
  // Abort tap if touch moves further than 10 pixels in any direction
  distanceThreshold: 10,
  // Abort tap if touch lasts longer than half a second
  timeThreshold: 500,
  setup: function() {
    var self = this,
      $self = $(self);

    // Bind touch start
    $self.on('touchstart', function(startEvent) {
      // Save the target element of the start event
      var target = startEvent.target,
        touchStart = startEvent.originalEvent.touches[0],
        startX = touchStart.pageX,
        startY = touchStart.pageY,
        threshold = $.event.special.tap.distanceThreshold,
        timeout;

      function removeTapHandler() {
        clearTimeout(timeout);
        $self.off('touchmove', moveHandler).off('touchend', tapHandler);
      };

      function tapHandler(endEvent) {
        removeTapHandler();

        // When the touch end event fires, check if the target of the
        // touch end is the same as the target of the start, and if
        // so, fire a click.
        if (target == endEvent.target) {
          $.event.simulate('tap', self, endEvent);
        }
      };

      // Remove tap and move handlers if the touch moves too far
      function moveHandler(moveEvent) {
        var touchMove = moveEvent.originalEvent.touches[0],
          moveX = touchMove.pageX,
          moveY = touchMove.pageY;

        if (Math.abs(moveX - startX) > threshold ||
            Math.abs(moveY - startY) > threshold) {
          removeTapHandler();
        }
      };

      // Remove the tap and move handlers if the timeout expires
      timeout = setTimeout(removeTapHandler, $.event.special.tap.timeThreshold);

      // When a touch starts, bind a touch end and touch move handler
      $self.on('touchmove', moveHandler).on('touchend', tapHandler);
    });
  }
};

Other considerations

Reading the fastclick source is the best way to understand all the nuances of fast click handling on touch devices. Fastclick will take care of some quirks in older mobile browsers, different requirements for certain input elements, as well as eliminating “phantom” clicks.

comments powered by Disqus