How to make Syncing two Divs scroll positions smoother - javascript

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.
implementing manual scrolling
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.
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.
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.

Related

Detect document scrolling on mobile without touch event

I am trying to achieve an effect on a site I'm working on, where as the user scrolls vertically, an element slides horizontally the relative amount (i.e. if the user scrolls down by 10% of the document height, the particular element will move to the left by 10% of its own width).
The issue I'm having is that I'm using the touchstart and touchend events to calculate how much the user has scrolled vertically. However, on mobile devices (e.g. iPhone), the document will continue to scroll for a short while after the user finishes swiping and stops touching the screen.
Is there an alternative way to detect the document scrolling on mobile devices that will include the distance scrolled after the touch event ends?
Thanks!
My code:
var wave = $('.front-page__mobile-wave');
var height = $(document).height();
var width = wave.width();
var oneWidth = width/100;
var ts;
$(document).bind('touchstart', function (e){
ts = e.originalEvent.touches[0].clientY;
});
$('.front-page').bind('touchmove', function(e){
var te = e.originalEvent.changedTouches[0].clientY;
var scroll = $(window).scrollTop();
var percentHeight = scroll / height * 100;
var bgSlideForward = oneWidth * percentHeight;
var bgSlideBack = -oneWidth * percentHeight;
if(ts < te){
wave.css('left', bgSlideBack);
} else if (ts > te){
wave.css('left', -bgSlideForward);
}
});
Resolution was to use the scroll event instead of touchstart and touchmove and use variables to keep track of the current vs previous scrollTop, i.e.:
var wave = $('.front-page__mobile-wave');
var height = $(document).height();
var width = wave.width();
var oneWidth = width/100;
var lastScrollTop = 0;
$(window).scroll(function(e){
var scroll = $(window).scrollTop();
var percentHeight = scroll / height * 100;
var bgSlideForward = oneWidth * percentHeight;
var bgSlideBack = -oneWidth * percentHeight;
if(scroll > lastScrollTop){
wave.css('left', bgSlideBack);
} else if (scroll < lastScrollTop){
wave.css('left', -bgSlideForward);
}
lastScrollTop = scroll;
});

Scroll one div down and one up at the same time?

