Multi-touch events playground

March 2012

Here's a piece of my Javascript code that I use from time to time when I develop apps that deploy touch events and wonder what's going on behind the curtains.

The code tracks all the mouse and touch events and displays the data about them. It has helped me to see the flow of events and understand the internals of touch data. Maybe the code provides some assistance for you too in learning or debugging the touch events. Enjoy.

Demo

Sorry - The old demo is no longer embedded here!

Just view this blog post on your tablet or touch enabled device and track your fingers below.

Note that on iPad, you need to disable "Multitasking Gestures" in the General Settings because otherwise the system captures events with 4 and 5 fingers!

Finger count:

Clear the list of printed event names above by clicking on the box.

How many fingers?

How many fingers can be simultaneously tracked by a device? I get these results with my devices:

  • Retina iPad (iOS 5.1): 11 fingers can be detected. (My wife provided the extra fingers, no toes required ;)
  • iPod Touch (iOS 5.1): 5 fingers.
  • iPod Touch (iOS 2.2): 5 fingers too.
  • HTC Desire (Android 2.2): only 1 finger.

When the number of fingers exceed the maximum, a touchcancel event is received.

Source code

Below is the source code for this demo. The code should be pretty much self-documenting. For each touch event, the finger data in the array event.changedTouches[] is inspected. All the properties of an event object are enumerated and printed on screen.

The touch events are documented for example by Apple in Safari Web Guide.

Enable the gesture events if you want to see them too. I've disabled them in the demo since they flood the event name box.

// black div for touch events
var area = $("#area");

// mouse events
area.bind("mousedown", function(e) {
    dumpEvent(e);
    var id = 1;
    createDiv(id);
    moveBox(id, e);
})
.bind("mousemove", function(e) {
    dumpEvent(e);
    moveBox(1, e);
})
.bind("mouseup", function(e) {
    dumpEvent(e);
    delDiv(1);
})
.bind("click", function(e) {
    dumpEvent(e);
});

// touch events
area.bind("touchstart", function(e) {
    dumpEvent(e);

    forEachChangedFinger(e, function(e2, id) {
        createDiv(id);
        moveBox(id, e2);
    });
})
.bind("touchmove", function(e) {
    dumpEvent(e);
    e.preventDefault(); // prevent page scroll

    forEachChangedFinger(e, function(e2, id) {
        moveBox(id, e2);
    });
})
.bind("touchend", function(e) {
    dumpEvent(e);

    forEachChangedFinger(e, function(e2, id) {
        delDiv(id);
    });
})
.bind("touchcancel", function(e) {
    dumpEvent(e);
    $("#area").empty();
});

/* disabled because flood of events

// gesture events
area.bind("gesturestart", function(e) {
    dumpEvent(e);
});
area.bind("gesturechange", function(e) {
    dumpEvent(e);
});
area.bind("gestureend", function(e) {
    dumpEvent(e);
});
*/

// clear event name list
$("#evlist").on("click", function(){
    $(this).empty();
});

// extract each finger data from list, call callback
function forEachChangedFinger(e, cb) {
    e = e.originalEvent;

    // e.changedTouches is a list of finger events that were changed
    for (var i = 0; i < e.changedTouches.length; i++) {
        var finger = e.changedTouches[i];
        var id = finger.identifier;
        cb(finger, id);
    }
}

function createDiv(id) {
    var colors = ["red","blue", "green", "orange", "gray", "purple"];
    var area = $("#area");
    var count = area.find("div").length;
    var div = $("<div class='finger'>"+count+"</div>");
    area.append(div);
    div.css("background", colors[count%colors.length]);
    div.attr("id", id);

    $("#count").text(count+1);
}
function delDiv(id) {
    $("#"+id).remove();

    var count = $("#area").find("div").length;
    $("#count").text(count);
}
// move box on screen
function moveBox(id, e) {
    var div = $("#"+id);
    var off = $("#area").offset();
    // offset box a little so it can be seen under a finger!
    var x = e.pageX - off.left - 35;
    var y = e.pageY - off.top - 35;
    div.css({"left":x, "top":y});
}

// print properties of the event object
function dumpEvent(e) {
    // get hold of orig event in jQuery
    if (e.originalEvent) {
        e = e.originalEvent;
    }

    var txt = [];
    for (var p in e) {
        // ignore constants
        if (p == p.toUpperCase())
            continue;

        var val = e[p];

        // do not dump functions
        if($.isFunction(val))
            val = "func()";
        txt.push(p+" = "+val);
    }
    var s = txt.join("<br/>");
    $("#eprop").html(s);
    $("#ename").html(e.type);

    // print event name
    if (lastevename != e.type) {
        $("#evlist").append("<span>"+e.type+" </span>");
        lastevecount = 1;
    } else {
        lastevecount += 1;
        $("#evlist >span").last().replaceWith("<span>"+e.type+lastevecount+" </span>");
    }
    lastevename = e.type;
}
var lastevename = "";
var lastevecount = 1;
Back