【经验】移动端滚动穿透

5,892 阅读3分钟

背景

在项目中我们经常会遇到滚动穿透的问题如下:

弹窗首先有一个遮罩层,然后遮罩层中间有一块内容区域,内容区域里边有一部分内容也可以滚动

问题代码

  <div class="wrapper">
        <img src="./static/background.png" class="background">
        <div class="popup">
            <div class="mask"></div>
            <div class="content">
                页面内容页面内容页面内容页面内容页面内容页面内容页面内容页面内容页面内容页面内容页面内容
            </div>
        </div>
    </div>

其中图片的高度是超过一屏幕的,意思说可以产生滚动

.popup {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  top: 0;
}

.mask {
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, .5);
}

.content {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  background: #fff;
  width: 60%;
  height: 200px;
  border-radius: 3px;
  box-sizing: border-box;
  padding: 10px;
  overflow: auto;
}

遮罩层滚动穿透

可以看到在滑动遮罩层的时候,底部内容也可以滚动

中间内容滚动穿透

当我们给中间的框加了一个固定的高度,但是中间内容超过了框的高度。我们试图滚动中间内容的时候就会出现以下的情况:

可以看到这么一个行为:正常的滚动时候没问题,但是一旦滚动到页面底部的时候,我们继续滚动就发现背景页面也发生了滚动

解决方案

使用overflow: hidden

当这个弹窗出现的时候就让body设置overflow:hidden,高度设置为height: 100vh; 弹窗消失的时候在这个样式改回去伪代码如下:

// 表示弹窗打开
popup.addEventListener('show', () => {
    body.style.overflow = 'hidden';
    body.style.height = '100vh'
})
// 表示弹窗关闭
popup.addEventListene('close', () => {
    body.style.overflow = 'auto';
    body.style.height = 'auto'
})

这样就能避免产生滚动穿透,但是这种方案存在一个问题,就是当页面超过一屏幕的时候并且页面已经滚动了一部分距离,这时候当我们打开弹窗的时候,页面就滚动到了顶部,关闭弹窗的时候页面也就停留在了顶部,这种方式对用户来说不够友好,为什么刚才还在顶部,现在就滚动到了顶部呢。所以针对这种情况可以采用在打开弹窗的时候记住滚动的位置,然后在关闭的弹窗时候让页面滚动到先前的位置,伪代码如下:

let scrollTop;
// 表示弹窗打开
popup.addEventListener('show', () => {
    // 记住当前的滚动位置
    scrollTop = body.scrollTop;
    body.style.overflow = 'hidden';
    body.style.height = '100vh'
})
// 表示弹窗关闭
popup.addEventListene('close', () => {
    // 把当前的滚动位置设置为先前的scrollTop
    body.scrollTop = scrollTop;
    body.style.overflow = 'auto';
    body.style.height = 'auto'
})

移动端触摸事件

首先解决遮罩层的滚动,这个很简单只需要下面这张代码

$('.mask').on('touchmove', (e) => {
    e.preventDefault();
})

就是直接阻止遮罩层的touchmove事件。 然后是内容区域的滚动,首先进行行为分析,在上面我们可以看到,在内容区域滚动的时候,只要没有到达顶部或者底部的时候,滚动内容区域背景是不会跟着滚动的,但是当我们滚动到底部的时候继续向下滑动,页面就发生了滚动。根据这个行为大概就可以总结出方案,当页面滚动到底部或者顶部的时候组织他的默认滚动行为不就行了吗。 下面是他的具体代码

$('.content').on('touchstart', function (e) {
  const targetTouches = e.targetTouches || []
  if (targetTouches.length > 0) {
    const touch = targetTouches[0] || {};
    startY = touch.clientY;
  }
})
$('.content').on('touchmove', function(e) {
  const targetTouches = e.targetTouches;
  const scrollTop = this.scrollTop;
  if (targetTouches.length > 0) {
    const touch = targetTouches[0] || {};
    const moveY = touch.clientY;
    if (scrollTop === 0 && moveY > startY) {
      e.preventDefault();
    } else if (scrollTop === scrollHeight - offsetHeight && moveY < startY) {
      e.preventDefault();
    }
  }
})

思路很简单,就是监听touchstarttouchmove事件,判断是否滚动到顶部还有顶部。首先记录touchstart的时候手指距离顶部的距离,在移动的过程中在获取手相对屏幕的一个位置,通过这两个位置比较,判断现在是向上滑动还是向下滑动,从而判断是否到达了边界情况。

使用better-scroll

这个就比较简单了,使用第三方库他这个核心原理就是直接让滚动的元素设置overflow: hidden

然后内容区域超过容器区域,使用translate代替原生的scroll事件来进行滚动,所以需要注意的是打开弹窗时候better-scroll需要知道这个弹窗的内容区域的高度才能正确的滚动(那时候用这个总有小伙伴遇到这个问题)

上面就是常用的几种解决滚动弹窗的方法。个人比较常用的是第二种方法,1:相对第一种用户体验好;2: 相对第三种,不用引入额外的库。