Wednesday, August 5, 2009

Detecting a swipe in WebKit

I was a bit surprised that there wasn't a built-in swipe event for iPhone web apps or an easy to find library to detect one, so I wrote one based on the Mobile Safari documentation.

You may need to tweak the vertical threshold (15) or horizontal threshold (50).


/**
* You can identify a swipe gesture as follows:
* 1. Begin gesture if you receive a touchstart event containing one target touch.
* 2. Abort gesture if, at any time, you receive an event with >1 touches.
* 3. Continue gesture if you receive a touchmove event mostly in the x-direction.
* 4. Abort gesture if you receive a touchmove event mostly the y-direction.
* 5. End gesture if you receive a touchend event.
*
* @author Dave Dunkin
* @copyright public domain
*/
function addSwipeListener(el, listener)
{
var startX;
var dx;
var direction;

function cancelTouch()
{
el.removeEventListener('touchmove', onTouchMove);
el.removeEventListener('touchend', onTouchEnd);
startX = null;
startY = null;
direction = null;
}

function onTouchMove(e)
{
if (e.touches.length > 1)
{
cancelTouch();
}
else
{
dx = e.touches[0].pageX - startX;
var dy = e.touches[0].pageY - startY;
if (direction == null)
{
direction = dx;
e.preventDefault();
}
else if ((direction < 0 && dx > 0) || (direction > 0 && dx < 0) || Math.abs(dy) > 15)
{
cancelTouch();
}
}
}

function onTouchEnd(e)
{
cancelTouch();
if (Math.abs(dx) > 50)
{
listener({ target: el, direction: dx > 0 ? 'right' : 'left' });
}
}

function onTouchStart(e)
{
if (e.touches.length == 1)
{
startX = e.touches[0].pageX;
startY = e.touches[0].pageY;
el.addEventListener('touchmove', onTouchMove, false);
el.addEventListener('touchend', onTouchEnd, false);
}
}

el.addEventListener('touchstart', onTouchStart, false);
}



Use it like this:

addSwipeListener(document.body, function(e) { alert(e.direction); });

8 comments:

A.V.Ebrahimi said...

Thanks for code.

The Moose said...

Hey there, so would this be detecting a swipe on a HTML site on iPhone? I've been trying to track down a way to make a gallery that works like google image results does....?

doniguan said...

Nice code, only problem I ran into is there's no easy way to remove the listener. I just rewrote the class a bit to move all the functions and objects outside the main function, so you can remove the listener if you need to.

-andy

Phil Gyford said...

Thanks for this, it's nice and simple but works!

One thing though. I have a site where I want the user to be able to swipe left/right to move between articles, but swiping up down should use the default scrolling action. (It's probably easier to see than describe.)

This seems to work fine on an iPad, but on my iPhone 3G the vertical scrolling ends up being very jerky. Something is interrupting the default action from happening, but I can't work out what to tweak to fix this. Any ideas?

MillCreekPhil said...

I had a similar issue as the other Phil, specific to iOS4 for some reason, but changing the e.preventDefault() in onTouchMove to e.stopPropagation() solved the issue for me.

Thanks for the code!

dnad said...

as doniguan said, you need to remove the listener.

if you want to do this add these two lines immediately after the "listener(...)" line in the OnTouchEnd function.

el.removeEventListener('touchstart', onTouchStart);
addSwipeListener(el, listener);

Zack said...

I think this is what I am looking for, but I'm not sure if I'm implementing it correctly, because I don't get any alert messages when I swipe. How exactly do I go about testing this?

thargenediad said...

Awesome code! Thank you!

To get this code work properly on subsequent "swipe" events, I had to add this line to cancelTouch()...


dx = null;


...otherwise Safari kept registering swipe events after my initial swipe, when all I was doing was tapping.

Also, if you make the above change, you also have to change onTouchEnd(e)...

function onTouchEnd(e) {
    var dir = dx > 0 ? "right" : "left";
    var distance = Math.abs(dx);
    cancelTouch();
    if (distance > horizontalThreshold) {
        listener({ target: el, direction: dir});
    }
}



Also, add horizontalThreshold and verticalThreshold in the variable-instantiation section:


var verticalThreshold = 15; // pixels?
var horizontalThreshold = 100; // pixels?