Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make Syncing two Divs scroll positions smoother

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.

like image 243
redV Avatar asked Aug 25 '14 00:08

redV


1 Answers

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.

like image 70
dreamlab Avatar answered Oct 17 '22 03:10

dreamlab