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.
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.
to have a scroll event while the user scrolls on iOS requires to implement the scrolling manually.
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);
}
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.
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.
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.
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.
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;
}
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With