I created a site which is devided vertically into two columns (each one half of the screen). If I scroll down, having the cursor on any position on the site, the left column should behave normal and scroll down, at the same the right column should scroll up in the opposite direction.
I came along this question – Modify scroll direction – and tried to get a solution out of it, but I cant get it working.
This is what I tried: http://jsbin.com/UJEBohu/1/edit
I've made a working solution, here: http://jsfiddle.net/QDUyR/1/
Put this in your <body> onload event and you should be set :)
// Add event listener for scrolling
$("#left").on("scroll", function () {
var scrolledleft = parseInt($("#left").scrollTop()) * -1;
console.log(scrolledleft + scrolledright)
$("#right").scrollTop(scrolledleft + scrolledright)
})
//Move right column to bottom initially
$("#right").scrollTop($("#right").height())
//Get actual distance scrolled
var scrolledright = parseInt($("#right").scrollTop())
Edit: Updated to work no matter what height the div's have, as long as they are equal.
The different browsers all scroll a different amount each time you scroll.
Here's my updated fiddle i have not tried it yet in safari.
I'm using a bind on the mouse wheel because Mozilla's scroll event fires multiple times every time you scroll once.
First I check to see which browser we are using and setting how much each scroll will be for that browser. Then I calculate how much to move the right div based on the amount to scroll for each browser.
In the mouse wheel event I check to make sure the right div does not go to far below or above the screen.
I'm using event.originalEvent.detail to tell which direction the mouse wheel is going in mozilla, and in IE and Chrome I am using event.originalEvent.wheelDelta.
Below is the code.
$(function()
{
if (navigator.userAgent.indexOf('Firefox') != -1 && parseFloat(navigator.userAgent.substring(navigator.userAgent.indexOf('Firefox') + 8)) >= 3.6)
{
//Firefox
var eachScroll = 114;
}
else if (navigator.userAgent.indexOf('Chrome') != -1 && parseFloat(navigator.userAgent.substring(navigator.userAgent.indexOf('Chrome') + 7).split(' ')[0]) >= 15)
{
//Chrome
var eachScroll = 100;
}
else if(navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Version') != -1 && parseFloat(navigator.userAgent.substring(navigator.userAgent.indexOf('Version') + 8).split(' ')[0]) >= 5)
{
//Safari
}
else
{
//IE
var eachScroll = 94;
}
var windowHeight = $(window).height();
var containerHeight = $("#container").height();
var heightLeftover = containerHeight - windowHeight; // Actual amount left to scroll
var totalScrolls = heightLeftover / eachScroll; // Total number of scrolls
totalScrolls = Math.ceil(totalScrolls); // Always round up
var amountToScroll = ($("#right").height() - containerHeight) / totalScrolls;
// Amount that right div will move every time we scroll
$(window).bind("mousewheel DOMMouseScroll", function(event){
var top = $("#right").position().top;
if(event.originalEvent.wheelDelta) // Check if wheelDelta exists
{
if(event.originalEvent.wheelDelta == - 120)
{
if(top < 2)
$("#right").css({top: top + amountToScroll});
}
else
{
if(top > -2000)
$("#right").css({top: top - amountToScroll});
}
}
else if(event.originalEvent.detail) // check if detail exists
{
if(event.originalEvent.detail == 3)
{
if(top < 2)
$("#right").css({top: top + amountToScroll});
}
else
{
if(top > -2000)
$("#right").css({top: top - amountToScroll});
}
}
});
});

How to prevent document scrolling but allow scrolling inside div elements on websites for iOS and Android?

I created a website with jQueryMobile for iOS and Android.
I don't want the document itself to scroll. Instead, just an area (a <div> element) should be scrollable (via css property overflow-y:scroll).
So I disabled document scrolling via:
$(document).bind("touchstart", function(e){
e.preventDefault();
});
$(document).bind("touchmove", function(e){
e.preventDefault();
});
But that will also disable scrolling for all other elements in the document, no matter if overflow:scroll is set or not.
How can I solve this?
How about this CSS only solution:
https://jsfiddle.net/Volker_E/jwGBy/24/
body gets position: fixed; and every other element you wish an overflow: scroll;.
Works on mobile Chrome (WebKit)/Firefox 19/Opera 12.
You'll also see my various attempts towards a jQuery solution. But as soon as you're binding touchmove/touchstart to document, it hinders scrolling in the child div no matter if unbinded or not.
Disclaimer: Solutions to this problem are in many ways basically not very nice UX-wise! You'll never know how big the viewport of your visitors exactly is or which font-size they are using (client user-agent style like), therefore it could easily be, that important content is hidden to them in your document.
Maybe I misunderstood the question, but if I'm correct:
You want not to be able to scroll except a certain element so you:
$(document).bind("touchmove", function(e){
e.preventDefault();
});
Prevent everything within the document.
Why don't you just stop the event bubbling on the element where you wish to scroll?
(PS: you don't have to prevent touchstart -> if you use touch start for selecting elements instead of clicks that is prevented as well, touch move is only needed because then it is actually tracing the movement)
$('#element').on('touchmove', function (e) {
e.stopPropagation();
});
Now on the element CSS
#element {
overflow-y: scroll; // (vertical)
overflow-x: hidden; // (horizontal)
}
If you are on a mobile device, you can even go a step further.
You can force hardware accelerated scrolling (though not all mobile browsers support this);
Browser Overflow scroll:
Android Browser Yes
Blackberry Browser Yes
Chrome for Mobile Yes
Firefox Mobile Yes
IE Mobile Yes
Opera Mini No
Opera Mobile Kinda
Safari Yes
#element.nativescroll {
-webkit-overflow-scrolling: touch;
}
normal:
<div id="element"></div>
native feel:
<div id="element" class="nativescroll"></div>
Finally, I got it to work. Really simple:
var $layer = $("#layer");
$layer.bind('touchstart', function (ev) {
var $this = $(this);
var layer = $layer.get(0);
if ($this.scrollTop() === 0) $this.scrollTop(1);
var scrollTop = layer.scrollTop;
var scrollHeight = layer.scrollHeight;
var offsetHeight = layer.offsetHeight;
var contentHeight = scrollHeight - offsetHeight;
if (contentHeight == scrollTop) $this.scrollTop(scrollTop-1);
});
Here is a solution I am using:
$scrollElement is the scroll element, $scrollMask is a div with style position: fixed; top: 0; bottom: 0;.
The z-index of $scrollMask is smaller than $scrollElement.
$scrollElement.on('touchmove touchstart', function (e) {
e.stopPropagation();
});
$scrollMask.on('touchmove', function(e) {
e.stopPropagation();
e.preventDefault();
});
I was looking for a solution that did not require calling out specific areas that should scroll. Piecing together a few resources, here is what worked for me:
// Detects if element has scroll bar
$.fn.hasScrollBar = function() {
return this.get(0).scrollHeight > this.outerHeight();
}
$(document).on("touchstart", function(e) {
var $scroller;
var $target = $(e.target);
// Get which element could have scroll bars
if($target.hasScrollBar()) {
$scroller = $target;
} else {
$scroller = $target
.parents()
.filter(function() {
return $(this).hasScrollBar();
})
.first()
;
}
// Prevent if nothing is scrollable
if(!$scroller.length) {
e.preventDefault();
} else {
var top = $scroller[0].scrollTop;
var totalScroll = $scroller[0].scrollHeight;
var currentScroll = top + $scroller[0].offsetHeight;
// If at container edge, add a pixel to prevent outer scrolling
if(top === 0) {
$scroller[0].scrollTop = 1;
} else if(currentScroll === totalScroll) {
$scroller[0].scrollTop = top - 1;
}
}
});
This code requires jQuery.
Sources:
this post
https://github.com/luster-io/prevent-overscroll
How can I check if a scrollbar is visible?
Jquery check if any parent div have scroll bar
Update
I needed a vanilla JavaScript version of this, so the following is a modified version. I implemented a margin-checker and something that explicitly allows input/textareas to be clickable (I was running into issues with this on the project I used it on...it may not be necessary for your project). Keep in mind this is ES6 code.
const preventScrolling = e => {
const shouldAllowEvent = element => {
// Must be an element that is not the document or body
if(!element || element === document || element === document.body) {
return false;
}
// Allow any input or textfield events
if(['INPUT', 'TEXTAREA'].indexOf(element.tagName) !== -1) {
return true;
}
// Get margin and outerHeight for final check
const styles = window.getComputedStyle(element);
const margin = parseFloat(styles['marginTop']) +
parseFloat(styles['marginBottom']);
const outerHeight = Math.ceil(element.offsetHeight + margin);
return (element.scrollHeight > outerHeight) && (margin >= 0);
};
let target = e.target;
// Get first element to allow event or stop
while(target !== null) {
if(shouldAllowEvent(target)) {
break;
}
target = target.parentNode;
}
// Prevent if no elements
if(!target) {
e.preventDefault();
} else {
const top = target.scrollTop;
const totalScroll = target.scrollHeight;
const currentScroll = top + target.offsetHeight;
// If at container edge, add a pixel to prevent outer scrolling
if(top === 0) {
target.scrollTop = 1;
} else if(currentScroll === totalScroll) {
target.scrollTop = top - 1;
}
}
};
document.addEventListener('touchstart', preventScrolling);
document.addEventListener('mousedown', preventScrolling);
In my case, I have a scrollable body and a scrollable floating menu over it. Both have to be scrollable, but I had to prevent body scrolling when "floating menu" (position:fixed) received touch events and was scrolling and it reached top or bottom. By default browser then started to scroll the body.
I really liked jimmont's answer, but unfortunatelly it did not work well on all devices and browsers, especially with a fast and long swipe.
I ended up using MOMENTUM SCROLLING USING JQUERY (hnldesign.nl) on floating menu, which prevents default browser scrolling and then animates scrolling itself. I include that code here for completeness:
/**
* jQuery inertial Scroller v1.5
* (c)2013 hnldesign.nl
* This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-sa/4.0/.
**/
/*jslint browser: true*/
/*global $, jQuery*/
/* SETTINGS */
var i_v = {
i_touchlistener : '.inertialScroll', // element to monitor for touches, set to null to use document. Otherwise use quotes. Eg. '.myElement'. Note: if the finger leaves this listener while still touching, movement is stopped.
i_scrollElement : '.inertialScroll', // element (class) to be scrolled on touch movement
i_duration : window.innerHeight * 1.5, // (ms) duration of the inertial scrolling simulation. Devices with larger screens take longer durations (phone vs tablet is around 500ms vs 1500ms). This is a fixed value and does not influence speed and amount of momentum.
i_speedLimit : 1.2, // set maximum speed. Higher values will allow faster scroll (which comes down to a bigger offset for the duration of the momentum scroll) note: touch motion determines actual speed, this is just a limit.
i_handleY : true, // should scroller handle vertical movement on element?
i_handleX : true, // should scroller handle horizontal movement on element?
i_moveThreshold : 100, // (ms) determines if a swipe occurred: time between last updated movement # touchmove and time # touchend, if smaller than this value, trigger inertial scrolling
i_offsetThreshold : 30, // (px) determines, together with i_offsetThreshold if a swipe occurred: if calculated offset is above this threshold
i_startThreshold : 5, // (px) how many pixels finger needs to move before a direction (horizontal or vertical) is chosen. This will make the direction detection more accurate, but can introduce a delay when starting the swipe if set too high
i_acceleration : 0.5, // increase the multiplier by this value, each time the user swipes again when still scrolling. The multiplier is used to multiply the offset. Set to 0 to disable.
i_accelerationT : 250 // (ms) time between successive swipes that determines if the multiplier is increased (if lower than this value)
};
/* stop editing here */
//set some required vars
i_v.i_time = {};
i_v.i_elem = null;
i_v.i_elemH = null;
i_v.i_elemW = null;
i_v.multiplier = 1;
// Define easing function. This is based on a quartic 'out' curve. You can generate your own at http://www.timotheegroleau.com/Flash/experiments/easing_function_generator.htm
if ($.easing.hnlinertial === undefined) {
$.easing.hnlinertial = function (x, t, b, c, d) {
"use strict";
var ts = (t /= d) * t, tc = ts * t;
return b + c * (-1 * ts * ts + 4 * tc + -6 * ts + 4 * t);
};
}
$(i_v.i_touchlistener || document)
.on('touchstart touchmove touchend', function (e) {
"use strict";
//prevent default scrolling
e.preventDefault();
//store timeStamp for this event
i_v.i_time[e.type] = e.timeStamp;
})
.on('touchstart', function (e) {
"use strict";
this.tarElem = $(e.target);
this.elemNew = this.tarElem.closest(i_v.i_scrollElement).length > 0 ? this.tarElem.closest(i_v.i_scrollElement) : $(i_v.i_scrollElement).eq(0);
//dupecheck, optimizes code a bit for when the element selected is still the same as last time
this.sameElement = i_v.i_elem ? i_v.i_elem[0] == this.elemNew[0] : false;
//no need to redo these if element is unchanged
if (!this.sameElement) {
//set the element to scroll
i_v.i_elem = this.elemNew;
//get dimensions
i_v.i_elemH = i_v.i_elem.innerHeight();
i_v.i_elemW = i_v.i_elem.innerWidth();
//check element for applicable overflows and reevaluate settings
this.i_scrollableY = !!((i_v.i_elemH < i_v.i_elem.prop('scrollHeight') && i_v.i_handleY));
this.i_scrollableX = !!((i_v.i_elemW < i_v.i_elem.prop('scrollWidth') && i_v.i_handleX));
}
//get coordinates of touch event
this.pageY = e.originalEvent.touches[0].pageY;
this.pageX = e.originalEvent.touches[0].pageX;
if (i_v.i_elem.is(':animated') && (i_v.i_time.touchstart - i_v.i_time.touchend) < i_v.i_accelerationT) {
//user swiped while still animating, increase the multiplier for the offset
i_v.multiplier += i_v.i_acceleration;
} else {
//else reset multiplier
i_v.multiplier = 1;
}
i_v.i_elem
//stop any animations still running on element (this enables 'tap to stop')
.stop(true, false)
//store current scroll positions of element
.data('scrollTop', i_v.i_elem.scrollTop())
.data('scrollLeft', i_v.i_elem.scrollLeft());
})
.on('touchmove', function (e) {
"use strict";
//check if startThreshold is met
this.go = (Math.abs(this.pageX - e.originalEvent.touches[0].pageX) > i_v.i_startThreshold || Math.abs(this.pageY - e.originalEvent.touches[0].pageY) > i_v.i_startThreshold);
})
.on('touchmove touchend', function (e) {
"use strict";
//check if startThreshold is met
if (this.go) {
//set animpar1 to be array
this.animPar1 = {};
//handle events
switch (e.type) {
case 'touchmove':
this.vertical = Math.abs(this.pageX - e.originalEvent.touches[0].pageX) < Math.abs(this.pageY - e.originalEvent.touches[0].pageY); //find out in which direction we are scrolling
this.distance = this.vertical ? this.pageY - e.originalEvent.touches[0].pageY : this.pageX - e.originalEvent.touches[0].pageX; //determine distance between touches
this.acc = Math.abs(this.distance / (i_v.i_time.touchmove - i_v.i_time.touchstart)); //calculate acceleration during movement (crucial)
//determine which property to animate, reset animProp first for when no criteria is matched
this.animProp = null;
if (this.vertical && this.i_scrollableY) { this.animProp = 'scrollTop'; } else if (!this.vertical && this.i_scrollableX) { this.animProp = 'scrollLeft'; }
//set animation parameters
if (this.animProp) { this.animPar1[this.animProp] = i_v.i_elem.data(this.animProp) + this.distance; }
this.animPar2 = { duration: 0 };
break;
case 'touchend':
this.touchTime = i_v.i_time.touchend - i_v.i_time.touchmove; //calculate touchtime: the time between release and last movement
this.i_maxOffset = (this.vertical ? i_v.i_elemH : i_v.i_elemW) * i_v.i_speedLimit; //(re)calculate max offset
//calculate the offset (the extra pixels for the momentum effect
this.offset = Math.pow(this.acc, 2) * (this.vertical ? i_v.i_elemH : i_v.i_elemW);
this.offset = (this.offset > this.i_maxOffset) ? this.i_maxOffset : this.offset;
this.offset = (this.distance < 0) ? -i_v.multiplier * this.offset : i_v.multiplier * this.offset;
//if the touchtime is low enough, the offset is not null and the offset is above the offsetThreshold, (re)set the animation parameters to include momentum
if ((this.touchTime < i_v.i_moveThreshold) && this.offset !== 0 && Math.abs(this.offset) > (i_v.i_offsetThreshold)) {
if (this.animProp) { this.animPar1[this.animProp] = i_v.i_elem.data(this.animProp) + this.distance + this.offset; }
this.animPar2 = { duration: i_v.i_duration, easing : 'hnlinertial', complete: function () {
//reset multiplier
i_v.multiplier = 1;
}};
}
break;
}
// run the animation on the element
if ((this.i_scrollableY || this.i_scrollableX) && this.animProp) {
i_v.i_elem.stop(true, false).animate(this.animPar1, this.animPar2);
}
}
});
Another observation: I also tried various combinations of e.stopPropagation() on menu div and e.preventDefault() on window/body at touchmove event, but without success, I only managed to prevent scrolling I wanted and not scrolling I did not want. I also tried to have a div over whole document, with z-index between document and menu, visible only between touchstart and touchend, but it did not receive touchmove event (because it was under menu div).
Here is a solution that uses jQuery for the events.
var stuff = {};
$('#scroller').on('touchstart',stuff,function(e){
e.data.max = this.scrollHeight - this.offsetHeight;
e.data.y = e.originalEvent.pageY;
}).on('touchmove',stuff,function(e){
var dy = e.data.y - e.originalEvent.pageY;
// if scrolling up and at the top, or down and at the bottom
if((dy < 0 && this.scrollTop < 1)||(dy > 0 && this.scrollTop >= e.data.max)){
e.preventDefault();
};
});
First position the innerScroller wherever you want on the screen and then fix outerScroller by setting it css to 'hidden'. When you want to restore it you can set it back to 'auto' or 'scroll', whichever you used previously.
Here is my implementation which works on touch devices and laptops.
function ScrollManager() {
let startYCoord;
function getScrollDiff(event) {
let delta = 0;
switch (event.type) {
case 'mousewheel':
delta = event.wheelDelta ? event.wheelDelta : -1 * event.deltaY;
break;
case 'touchstart':
startYCoord = event.touches[0].clientY;
break;
case 'touchmove': {
const yCoord = event.touches[0].clientY;
delta = yCoord - startYCoord;
startYCoord = yCoord;
break;
}
}
return delta;
}
function getScrollDirection(event) {
return getScrollDiff(event) >= 0 ? 'UP' : 'DOWN';
}
function blockScrollOutside(targetElement, event) {
const { target } = event;
const isScrollAllowed = targetElement.contains(target);
const isTouchStart = event.type === 'touchstart';
let doScrollBlock = !isTouchStart;
if (isScrollAllowed) {
const isScrollingUp = getScrollDirection(event) === 'UP';
const elementHeight = targetElement.scrollHeight - targetElement.offsetHeight;
doScrollBlock =
doScrollBlock &&
((isScrollingUp && targetElement.scrollTop <= 0) ||
(!isScrollingUp && targetElement.scrollTop >= elementHeight));
}
if (doScrollBlock) {
event.preventDefault();
}
}
return {
blockScrollOutside,
getScrollDirection,
};
}
const scrollManager = ScrollManager();
const testBlock = document.body.querySelector('.test');
function handleScroll(event) {
scrollManager.blockScrollOutside(testBlock, event);
}
window.addEventListener('scroll', handleScroll);
window.addEventListener('mousewheel', handleScroll);
window.addEventListener('touchstart', handleScroll);
window.addEventListener('touchmove', handleScroll);
.main {
border: 1px solid red;
height: 200vh;
}
.test {
border: 1px solid green;
height: 300px;
width: 300px;
overflow-y: auto;
position: absolute;
top: 100px;
left: 50%;
}
.content {
height: 100vh;
}
<div class="main">
<div class="test">
<div class="content"></div>
</div>
</div>
This is what worked for me for Android and IOS devices.
Imagine that we have a div class="backdrop"> element that we don't want to be scrolled, ever. But we want to be able to scroll over an element that is on top of this backdrop.
function handleTouchMove(event) {
const [backdrop] = document.getElementsByClassName('backdrop');
const isScrollingBackdrop = backdrop === event.target;
isScrollingBackdrop ? event.preventDefault() : event.stopPropagation();
}
window.addEventListener('touchmove', handleTouchMove, { passive: false });
So, we listen to the touchmove event, if we're scrolling over the backdrop, we prevent it. If we're scrolling over something else, we allow it but stop its propagation so it doesn't scroll also the backdrop.
Of course this is pretty basic and can be re-worked and expanded a lot, but this is what fixed my issue in a VueJs2 project.
Hope it helps! ;)
I was able to disable scrolling of the main document by adding css "overflow-y: hidden" on HTML.
It did not mess with positioning at all.

