Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
185 views
in Technique[技术] by (71.8m points)

javascript - Determine if a snap-scroll element's snap scrolling event is complete

Abstract

I am creating an image gallery using a scrollable element. I am using CSS' scroll-snap feature, which allows me to snap onto the elements (images) in the scroller.

By binding to the element's scroll event, I am applying various actions when the user is scrolling the element (things like preloading, hiding interface elements, etc). One of these is dependent on the scrolling event and needs to stop at the exact moment scrolling is completed. But scroll-snapping presents me with an unforeseen, and yet un-handled, situation;

I can't accurately determine if the snap-scrolling action is complete.

I can set a setTimeout on each scroll, which cancels itself and re-sets - effectively debouncing - and finally does get called if not reset. But the timeout used when setting this, can mean you are 'too late' when determining scrolling is done.

Bottom line: how do I check if scrolling is done, either because:

  1. The user has stopped scrolling, or;
  2. The scroller has reached its snapping point (scroll-snap-type is set)
question from:https://stackoverflow.com/questions/65952068/determine-if-a-snap-scroll-elements-snap-scrolling-event-is-complete

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

I have finally, definitively, solved this brainteaser. It was much more simple to solve than I originally thought. (Note: in my case it's for a horizontal scroller; change offsetWidth to offsetHeight and scrollLeft to scrollTop in the example to adapt for a vertical scroller.)

function scrollHandler(e) {
    var scrollPosition  = Math.round(e.target.scrollLeft * devicePixelRatio);
    var elementWidth    = Math.round(e.target.offsetWidth * devicePixelRatio);
    var atSnappingPoint = scrollPosition % elementWidth === 0;
    var timeOut         = atSnappingPoint ? 0 : 150; //see notes

    clearTimeout(e.target.scrollTimeout);
    e.target.scrollTimeout = setTimeout(function() {
        console.log('Scrolling is done!');
    }, timeOut);
}

myElement.addEventListener('scroll', scrollHandler);

Breakdown

By using the scrolling element's own width (or height in case of vertical scroll) we can calculate if it has reached its snapping point by dividing the element's scrollposition (scrollLeft in my case) by its width (offsetWidth), and if that produces a round integer (meaning: the width 'fits' the scrolling position exactly x times) it has reached the snapping point. We do this by using the remainder operator:

var atSnappingPoint = scrollPosition % elementWidth === 0;

Then, if snapping point is reached, you set the timeOut (used in the setTimeout that should fire when scrolling has finished) to 0. Otherwise, the regular value is used (in the above example 150, see notes).

This works because when the element actually reaches its snapping point, one last scroll event is fired, and our handler is fired (again). Adjusting the timeOut to 0 will then instantly (see mdn) call our timeout function; so when the scroller 'hits' the snapping point, we know that instantaneously.


Notes

scrolling over snapping point I have not yet implemented/taken into account the fact that you can 'hit' the snapping point by scrolling over it. This is extremely rare, though. I will update the answer when I find a solution.

pixel ratio: Note that I take the device's pixel ratio into account. This is an important factor, which was the cause of an issue that took me some time to find out: It messes up the calculation of both the scrolling position and the offsetWidth calculation, so we need to 'revert' back to the values at ratio 1, by multiplying these values by the pixelratio. Important: turns out pixel ratio does not only indicate if the user has a high-dpi screen, but it also changes when the user has zoomed the page.

timeout the arbirtrary timeOut used when scrolling has not yet reached snapping point is at 150. This is long enough to prevent it being fired before Safari @ iOS is done scrolling (it uses a bezier curve for scroll snapping, which produces a very long 'last frame' of around 120-130ms) and short enough to produce an acceptible result when the user pauses scrolling in between snapping points.

scroll-padding if you have set scroll-padding on the scroll element, you will need to take that into account when determining the snapping point.

pixels remaining: You could even break things down further, to calculate the pixels remaining before reaching snapping point:

var pxRemain = scrollPosition % elementWidth;
var atSnappingPoint = pxRemain === 0;

But note that you will need to subtract that from the element's width, depending on which way you are scrolling. This requires you to calculate the distance scrolled, and checking if that is negative or positive. Then it would become:

var pxRemain = scrollPosition % elementWidth;
    pxRemain = (pxRemain === 0) ? 0 : ((distance > 0) ? pxRemain : elementWidth - pxRemain);
var atSnappingPoint = pxRemain === 0;

Only snapping

This script is written so it takes into account two situations:

  1. the element has snapped to its snapping point, or;
  2. the user has paused scrolling (or snapping detection has somehow gone wrong)

If you only need the former, you don't need the timeout, and you can just write:

function scrollHandler(e) {
  var scrollPosition  = Math.round(e.target.scrollLeft * devicePixelRatio);
  var elementWidth    = Math.round(e.target.offsetWidth * devicePixelRatio);
  if (scrollPosition % elementWidth === 0) {
    console.log('Scrolling is done!');
  }
}

myElement.addEventListener('scroll', scrollHandler);

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...