7

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?
Linus Juhlin
  • 1,175
  • 10
  • 31
Tobia
  • 17,856
  • 6
  • 74
  • 93
  • Would it be practical to add the div elements as child elements to the scroll layer? – Four_lo Mar 18 '13 at 10:31
  • What do you mean by "scroll layer"? The element being scrolled is the window, that is the entire body. The fixed divs are inside the body, yes, but they have `position:fixed`. – Tobia Mar 18 '13 at 10:34
  • I am basically going of the information i get here http://developer.apple.com/library/ios/#documentation/WindowsViews/Conceptual/UIScrollView_pg/CreatingBasicScrollViews/CreatingBasicScrollViews.html#//apple_ref/doc/uid/TP40008179-CH101-SW1 – Four_lo Mar 18 '13 at 11:02
  • I have added a working demo that reproduces the problem. Please disregard the huge amount of HTML and CSS. You can use your browser's inspector if you wish to see the element structure and their CSS, but really all that's needed to understand the problem is expressed in my question. Thanks. – Tobia Mar 18 '13 at 12:46

3 Answers3

4

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.

OldDrunkenSailor
  • 338
  • 1
  • 5
  • 20
  • Thank you for your research. I had not thought of completely replacing native scrolling with a library, but it's a workaround that makes perfect sense. I hope it's ok that I awarded the bounty to Nick for his working demo. – Tobia Mar 19 '13 at 08:58
4

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.

  • I had not thought of completely replacing native scrolling with a library, but it's a workaround that makes perfect sense. Thank you both @OldDrunkenSailor and Nick. Bounty awarded to Nick for the working demo! – Tobia Mar 19 '13 at 08:52
0

Triggers when a scroll begins. Note that iOS devices freeze DOM manipulation during scroll, queuing them to apply when the scroll finishes. We're currently investigating ways to allow DOM manipulations to apply before a scroll starts.

http://jquerymobile.com/demos/1.0rc1/docs/api/events.html#/demos/1.0rc1/docs/api/events.html

Thats what they had to say under scroll start section

Four_lo
  • 1,150
  • 10
  • 28
  • Thanks, but that fix is unrelated to my problem. This is not an hardware acceleration bug. There are no disappearing elements. This is a problem with Javascript events not being fired during the automatic "inertial" scrolling phase. – Tobia Mar 18 '13 at 12:43
  • oh i was under the impression they were just lagging – Four_lo Mar 18 '13 at 13:09
  • is that your complete call to the win.scroll? – Four_lo Mar 18 '13 at 13:19
  • It's a very simple call. The complete code syncs a couple of divs in that way. I fear the document reference is not doing anything. – Tobia Mar 18 '13 at 13:55
  • O well, good luck. I just know scrolling can rapidly fire at the eventHandlers possibly hogging the spotight, so i figured it needed an interuption. Good luck – Four_lo Mar 18 '13 at 13:57