阻止用户一次滚动超过 1 个元素

Stop user from scrolling more than 1 element at once

问题

基本上我有一个有一些视频的移动网站。每个视频占用 100% 宽度和 100% 高度。我正在尝试制作一个滚动捕捉类型的提要,用户一次只能滚动一个视频,如果他们在滚动时到达页面上的某个点,它将捕捉到下一个视频。这有点像 tiktok 的视频提要或 instagram 卷轴作为一个想法。

我正在使用 scroll-snap 包,它有点让我达到了我想要实现的目标。缓慢滚动时它会卡住,但是如果我在移动设备上,我可以非常快速地滚动并允许滚动势头跳过视频。我正在努力做到这一点,以便用户无论滚动多用力,一次只能滚动 1 个视频。

这是正在发生的事情:https://streamable.com/f98slq。如您所见,我一次可以滚动超过 1 个视频。

我试过的

我试图在滚动提要时获取滚动速度。如果它高于某个值,我会应用一种停止页面滚动 10 毫秒的样式。这并没有真正起作用,因为即使滚动速度不高,滚动动量也能够滚动页面。不过我不太喜欢这个解决方案。

我不太确定解决此问题的最佳方法。如有任何反馈,我们将不胜感激。

这是我的代码:

    // Scroll snap
    useEffect(() => {
        const { unbind } = createScrollSnap(MobileSnapContainer.current!, {
            snapDestinationX: '0%',
            snapDestinationY: '100%',
            timeout: 0,
            duration: 100,
            threshold: 0.1,
            snapStop: true,
        }, () => {})

        return () => unbind()
    }, [])

    return (
    <>        
        <InfiniteScroll
                dataLength={mobileVideos.length}
                next={loadMore}
                hasMore={hasMore}
                loader={null}
                scrollableTarget="container"
            >
                <div 
                    className="overflow-auto select-none hide-scrollbar" 
                    id="container" 
                    ref={MobileSnapContainer}
                >
                    {mobileVideos.map((data: video, index: number) => (
                        <SingleMobileVideo 
                          index={index}
                        />
                    ))}  
                </div>
            </InfiniteScroll>
      </>

一种策略是检查某些元素的位置通过 运行ge 触发 滚动冻结 .

因此在滚动时,如果滚动的像素数量或多或少接近这些元素之一的位置,您可以将其冻结一段时间。

在开始我的演示之前,这里有四件事:

  1. 如何冻结卷轴?
  2. 为什么 运行ge?
  3. 什么时候解冻它?
  4. 一些注意事项...

如何冻结卷轴?

很简单...您可以在每次滚动事件中将 scrollTop 设置为固定值。

let freezed = true;
let noScrollElement = document.querySelector(".outer");
noScrollElement.scrollTop = 2920;

noScrollElement.addEventListener("scroll", function(e) {
  if (freezed) {
    e.target.scrollTop = 2920;
  }
})

document.querySelector("button").addEventListener("click", function(e) {
  freezed = false;
})
.outer {
  height: 300px;
  overflow: scroll;
}

.space {
  height: 3000px;
}
<div class="outer">
  <div class="space"></div>
  <div>
    No! You just can't scroll me! <button>Unfreeze</button>
  </div>
  <div class="space"></div>
</div>

为什么是 运行ge?

因为 scrollTop 值不会完全匹配元素的位置。所以你必须评估它周围的一定数量的像素。它估计 +/- 16px 即使鼠标滚轮 正常快速 旋转也很好。这使得 运行ge 为 32px。由你来调整它。 异常快速旋转几乎没有任何关系。

什么时候解冻?

你必须! .o(lol) 它可以在一定的延迟之后(就像我在我的演示中所做的那样)或者在视频播放一定时间之后。您必须查看视频 currentTime

一些注意事项...

虽然编写代码可能很有趣...您的用户很可能会非常不喜欢。所以不要滥用冻结时间。它应该是微妙的。

它最有趣的用例是关于超快速滚动...就像在移动设备上做一个快速向上滑动的手势,它可以在一秒钟内滚动 10 公里的页面。但遗憾的是,这种解决方案在这种情况下并不可靠。

下面是我演示的相关部分

可以在 replit

上 运行
// Some globals
let snapPrecision = 16;  // pixels
let unsnapTime = 5000;  // milliseconds
let freezed = false;    // boolean to know if the scrolling is freezed
let freezedAt = 0;      // Pixels where it is frozen
let lastSnaped = 0;     // index of the sanppable element

// Have all position of the divs with className snap in state
const [snapPos, snapPosSetter] = React.useState([])

// If the page updates, like new elements appear,
// Update the positions array in the state
React.useEffect(() => {
  // Get the position of the snap elements
  let snapPos = [];
  Array.from(document.querySelectorAll(".snap")).forEach(s => snapPos.push(s.getBoundingClientRect().top));

  // Update the state
  snapPosSetter(snapPos)
}, [])

// The scroll handler
function scrollHandler(e){
  // console.log(e.target.scrollTop)

  // Scrolled position
  let scrolled = parseInt(e.target.scrollTop);

  // If not freezed, check if there is a match between
  // the scrolled position and all the snap items in state
  if (!freezed) {

    // If the scrolled position is withing +/- snapPrecision
    // and not the last snapped
    for (let i = 0; i < snapPos.length; i++) {
      if (
        scrolled + snapPrecision > snapPos[i] &&
        scrolled - snapPrecision < snapPos[i] &&
        lastSnaped != i
      ) {
        freezed = true;
        freezedAt = snapPos[i];
        lastSnaped = i;

        // Unsnap it in unsnapTime (milliseconds)
        setTimeout(function () {
          console.log("unfreezed");
          freezed = false;
        }, unsnapTime);

        break;
      }
    }
  }

  // This is the part that really freezes the scroll
  if (freezed) {
    console.clear();
    console.log("FREEZED at ", freezedAt);

    // Force the scroll at a specific position!
    e.target.scrollTop = freezedAt;
  }
}