How to make Syncing two Divs scroll positions smoother
Asked Answered
M

1

6

I am trying to sync two scrollable DIVS scroll positions.

Methods followed :

Method - 1 : on-scroll event setting the scrollTop of other DIV. problem : scroll event executed at the end and UI is sluggish in iOS safari.

Method - 2 : used setInterval to sync both scroll positions. Problem : iOS does not execute timer functions during scroll, so scroll positions synced at the end. Again this is more sluggish. Tried, timers fix as mentioned in many blogs but still no grace.

Method -3 : Tried custom scrollbar, so iScroll and tried to sync both on scroll event, Problem : this seems better but in iOS still it is sluggish!!!

Method -4 : Tried custom scrollbar, so iScroll and tried to sync both on scroll event, Problem : Used iScroll but using timers rather depending on onScroll event, But during touchmove, iOS is busy in providing animations rather executing required timers till touchend. Below code refers to this method. It is also sluggish.

var active = .., other = ...
// active : active Scrolling element
// other : Element to be in sync with active
window.setInterval(function () {
    var y;
    if (active) {
        y = active.y;
    } else {
        return;
    }
    var percentage = -y / (active.scrollerHeight - active.wrapperHeight);
    var oscrollTop = percentage * (other.scrollerHeight - other.wrapperHeight);
    if (-other.maxScrollY >= toInt(oscrollTop)) {
        other.scrollTo(0, -toInt(oscrollTop));
    }
}, 20);

How can make syncing scroll positions of two scrollable DIVS smoother. Please suggest me something, it is irritating me.

Mariselamarish answered 25/8, 2014 at 0:38 Comment(9)
Do they have to be 2 DIVs? Can you share a bit of HTML?Hesychast
@Hesychast They do not share same HTMl, so could be 0-100px difference in height. That's why I am calculating percentage scrolled and scrolling accordingly.Mariselamarish
Could you make a simple demo for us to edit?Ammamaria
@ZachSaucier Updated fiddles for last two methods and will post for others too.Mariselamarish
so method 3 is just an iOS issue? do you need vanilla or jQuery?Popish
@Popish Anything is fine, I just want it to work :) If you are comfortable with jQuery please go ahead. I will convert it to vanilla JS.Mariselamarish
@Popish Added fiddles for Method 1 and 2Mariselamarish
Where are you seeing the sluggish behavior? I'm running #4 on a retina iPad - latest iOS and it seems to keep up just fine.Tamarau
@Tamarau I am running it on iOS7 with iPhone4 device.Mariselamarish
P
3

relying on the scroll events (OPs method 1) is fine for a desktop implementation. the scroll event fires before the screen is updated. on mobile devices, especially iOS this is not the case. due to limited resources the scroll event only fires after the user completed (lifted his finger) the scroll operation.

implementing manual scrolling

to have a scroll event while the user scrolls on iOS requires to implement the scrolling manually.

  1. register the touchstart event. and get the first touch:

    var element1 = document.getElementById('content1');
    var element2 = document.getElementById('content2');
    
    var activeTouch = null;
    var touchStartY = 0;
    var element1StartScrollTop = 0;
    var element2scrollSyncFactor = 0;
    
    document.addEventListener('touchstart', function(event) {
        event.preventDefault();
    
        var touch = event.changedTouches[0];
    
        if ( activeTouch == null ) {
            // implement check if touch started on an element you want to be scrollable
            // save a reference to the scrolling element for the other functions
            activeTouch = touch;
            touchStartY = touch.screenY;
            // if scroll content does not change do this calculation only once to safe compute and dom access time while animating
            calcSyncFactor();
        }
    });
    
    function calcSyncFactor()
    {
        // calculate a factor for scroll areas with different height
        element2scrollSyncFactor = (element2.scrollHeight - element2.clientHeight) / (element1.scrollHeight - element1.clientHeight);    
    }
    
  2. update your scroll position on finger movement:

    document.addEventListener('touchmove', function() {
        for ( var i = 0; i < event.changedTouches.length; i++ ) {
            var touch = event.changedTouches[i];
    
            if ( touch === activeTouch ) {
                var yOffset = touch.screenY - touchStartY;
                element1.scrollTop = element1StartScrollTop + (0 - yOffset);
                syncScroll();
                break;
            }
        }    
    });
    
    function syncScroll()
    {
        element2.scrollTop = Math.round(element1.scrollTop * element2scrollSyncFactor);
    }
    

    it is possible to add a check that starts the scrolling only after the user has moved his finger some pixels. this way if the user clicks an element the document will not scroll some pixels.

  3. cleanup after the user lifts the finger:

    document.addEventListener('touchend', touchEnd);
    document.addEventListener('touchcancel', touchEnd);
    
    function touchEnd(event)
    {
        for ( var i = 0; i < event.changedTouches.length; i++ ) {
            var touch = event.changedTouches[i];
            if ( touch === activeTouch ) {
                // calculate inertia and apply animation
                activeTouch = null;
                break;
            }
        }    
    }
    

    to have the scrolling feel more natuaral apply the iOS rubber band effect and inertia. calculate the velocity of the scroll by comparing the last touchMove yOffset with the one before. from this velocity apply an animation (for example css transition) that slowly stops the scrolling

see FIDDLE. see result on iOS. the fiddle only implements the solution for touch devices. for desktop devices use OP's method 1. implement a condition which checks which method to use depending on device.

how to apply inertia with css transitions

