Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?
like image 562
Tobia Avatar asked Mar 08 '13 22:03

Tobia


2 Answers

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.

like image 140
Nick Blanchard-Wright Avatar answered Sep 16 '22 16:09

Nick Blanchard-Wright


iOS actually freezes DOM manipulation and Javascript while inertial scrolling is happening. Here is a simple demo that I made to illustrate the difference between scrolling in a normal desktop environment vs the iPad: http://jsfiddle.net/notjoelshapiro/LUcR6/. This code only has this JS in it:

var num = 0;
function updateNum(){
    num++;
    $('#awesomeDiv').text(num);
}
$(window).scroll(updateNum);

On scroll it increments a number and displays it on the bottom of the page. You'll see that the scroll number at the bottom of the screen is only being incremented when the inertial scroll has stopped. If Javascript was acting in the background it wouldn't refresh until the scroll ended but the number should be incremented higher.

So to answer your questions specifically:

Is there any touch or scroll event fired during the inertial scrolling phase?

Negative, ghostrider.

Is there any way to run a Javascript callback during that phase?

See above.

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?

Not quite sure what you mean here, JS can do all of the math'ing that you need but it will have to be when inertial scroll has completed. To be honest though, this may be a symptom of tl;dr since I'm at work right now.

Have you looked at the iScroll library? It simulates intertial (or non-inertial) scrolling for touch and non-touch environments and gives you JS callbacks during/after "scroll" and "inertial scroll" and provides a lot of information that you can use to calculate where you are on the page.

like image 44
OldDrunkenSailor Avatar answered Sep 19 '22 16:09

OldDrunkenSailor