parallax scrolling issue - div element jerking when scrolling in webkit browsers

I have created a parallax scroll, which seem to be working fine in firefox however in the chrome browser there's a slight jump on the body text when scrolling. click here scroll to the about section. I am not sure if t this is a css or JS issue.. below is a snippet i have incorporated into my parallax function
Does anyone know how i an fix this issue?
$(document).ready(function(){
// Cache the Window object
$window = $(window);
// Cache the Y offset and the speed of each sprite
$('[data-type]').each(function() {
$(this).data('offsetY', parseInt($(this).attr('data-offsetY')));
$(this).data('Xposition', $(this).attr('data-Xposition'));
$(this).data('speed', $(this).attr('data-speed'));
});
// For each element that has a data-type attribute
$('[data-type="background"]').each(function(){
// Store some variables based on where we are
var $self = $(this),
offsetCoords = $self.offset(),
topOffset = offsetCoords.top;
// When the window is scrolled...
$(window).scroll(function() {
// If this section is in view
if ( ($window.scrollTop() + $window.height()) > (topOffset) &&
( (topOffset + $self.height()) > $window.scrollTop() ) ) {
// Scroll the background at var speed
// the yPos is a negative value because we're scrolling it UP!
var yPos = -($window.scrollTop() / $self.data('speed'));
// If this element has a Y offset then add it on
if ($self.data('offsetY')) {
yPos += $self.data('offsetY');
}
// Put together our final background position
var coords = '50% '+ yPos + 'px';
// Move the background
$self.css({ backgroundPosition: coords });
$('[data-type="scroll-text"]', $self).each(function() {
var $text= $(this);
var pos = ($window.scrollTop()/10) * $text.data('speed');
var curP = $text.css('margin-top');
var is_chrome = navigator.userAgent.toLowerCase().indexOf('chrome') > -1;
if(is_chrome) {
$text.animate({
paddingTop: pos,
}, 200, 'linear', function() {
// Animation complete.
});
} else {
$text.css('padding-top', pos);
}
});
}; // in view
}); // window scroll
}); // each data-type
}); // document ready
Some suggestions:
1.) Use position: fixed to avoid any jitter, as you'll be taking the element out of the document flow. You can then position it using z-index.
2.) Cache as much as you can to ease processing time.
3.) Math.round may not be necessary, but try adding this CSS to your moving areas: -webkit-transform: translate3d(0,0,0); This will force hardware acceleration in Chrome, which may ease some of the jittering. (It looked smoother on my screen when I added this with Inspector, but it didn't get rid of the jumpiness with the scroll wheel.) Note: Don't do this on your entire document (e.g. body tag), as it might cause some issues with your current layout. (Your navigation bar didn't stick to the top of the window, for instance.)
4.) If you have any animations running as part of your parallax logic (tweening the margin into place or something along those lines), remove it - that would probably cause the jump you see.
Hope this helps. Best of luck.
I see the same jittering in FireFox and Chrome (Mac). Looking at your containers, one thing that's glaring at me is the pixel position that's being calculated/used.
Chrome: <div id="about-title" style="margin-top: 1562.3999999999999px;">
FireFox: <div id="about-title" style="margin-top: 1562.4px;">
Browsers aren't going to allow content to sit at 1/2 pixel, let alone 0.3999999 of a pixel. I think it's moving it, and trying to calculate whether to round up or round down. It jitters because it's calculating with every click of your mouse wheel.
Thus, I'd try adding Math.round() to your positions so that the containers are never being left in limbo.
Take a look at the code here: http://webdesigntutsplus.s3.amazonaws.com/tuts/338_parallax/src/index.html
Firebug some of the elements, and you'll see that their only fraction of a pixel is '0.5'. Most of them (the bulk) go to round number values.
You are going to have to change the way that the scrolling works (i.e. change how the spacing is computed), but this can be fixed by adding the position:fixed CSS element to the page elements that are scrolling. The problem is coming from the time that it takes for the JavaScript to process and then render.
For example, on your page you would set each of the <div> tags containing text to have a fixed position and then use the JavaScript/JQuery function to update the top: CSS element. This should make the page scroll smoothly.
Have you tried adding the preventdefault inside the scroll function?
$(window).scroll(function(e) {
e.preventDefault();
// rest of your code
}
In a previous question I created a fairly good parallax scrolling implementation. Jquery Parallax Scrolling effect - Multi directional You might find it useful.
Here's the JSFiddle http://jsfiddle.net/9R4hZ/40/ use the up/down arrows or scroll wheel.
Using padding and margin for the positioning are probably why you're experiencing rendering issues. While my code uses scroll or keyboard input for the effect you can loop the relavent portion and check the $moving variable until you reach the desired element on screen.
function parallaxScroll(scroll) {
// current moving object
var ml = $moving.position().left;
var mt = $moving.position().top;
var mw = $moving.width();
var mh = $moving.height();
// calc velocity
var fromTop = false;
var fromBottom = false;
var fromLeft = false;
var fromRight = false;
var vLeft = 0;
var vTop = 0;
if($moving.hasClass('from-top')) {
vTop = scroll;
fromTop = true;
} else if($moving.hasClass('from-bottom')) {
vTop = -scroll;
fromBottom = true;
} else if($moving.hasClass('from-left')) {
vLeft = scroll;
fromLeft = true;
} else if($moving.hasClass('from-right')) {
vLeft = -scroll;
fromRight = true;
}
// calc new position
var newLeft = ml + vLeft;
var newTop = mt + vTop;
// check bounds
var finished = false;
if(fromTop && (newTop > t || newTop + mh < t)) {
finished = true;
newTop = (scroll > 0 ? t : t - mh);
} else if(fromBottom && (newTop < t || newTop > h)) {
finished = true;
newTop = (scroll > 0 ? t : t + h);
} else if(fromLeft && (newLeft > l || newLeft + mw < l)) {
finished = true;
newLeft = (scroll > 0 ? l : l - mw);
} else if(fromRight && (newLeft < l || newLeft > w)) {
finished = true;
newLeft = (scroll > 0 ? l : l + w);
}
// set new position
$moving.css('left', newLeft);
$moving.css('top', newTop);
// if finished change moving object
if(finished) {
// get the next moving
if(scroll > 0) {
$moving = $moving.next('.parallax');
if($moving.length == 0)
$moving = $view.find('.parallax:last');
} else {
$moving = $moving.prev('.parallax');
if($moving.length == 0)
$moving = $view.find('.parallax:first');
}
}
// for debug
$('#direction').text(scroll + " " + l + "/" + t + " " + ml + "/" + mt + " " + finished + " " + $moving.text());
}
May not be related to your specifics, but I had a jumpy parallax scrolling problem, I was able to solve it adding the following CSS for the fixed portions of the page:
#supports (background-attachment: fixed)
{
.fixed-background
{
background-attachment: fixed;
}
}
Not sure of all the specifics, but found at Alternate Fixed & Scroll Backgrounds

Handling swipe in touch devices

I have this horizontal carousel component that I want to make it work for both Mouse and Swipe events.
Everything is working fine, except for one bit: in touch devices, I don't want the carousel to scroll horizontally if the user is trying to swipe vertically to scroll through the whole page.
What I am doing is,
on mousedown/touchstart - I stop the event from propagating to avoid page scroll, item selects, etc...
on the first move event, on the carousel, I set a 50ms timeout to determine if the user is moving vertically or horizontally.
If deltaX < deltaY, I stop my horizontal scrolling, manually fire the touchstart event, with a flag indicating that i fired it manually
on my mousedown/touchstart handler, I read that "manually" flag and, if it's true, I return true from my function so all the default browser events, like the page vertical scrolling, continue to work.
This is not working, everything I'm doing responds correctly but the browser doesn't pick up and scroll the page. I hope I am explaining myself correctly enough so you guys can help me... I don't have an online example because this is a "secret" project for my company...
I was trying to do the same thing as you (were?). The key is to check on touchmove if the current touch and the last touch is more vertical than horizontal. If the touch was more left to right or right to left, prevent the event's default, otherwise ignore it. Here's what I ended up writing. Hopefully it works for you!
var gestures = function() {
var self = this,
coords = {
startX: null,
startY: null,
endX: null,
endY: null
};
self.$el.on('touchstart', function(e) {
coords.startX = e.originalEvent.targetTouches[0].clientX;
coords.startY = e.originalEvent.targetTouches[0].clientY;
coords.endX = coords.startX;
coords.endY = coords.startY;
});
self.$el.on('touchmove', function(e) {
var newX = e.originalEvent.targetTouches[0].clientX,
newY = e.originalEvent.targetTouches[0].clientY,
absX = Math.abs(coords.endX - newX),
absY = Math.abs(coords.endY - newY);
// If we've moved more Y than X, we're scrolling vertically
if (absX < absY) {
return;
}
// Prevents the page from scrolling left/right
e.preventDefault();
coords.endX = newX;
coords.endY = newY;
});
self.$el.on('touchend', function(e) {
var swipe = {},
deltaX = coords.startX - coords.endX,
deltaY = coords.startY - coords.endY,
absX = Math.abs(deltaX),
absY = Math.abs(deltaY);
swipe.distance = (absX > absY) ? absX : absY;
swipe.direction = (absX < absY) ?
(deltaY < 0 ? 'down' : 'up') :
(deltaX < 0 ? 'right' : 'left');
// console.log(swipe.direction + ' ' + swipe.distance + 'px');
// If we have a swipe of > 50px, let's use it!
if (swipe.distance > 50) {
if (swipe.direction === 'left') {
self.advance();
} else if (swipe.direction === 'right') {
self.retreat();
}
}
});
};
this is my slider object and $el is the container element.

Categories