确定 snap-scroll 元素的 snap scrolling 事件是否完成

问题描述

摘要

我正在使用可滚动元素创建图片库。我正在使用 CSS 的 scroll-snap 功能,它允许我捕捉到滚动条中的元素(图像)。

通过绑定到元素的 scroll 事件,我在用户滚动元素时应用各种操作(比如预加载、隐藏界面元素等)。其中之一取决于滚动事件,需要在确切时刻滚动完成时停止。但是滚动捕捉给我带来了一个无法预见但尚未处理的情况;

我无法准确确定快速滚动操作是否完成。

我可以在每个滚动条上设置一个 setTimeout,它会自行取消并重新设置 - 有效地消除抖动 - 如果没有重置,最终会被调用。但是设置此设置时使用的超时可能意味着您在确定滚动完成时“为时已晚”。

底线:我如何检查滚动是否完成,要么是因为:

  1. 用户已停止滚动,或者;
  2. 滚动条已达到其捕捉点(scroll-snap-type 已设置)

解决方法

我终于明确地解决了这个脑筋急转弯。解决起来比我原先想象的要简单得多。 (注意:在我的例子中,它用于水平滚动条;在示例中将 offsetWidth 更改为 offsetHeight 并将 scrollLeft 更改为 scrollTop 以适应垂直滚动条。)

function scrollHandler(e) {
    var atSnappingPoint = e.target.scrollLeft % e.target.offsetWidth === 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);

故障

通过使用滚动元素自己的宽度(或垂直滚动​​时的高度),我们可以通过除以元素的滚动位置(在我的情况下为 scrollLeft ) 的宽度 (offsetWidth),如果它产生一个整数(意思是:宽度“适合”滚动位置正好 x 次),它有达到了捕捉点。我们通过使用 remainder operator:

var atSnappingPoint = e.target.scrollLeft % e.target.offsetWidth === 0;

然后,如果到达捕捉点,则将 timeOut(在滚动完成时应触发的 setTimeout 中使用)设置为 0。否则,使用常规值(在上面示例 150,请参阅注释)。

这是有效的,因为当元素真正到达它的捕捉点时,最后一个 scroll 事件被触发,我们的处理程序被触发(再次)。将 timeOut 调整为 0 将立即 (see mdn) 调用我们的超时函数;因此,当滚动条“击中”捕捉点时,我们会立即知道这一点。


注意事项

滚动捕捉点 我还没有实现/考虑到您可以通过滚动捕捉点来“击中”捕捉点这一事实。不过,这是极其罕见的。当我找到解决方案时,我会更新答案。

像素比:如果计算混乱(剩余 1 像素等,因此函数无法正确解析),您可能会遇到一些缩放/盒模型问题,从而使计算混乱计算滚动位置和 offsetWidth 计算。 这是由设备的像素比引起的可能性很小,因此您可以尝试通过将这些值乘以像素比来“校正”这些值(scrollleft 和 width)。 重要提示:像素比不仅表明用户是否拥有高 dpi 屏幕,而且在用户缩放页面时它也会改变

timeout 滚动尚未到达捕捉点时使用的任意 timeOut 为 150。这足够长以防止它在 Safari @ iOS 完成滚动之前被触发(它使用用于滚动捕捉的贝塞尔曲线,它产生大约 120-130 毫秒的非常长的“最后一帧”)并且足够短以在用户在捕捉点之间暂停滚动时产生可接受的结果。

scroll-padding 如果您在滚动元素上设置了 scroll-padding,则在确定捕捉点时需要考虑这一点。

剩余像素:您甚至可以进一步细分,以计算到达捕捉点之前剩余的像素:

var pxRemain = e.target.scrollLeft % e.target.offsetWidth;
var atSnappingPoint = pxRemain === 0;

但请注意,您需要从元素的宽度中减去它,这取决于您滚动的方式。这需要您计算滚动的距离,并检查它是负数还是正数。然后它会变成:

var pxRemain = e.target.scrollLeft % e.target.offsetWidth;
    pxRemain = (pxRemain === 0) ? 0 : ((distance > 0) ? pxRemain : elementWidth - pxRemain);
var atSnappingPoint = pxRemain === 0;

仅捕捉

编写此脚本是为了考虑两种情况:

  1. 元素已捕捉到其捕捉点,或者;
  2. 用户已暂停滚动(或捕捉检测不知何故出错)

如果只需要前者,则不需要超时,直接写:

function scrollHandler(e) {
  if (e.target.scrollLeft % e.target.offsetWidth === 0) {
    console.log('Scrolling is done!');
  }
}

myElement.addEventListener('scroll',scrollHandler);