Search code examples
ipadscrollmobile-safaritouchmove

How to synchronize the scroll offset of two elements during inertial scrolling


I need to keep the scroll offset of an element in sync with another (the window actually) and I'm having trouble doing so during the inertial "roll off" phase of scrolling on Mobile Safari (iPad).

I have a couple of divs with position:fixed; overflow:hidden and I need to keep their scroll offset in sync with the window's one (meaning the entire body scroll.) Usually I'd code it like this (jQuery):

var $win = $(window),
    $div1 = $(...)

$win.scroll(function() { 
    $div1.scrollTop($win.scrollTop())
})

But testing the interface on an iPad, I noticed that the div was not being updated neither during the touch phase, when you are dragging the virtual page with your finger, nor during the inertial phase, when you let go and the page slows down to a stop.

I solved it for the dragging phase by registering the handler for the touchmove event as well as the scroll one.

But I can't find a way to solve the problem for the inertial phase. The div stays still (and goes slowly out of sync with the rest of the page) until the inertial movement comes to a full stop, when the scroll event is finally fired and it skips into position.

Here's a working demo.

Try to scroll it on an iPad to see the "inertial scolling" problem. Unfortunately I couldn't get it to work on jsFiddle, due to the iPad's weird behaviour with iframe scrolling.

If I could just run a polling during that phase, I could keep a semblance of synchronization between the two elements. I've tried with setTimeout, setInterval, and requestAnimationFrame, but neither of them fires during the inertial scrolling phase. It seems like all Javascript stops during that phase.

Questions:

  • Is there any touch or scroll event fired during the inertial scrolling phase?
  • Is there any way to run a Javascript callback during that phase?
  • Is there a way to sync the scroll offset of two elements (either X or Y, not both) using CSS or some other technology other than JS?

Solution

  • OldDrunkenSailor beat me to suggesting iScroll.

    Unfortunately, out of the box iScroll just replicates the same problem as native inertial scrolling -- there's no event handling during the inertial phase.

    Here's a version of your demo with a monkey-patched iScroll to add a custom event that fires even during the inertial stage: https://dl.dropbox.com/u/15943645/scrollingdemo.html

    Works great on my 2nd gen iPad.

    JS:

    // Disable touch events
    document.addEventListener('touchmove', function (e) { e.preventDefault(); }, false);
    
    // Patch iScroll for position change custom event
    iScroll.prototype._oldPos = iScroll.prototype._pos;
    iScroll.prototype._pos = function(x, y) {
        this._oldPos(x, y);
        if (this.options.onPositionChange) this.options.onPositionChange.call(this);
    }
    
    $(function() {
        var $win = $(window),
            $div_cols = $('#cols'),
            $div_rows = $('#rows'),
            $div_body = $('#body')
    
        // attach scrolling sync handler and execute it once
        function sync_scroll(e) {
            $div_cols.scrollLeft(0 - $div_body.position().left);
            $div_rows.scrollTop(0 - $div_body.position().top);
    
        }           
    
        // initialize iScroll on wrapper div, with position change handler
        var myScroll = new iScroll('iscroll_wrapper', {
            bounce: false,
            onPositionChange: sync_scroll
        });
    })
    

    CSS:

    #iscroll_wrapper {
        position:absolute;
        z-index: 1;
        left: 168px; 
        top:77px; 
        bottom:0px; 
        right:0;
        overflow:auto;
    }
    
    #body {
        position:absolute;
        z-index: 1;
        width: 2046px; 
        height: 3376px;
    
    }
    

    Note only the body responds to touch events, but you can extend the technique to the rows and cols divs for the reverse relationship.