确定 snap-scroll 元素的 snap scrolling 事件是否完成
Determine if a snap-scroll element's snap scrolling event is complete
摘要
我正在使用可滚动元素创建图片库。我正在使用 CSS' scroll-snap
功能,它允许我捕捉到滚动条中的元素(图像)。
通过绑定到元素的 scroll
事件,我在用户滚动元素时应用各种操作(如预加载、隐藏界面元素等)。其中之一取决于滚动事件,需要在 确切时刻 滚动完成时停止。但是滚动捕捉给我带来了一种无法预料但尚未处理的情况;
我无法准确判断快照滚动动作是否完成。
我可以在每个滚动条上设置一个 setTimeout
,它会自行取消并重新设置 - 有效地消除抖动 - 如果不重置,最终会被调用。但是设置此设置时使用的超时可能意味着您在确定滚动完成时 'too late'。
底线:我如何检查滚动是否完成,或者是因为:
- 用户已停止滚动,或者;
- 滚动条已到达其吸附点(已设置
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); //clear previous timeout
e.target.scrollTimeout = setTimeout(function() {
console.log('Scrolling stopped!');
}, timeOut);
}
myElement.addEventListener('scroll', scrollHandler);
细分
通过使用滚动元素自己的宽度(或垂直滚动时的高度),我们可以通过除以元素的滚动位置(scrollLeft
在我的例子中)通过它的宽度(offsetWidth
),如果产生 一个整数 (意思是:宽度 'fits' 滚动位置 正好 x 次) 它已经到达捕捉点。我们通过使用 remainder operator:
var atSnappingPoint = e.target.scrollLeft % e.target.offsetWidth === 0;
然后,如果到达对齐点,则将 timeOut
(在滚动完成时应触发的 setTimeout
中使用)设置为 0。否则,使用常规值(在上面的例子150,见注释)。
这是可行的,因为当元素实际到达其对齐点时,最后一个 scroll
事件被触发,我们的处理程序(再次)被触发。将 timeOut
调整为 0 将 立即 (see mdn) 调用我们的超时函数;所以当滚动条 'hits' 捕捉点时,我们立即知道。
演示
下面是一个工作示例:
function scrollHandler(e) {
var atSnappingPoint = e.target.scrollLeft % e.target.offsetWidth === 0;
var timeOut = atSnappingPoint ? 0 : 150; //see notes
clearTimeout(e.target.scrollTimeout); //clear previous timeout
e.target.scrollTimeout = setTimeout(function() {
//using the timeOut to evaluate scrolling state
if (!timeOut) {
console.log('Scroller snapped!');
} else {
console.log('User stopped scrolling.');
}
}, timeOut);
}
myElement = document.getElementById('scroller');
myElement.addEventListener('scroll', scrollHandler);
.scroller {
display: block;
width: 400px;
height: 100px;
overflow-scrolling: touch;
-webkit-overflow-scrolling: touch;
overflow-anchor: none;
overflow-x: scroll;
overflow-y: hidden;
scroll-snap-type: x mandatory;
scroll-snap-stop: normal;
scroll-behavior: auto;
}
.scroller-canvas {
position: relative;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
}
.scroller-canvas > * {
position: relative;
display: inline-flex;
flex-flow: column;
flex-basis: 100%;
flex-shrink: 0;
width: 400px;
height: 100px;
scroll-snap-align: start;
scroll-snap-stop: normal;
}
.scroller-canvas > *:nth-child(even) {
background-color: #666;
color: #FFF;
}
/* Whosebug code wrapper fix */
.as-console-wrapper { max-height: 50px !important; }
<div class="scroller" id="scroller">
<div class="scroller-canvas">
<div class="slide" id="0">Slide 1</div>
<div class="slide" id="1">Slide 2</div>
<div class="slide" id="2">Slide 3</div>
<div class="slide" id="3">Slide 4</div>
<div class="slide" id="4">Slide 5</div>
</div>
</div>
备注
在捕捉点上滚动 我还没有implemented/taken考虑到您可以通过在捕捉点上滚动来'hit'这个事实。不过,这种情况极为罕见。当我找到解决方案时,我会更新答案。
像素比:如果你搞砸了计算(1 px 的余数等,所以函数没有正确解析),你可能有一些 scaling/box-model搞乱滚动位置和 offsetWidth 计算的问题。
这有可能是由设备的像素比引起的,因此您可以尝试 'correct' 值(向左滚动和宽度)乘以像素比。
重要提示:事实证明,像素比率不仅表明用户是否拥有高 dpi 屏幕,而且在用户缩放页面时也会发生变化。
timeout 滚动还没有到达捕捉点时使用的任意 timeOut
是在 150。这个时间足够长以防止它在 Safari @ iOS 完成滚动(它使用贝塞尔曲线进行滚动捕捉,这会产生大约 120-130 毫秒的非常长的 'last frame')并且当用户在捕捉点之间暂停滚动时足够短以产生可接受的结果。
scroll-padding如果你在scroll元素上设置了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;
仅捕捉
编写此脚本考虑了两种情况:
- 元素已对齐到其对齐点,或者;
- 用户已暂停滚动(或捕捉检测出了问题)
如果只需要前者,不需要timeout,直接写:
function scrollHandler(e) {
if (e.target.scrollLeft % e.target.offsetWidth === 0) {
console.log('Scrolling is done!');
}
}
myElement.addEventListener('scroll', scrollHandler);
摘要
我正在使用可滚动元素创建图片库。我正在使用 CSS' scroll-snap
功能,它允许我捕捉到滚动条中的元素(图像)。
通过绑定到元素的 scroll
事件,我在用户滚动元素时应用各种操作(如预加载、隐藏界面元素等)。其中之一取决于滚动事件,需要在 确切时刻 滚动完成时停止。但是滚动捕捉给我带来了一种无法预料但尚未处理的情况;
我无法准确判断快照滚动动作是否完成。
我可以在每个滚动条上设置一个 setTimeout
,它会自行取消并重新设置 - 有效地消除抖动 - 如果不重置,最终会被调用。但是设置此设置时使用的超时可能意味着您在确定滚动完成时 'too late'。
底线:我如何检查滚动是否完成,或者是因为:
- 用户已停止滚动,或者;
- 滚动条已到达其吸附点(已设置
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); //clear previous timeout
e.target.scrollTimeout = setTimeout(function() {
console.log('Scrolling stopped!');
}, timeOut);
}
myElement.addEventListener('scroll', scrollHandler);
细分
通过使用滚动元素自己的宽度(或垂直滚动时的高度),我们可以通过除以元素的滚动位置(scrollLeft
在我的例子中)通过它的宽度(offsetWidth
),如果产生 一个整数 (意思是:宽度 'fits' 滚动位置 正好 x 次) 它已经到达捕捉点。我们通过使用 remainder operator:
var atSnappingPoint = e.target.scrollLeft % e.target.offsetWidth === 0;
然后,如果到达对齐点,则将 timeOut
(在滚动完成时应触发的 setTimeout
中使用)设置为 0。否则,使用常规值(在上面的例子150,见注释)。
这是可行的,因为当元素实际到达其对齐点时,最后一个 scroll
事件被触发,我们的处理程序(再次)被触发。将 timeOut
调整为 0 将 立即 (see mdn) 调用我们的超时函数;所以当滚动条 'hits' 捕捉点时,我们立即知道。
演示
下面是一个工作示例:
function scrollHandler(e) {
var atSnappingPoint = e.target.scrollLeft % e.target.offsetWidth === 0;
var timeOut = atSnappingPoint ? 0 : 150; //see notes
clearTimeout(e.target.scrollTimeout); //clear previous timeout
e.target.scrollTimeout = setTimeout(function() {
//using the timeOut to evaluate scrolling state
if (!timeOut) {
console.log('Scroller snapped!');
} else {
console.log('User stopped scrolling.');
}
}, timeOut);
}
myElement = document.getElementById('scroller');
myElement.addEventListener('scroll', scrollHandler);
.scroller {
display: block;
width: 400px;
height: 100px;
overflow-scrolling: touch;
-webkit-overflow-scrolling: touch;
overflow-anchor: none;
overflow-x: scroll;
overflow-y: hidden;
scroll-snap-type: x mandatory;
scroll-snap-stop: normal;
scroll-behavior: auto;
}
.scroller-canvas {
position: relative;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
}
.scroller-canvas > * {
position: relative;
display: inline-flex;
flex-flow: column;
flex-basis: 100%;
flex-shrink: 0;
width: 400px;
height: 100px;
scroll-snap-align: start;
scroll-snap-stop: normal;
}
.scroller-canvas > *:nth-child(even) {
background-color: #666;
color: #FFF;
}
/* Whosebug code wrapper fix */
.as-console-wrapper { max-height: 50px !important; }
<div class="scroller" id="scroller">
<div class="scroller-canvas">
<div class="slide" id="0">Slide 1</div>
<div class="slide" id="1">Slide 2</div>
<div class="slide" id="2">Slide 3</div>
<div class="slide" id="3">Slide 4</div>
<div class="slide" id="4">Slide 5</div>
</div>
</div>
备注
在捕捉点上滚动 我还没有implemented/taken考虑到您可以通过在捕捉点上滚动来'hit'这个事实。不过,这种情况极为罕见。当我找到解决方案时,我会更新答案。
像素比:如果你搞砸了计算(1 px 的余数等,所以函数没有正确解析),你可能有一些 scaling/box-model搞乱滚动位置和 offsetWidth 计算的问题。 这有可能是由设备的像素比引起的,因此您可以尝试 'correct' 值(向左滚动和宽度)乘以像素比。 重要提示:事实证明,像素比率不仅表明用户是否拥有高 dpi 屏幕,而且在用户缩放页面时也会发生变化。
timeout 滚动还没有到达捕捉点时使用的任意 timeOut
是在 150。这个时间足够长以防止它在 Safari @ iOS 完成滚动(它使用贝塞尔曲线进行滚动捕捉,这会产生大约 120-130 毫秒的非常长的 'last frame')并且当用户在捕捉点之间暂停滚动时足够短以产生可接受的结果。
scroll-padding如果你在scroll元素上设置了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;
仅捕捉
编写此脚本考虑了两种情况:
- 元素已对齐到其对齐点,或者;
- 用户已暂停滚动(或捕捉检测出了问题)
如果只需要前者,不需要timeout,直接写:
function scrollHandler(e) {
if (e.target.scrollLeft % e.target.offsetWidth === 0) {
console.log('Scrolling is done!');
}
}
myElement.addEventListener('scroll', scrollHandler);