Search code examples
javascriptmobilecanvasmobile-safaritouchstart

Touch Events "touchstart" returns incorrect position on Mobile Safari. Workarounds?


I am developing an HTML5 Canvas project and while adding multitouch support for mobile devices, I seem to have run into an issue on Mobile Safari in iOS 7.1.

The main way that users interact with my app is by clicking and dragging. On the desktop, I add the appropriate mouse event listeners to my canvas...

c.addEventListener('mousedown', mouseDown, false);
c.addEventListener('mousemove', mouseMoved, false);
c.addEventListener('mouseup', mouseUp, false);

... have these event handlers calculate mouse position and call out to a more generic function that just accepts the X and Y position of the event...

function mouseDown(evt) 
{
    var pos = getMousePos(evt);
    inputDown(pos);
}

function mouseUp(evt) 
{
    var pos = getMousePos(evt);
    inputUp(pos);
}

function mouseMoved(evt) 
{
    var pos = getMousePos(evt);
    inputMoved(pos);
}

function getMousePos(evt) 
{
    var rect = c.getBoundingClientRect();
    return { 'x': evt.clientX - rect.left, 'y': evt.clientY - rect.top }
}

... and everything works great. I designed this level of abstraction to allow for easy expansion to other input methods. So now I attempt to use the Touch API to expand this. First I add my handlers to the canvas right next to the mouse handlers...

c.addEventListener("touchstart", touchDown, false);
c.addEventListener("touchmove", touchMoved, false);
c.addEventListener("touchend", touchUp, false);
c.addEventListener("touchcancel", touchUp, false);

... then add my thin event handlers to abstract away the touch events ...

function touchDown(evt) 
{
    evt.preventDefault();

    // Ignore touches after the first.
    if (activeTouch != null)
        return;

    if (evt.changedTouches.length > 0)
    {
        activeTouch = evt.changedTouches[0].identifier;
        var pos = getTouchPos(evt);
        inputDown(pos);
    }
}

function touchUp(evt) 
{
    var pos = getTouchPos(evt);
    activeTouch = null;
    inputUp(pos);
}

function touchMoved(evt) 
{
    var pos = getTouchPos(evt);
    inputMoved(pos);
}

function getTouchPos(evt) 
{
    var canX = 0;
    var canY = 0;

    for( var i = 0; i < evt.touches.length; i++ )
    {
        if (evt.touches[i].identifier == activeTouch)
        {
            canX = evt.touches[i].pageX - c.offsetLeft;
            canY = evt.touches[i].pageY - c.offsetTop;
            break;
        }
    }

    return { 'x': canX, 'y': canY }
}

... and this is where I run into trouble! You can see things get a little hairy but inefficiencies aside, I think I am using everything correctly. The problem is that getTouchPos(evt) seems unable to get the correct X, Y position of a touch on touchstart. On my first touch, I get a position of 0,0. If I drag my finger, the touchmove fires multiple times and the X,Y coordinates reported seem fine. If I lift my finger, touchend fires and I also get a correct location. However, if I touch again, touchstart fires and rather than where I am touching, I get the X,Y coordinates of where my last touch ended!

I've gone in with a debugger and sure enough, the stale/uninitialized values are right there in the Touch objects I get in the event data. Am I doing anything obviously wrong? I don't understand why I seem to be getting bad coordinates from touchstart events. What can I do to work around this issue? Is there another source I can use to get the correct position? A subtle tweak to event order?

Here's a link to an online demo of the full code if you'd like to run some in-browser debug.

And thanks in advance for any help.


Solution

  • I'm not sure if it's obvious, but it was a fun issue to debug. So basically, the confusion comes down to this guy:

    function mouseMoved(evt) {
        var pos = getMousePos(evt);
        inputMoved(pos);
    }
    

    For your mouse events, everything works great, because you're bound to mousemove.

    c.addEventListener('mousemove', mouseMoved, false);
    

    Which calls

    function mouseMoved(evt) {
        var pos = getMousePos(evt);
        inputMoved(pos);
    }
    

    So, as you move your mouse around, your posX/posY values are updated continuously. (Hopefully you see the issue now :)

    For TouchEvents, when a user touches (but does not move), you only trigger touchstart and touchend. It isn't until a user drags, that touchmove gets called, which in turn calls the function touchMoved (that calls inputMoved - which updates your posX/posY values).

    The easiest way to address this issue on your page is just to call inputMoved in both touchstart and touchmove so your MassHaver instance has the latest state.