it would be possible to animate in javascript with requestAnimationFrame. a probably more performant way on mobile might be the use of css transformations or css animations. although an elements scroll position can not be animated with css.

  1. change the structure of the html to.

    • div: container with overflow: hidden

      • div: content with position: absolute

        depending on content size use css property -webkit-transform: translateZ(0) on content div. this will create a new layer with its own backing surface, which will be composited on the gpu.

  2. implement the functions described above so that they animate the content's top position instend of scrollTop

    var element1 = document.getElementById('content1');
    var element2 = document.getElementById('content2');
    
    var activeTouch = null;
    var touchStartY = 0;
    var element1StartScrollTop = 0;
    var element2scrollSyncFactor = 0;
    var offsetY = 0;
    var lastOffsetY = 0;
    
    document.addEventListener('touchstart', function(event) {
        event.preventDefault();
    
        var touch = event.changedTouches[0];
    
        if ( activeTouch == null ) {
            activeTouch = touch;
            touchStartY = touch.screenY;
            // use offsetTop instead of scrollTop
            element1StartScrollTop = element1.offsetTop;
            // if scroll content does not change do this calculation only once to safe compute time while animating
            calcSyncFactor();
    
            // cancel inertia animations
            element1.style.webkitTransition = 'none';
            element2.style.webkitTransition = 'none';
        }
    });
    
    function calcSyncFactor()
    {
        // calculate a factor for scroll areas with different height   
        // use the div's sizes instead of scrollTop
        element2scrollSyncFactor = (element2.clientHeight - element2.parentNode.clientHeight) / (element1.clientHeight - element1.parentNode.clientHeight);    
    }
    
    document.addEventListener('touchmove', function() {
        for ( var i = 0; i < event.changedTouches.length; i++ ) {
            var touch = event.changedTouches[i];
    
            if ( touch === activeTouch ) {
                lastOffsetY = offsetY;
                offsetY = touch.screenY - touchStartY;
                // use offsetTop instead of scrollTop
                element1.style.top = (element1StartScrollTop + offsetY) + 'px';
                syncScroll();
                break;
            }
        }    
    });
    
    function syncScroll()
    {
        element2.style.top = Math.round(element1.offsetTop * element2scrollSyncFactor) + 'px';
    }
    
    document.addEventListener('touchend', touchEnd);
    document.addEventListener('touchcancel', touchEnd);
    
    function touchEnd(event)
    {
        for ( var i = 0; i < event.changedTouches.length; i++ ) {
            var touch = event.changedTouches[i];
            if ( touch === activeTouch ) {
                applyInertia();
                activeTouch = null;
                break;
            }
        }    
    }
    
  3. when the user finishes scrolling and lifts his finger apply the inertia

    function applyInertia()
    {
        var velocity = offsetY - lastOffsetY;
        var time = Math.abs(velocity) / 150;
        var element1EndPosition = element1.offsetTop + velocity;
    
        element1.style.webkitTransition = 'top ' + time + 's ease-out';
        element1.style.top = element1EndPosition + 'px';
    
        element2.style.webkitTransition = 'top ' + time + 's ease-out';
        element2.style.top = Math.round(element1EndPosition * element2scrollSyncFactor) + 'px';
    }
    

    the inertia is calculated from the velocity when the user lifted the finger. fiddle around with the values to get desired results. a rubberband effect could be implemented in this function aswell. to have no javascript involved applying css animations might be the trick. another way would be to register events for when the transitions finish. if the transition finishes and the scroll position is outside the container apply a new transition that animates the content back.

see FIDDLE. see result on iOS.

Popish answered 27/8, 2014 at 21:12 Comment(18)
@Thanks for the answer. But the problem will occur while moving finger fast and trying to set scrollTop? And they will not be having same scrollHeight so percentage scrolled need to calculated.Mariselamarish
what do you mean: "moving finger fast and trying to set scrollTop". have you checked the result on iOS. you can move the finger as fast as you want. all browser scrolling has to be disabled.Popish
I mean to say elastic scroll. I faced the problem of sluggishness when moving finger faster, ie Velocity of the touchmove.Mariselamarish
animate the inertia with css. this will get the most smooth result: calculate the velocity of the scroll by comparing the last touchMove yOffset with the one before. from this velocity apply an animation (for example css transition) that slowly stops the scrollingPopish
edited anwer to explain how to apply iOS style inertiaPopish
@Mariselamarish have you tried gpu hardware compositing. -webkit-transform: translateZ(0)?Popish
Yes, iScroll is dependent on transforms of available to make scroll smootherMariselamarish
so scrolling in general is not smooth? or syncing scroll positions is not smooth?Popish
Scroll positions, I mean while scrolling one, another's scrolling is not smooth.Mariselamarish
Thanks a a lot for your effort.Mariselamarish
is the second one also not smooth ins this example? i testes on iphone 4. fiddle.jshell.net/bqzo7cw4/40/show/lightPopish
Yes I tested all of those solutions in iPhone4 device with iOS7 OS. They were sluggish. FYI I am using it inside PhonegapMariselamarish
i can replicate method 3 on iphone 4. javascript execution is slower in apps (phonegap) that in safari. is it also slugish in safari? is it maybe slugish because the second div scrolls faster/at a different speed than the first one? it is an unfamiliar behaviour.Popish
Yes, they both have to scrolled at a time, rather second is scrolling without animation.Mariselamarish
This is one of the best explanations I have ever seen.Mariselamarish
Second fiddle does not work in mobile. activeTouch is not always null. Could you please check it.Mariselamarish
i just checked and can't replicate. it works on my devices. what do you mean activeTouch is not always null? activeTouch is a variable i set up to check if there is more than one finger on the screen. this way, a second finger can not scroll while a first is on the display. when the first finger is lifted activeTouch is set to null again.Popish
Sorry for that Yesterday I have checked it on Chrome emulator with Mac. Will check it in Device. It worked on Mobile when I have checked last time. ThanksMariselamarish

© 2022 - 2024 — McMap. All rights reserved.