Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android 4 Chrome hit testing issue on touch events after CSS transform

I'm having issues with the combination of CSS transforms and touch event hit testing. This only reproduces for me in Chrome on Android 4 (stable and beta). iOS Safari, as well as Chrome desktop with touch emulation both appear to be working fine.

I'm almost positive this has to be a bug, so I think I'm mostly looking for workarounds here.

The issue is that hit testing for touch only seems to work for where the element was before the transform, not the final position. You can see an example on my jsfiddle (only on Android 4 Chrome):

jsfiddle: http://jsfiddle.net/LfaQq/ full screen: http://jsfiddle.net/LfaQq/embedded/result/

If you drag the blue box half way down the screen and release it will snap back to the top. Now, if you try dragging from the top half of the page again, no touch will register. The touch events aren't even fired on the element. However, if you attempt to touch the bottom of the element, it works fine. You can then try moving it up from the bottom, and observing that hit testing no longer works on the bottom, but works on the top.

This is how I'm handling the events:

function handleTouch(e) {

    console.log("handle touch")

    e.preventDefault();

    switch(e.type){
        case 'touchstart':
            console.log("touchstart");
            touchOriginY = e.targetTouches[0].screenY;
            break;
        case 'touchmove':
            console.log("touchmove");
            el.innerHTML = e.targetTouches[0].screenY;
            var p = e.targetTouches[0].screenY - touchOriginY;
            el.style[TRANSFORM] = 'translate3d(0,' + p + 'px' + ',0)';
            break;
        case 'touchcancel':
            console.log("touchcancel");
            // Fall through to touchend
        case 'touchend':
            //console.log("touchend");
            //el.style[TRANSITION] = '.4s ease-out';
            el.style[TRANSFORM] = 'translate3d(0,0,0)';
            break;
    }

}

el.addEventListener('touchstart', handleTouch);
el.addEventListener('touchend', handleTouch);
el.addEventListener('touchmove', handleTouch);
el.addEventListener(TRANSITION_END, function(e) {
    console.log("transition end")
    el.style[TRANSITION] = '';
});

I don't have any problems with the transforms in touchmove, as those aren't new touches to be detected anyways.

Any suggestions?

like image 830
Jason Farnsworth Avatar asked May 22 '13 23:05

Jason Farnsworth


1 Answers

This is an unusual bug in Chrome.

Essentially the hit targets for an element is recorded during a layout pass by the browser. Each time you set innerHTML, the browser will relayout and the last time this is done, is before the touchend event is fired. There are a couple of ways around it:

OPTION 1: You can set a touch handler on the body element and check the target of touch event to see if it is touching the red block. Tip of the cap to Paul Lewis for this approach.

http://jsfiddle.net/FtfR8/5/

var el = document.body;
var redblock = $('.splash-section');

function handleTouch(e) {

    console.log("handle touch")
    if(e.target != redblock) {
        return;
    }

    ....

OPTION 2: Set an empty touch callback on the document seems to fix the problem as well - according to some of the linked bug reports, this causes the hit testing to be done on the main thread which is a hit on performance but it properly calculates the hit targets.

http://jsfiddle.net/LfaQq/2/

document.body.addEventListener('touchstart', function(){});

OPTION 3: Set innerHTML after the transition has ended to force a relayout:

el.addEventListener(TRANSITION_END, function(e) {
    console.log("trans end - offsettop:" + el.offsetTop);
    el.style[TRANSITION] = '';
    el.innerHTML = 'Relayout like a boss!';
});

I've created a bug report here and Rick Byers has linked to a related bug with additional info: https://code.google.com/p/chromium/issues/detail?id=253456&thanks=253456&ts=1372075599

like image 68
Matt Gaunt Avatar answered Sep 25 '22 20:09

Matt Gaunt