zoukankan      html  css  js  c++  java
  • 123.移动端滚动穿透完美解决方案加原理描述

    引言:这是个令人头疼并且及其常见的体验问题。

    所谓滚动穿透,指我们滑动顶层的弹窗,但效果上却滑动了底层的内容。


    什么情况会有该问题?

    出现该问题的大前提:

    • 整个webapp是设置为可以滚动的。

      例如:vue-cli中包裹的最外层html/body没有设置height:100%;overflow:hidden;

    • 在手机上打开页面。(chrome上观测不到!!!高维世界?)

      !!!Chrome的移动端调试模拟器是看不见任何问题的


    本文提供的解决案例的框架为vue-cli,若您使用原生或者react也不要紧,原理是一模一样的。

    具体原理分析如下:

    • 1、改变顶层:从穿透的思路考虑,如果顶层不会穿透过去,就解决了。
    • 2、改变底层:既然是顶层影响了底层,要是底层不会滚动,就解决了。

    明白了以上的两种原理,其实就很好解决了。
    明白了以上的两种原理,其实就很好解决了。
    明白了以上的两种原理,其实就很好解决了。

    有问题的原始代码和bug展示

    代码如下:

    <template>
      <div class="wrap">
        <div class="main">
          <button @click="showDialog">出现吧弹窗</button>
        </div>
        <div class="dialog-wrapper" v-if="visible" @click="closeDialog">
          <div class="dialog-content" @click.stop>
            <header @click="closeDialog">隐藏弹窗</header>
            <ul>
              <li>一个元素,不需要滚动</li>
            </ul>
          </div>
        </div>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          visible: false,
        };
      },
      created() {},
      mounted() {},
      methods: {
        showDialog() {
          this.visible = true
        },
        closeDialog() {
          this.visible = false
        }
      }
    };
    </script>
    
    <style scoped lang="less">
    .wrap {
       100%;
      height: 100%;
      background: #088a9e;
      overflow: scroll;
      border: 5px solid #089e8a;
    
      .main {
         100%;
        height: 100%;
        background: #eee;
      }
    
      .dialog-wrapper {
        position: fixed;
        top: 0;
        left: 0;
        bottom: 0;
        right: 0;
        background: rgba(0, 0, 0, .5);
        .dialog-content {
          position: fixed;
          bottom: 0;
          left: 0;
           100%;
          height: 300px;
          overflow: scroll;
          background: seagreen;
    
          li {
            margin-top: 10px;
             100%;
            height: 150px;
            background: khaki;
          }
        }
      }
    }
    </style>
    

    bug效果如下:

    情况一、若顶层弹窗本身不需要滚动(这种情况较为简单)

    如果弹窗本身不需要滑动,那是非常简单的。

    方法A、从让顶部不穿透的考虑触发,我们可以这么作修改

    直接修改顶层弹窗的div,设置 @touchmove.prevent 即可.

    <div class="dialog-wrapper" @touchmove.prevent v-if="visible" @click="visible = false">
    

    实现后的效果如下:


    方法B、从让底层不能滚动的考虑触发,我们可以这么作修改

    我们在弹窗出现的时候,临时不让底部可以滚动;在弹窗消失的时候,再把底部可以滚动的功能加回去。

    这里我们使用添加类名,使得底层临时不能滑动解决。
    类名里面我们利用设置了position:fixed;不会随屏幕滚动的原理。

    css添加如下

    .dialog-open {
      position: fixed;
    }
    

    vue中给滚动的元素加上ref便于获取,再加上两个method用于添加类名/删除类名。问题解决

    这里还是放出完整代码。

    <template>
      <div class="wrap" ref="scrollEl">
        <div class="main">
          <button @click="showDialog">出现吧弹窗</button>
        </div>
        <div class="dialog-wrapper" v-if="visible" @click="closeDialog">
          <div class="dialog-content" @click.stop>
            <header @click="closeDialog">隐藏弹窗</header>
            <ul>
              <li>一个元素,不需要滚动</li>
            </ul>
          </div>
        </div>
    
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          visible: false,
        };
      },
      created() {},
      mounted() {},
      methods: {
        showDialog() {
          this.visible = true
          this.afterDialogOpen()
        },
        closeDialog() {
          this.visible = false
          this.afterDialogClose()
        },
        afterDialogOpen() {
          this.$refs.scrollEl.classList.add('dialog-open')
        },
        afterDialogClose() {
          this.$refs.scrollEl.classList.remove('dialog-open')
        }
      }
    };
    </script>
    
    <style scoped lang="less">
    .wrap {
       100%;
      height: 100%;
      background: #088a9e;
      overflow: scroll;
      border: 5px solid #089e8a;
    
      .main {
         100%;
        height: 100%;
        background: #eee;
      }
    
      .dialog-wrapper {
        position: fixed;
        top: 0;
        left: 0;
        bottom: 0;
        right: 0;
        background: rgba(0, 0, 0, .5);
        .dialog-content {
          position: fixed;
          bottom: 0;
          left: 0;
           100%;
          height: 300px;
          overflow: scroll;
          background: seagreen;
    
          li {
            margin-top: 10px;
             100%;
            height: 150px;
            background: khaki;
          }
        }
      }
    }
    .dialog-open {
      position: fixed;
    }
    </style>
    

    效果其实基本一样。

    情况二、若弹窗本身需要滚动

    我们修改本文最上方的(未解决穿透时)的原始代码结构,仅添加多个li

    <ul>
      <li>很多元素,需要滚动</li>
      <li>很多元素,需要滚动</li>
      <li>很多元素,需要滚动</li>
      <li>很多元素,需要滚动</li>
      <li>很多元素,需要滚动</li>
    </ul>
    

    重现了穿透问题。
    并且这里直接对父级采用 touchmove.prevent 是不可行的,因为弹窗本身需要滚动,若使用了,本身也滚不了了。

    bug效果如下:


    显然这里我们不能完全照抄情况一的方法A。否则整块元素都划不动了。

    父级设置touchmove.prevent,其内的元素也是会受到影响的。

    但解决原理是一样的。
    但解决原理是一样的。
    但解决原理是一样的。

    既然父级会影响,那我搞个同级不就好了吗!
    如下:我们多添加一层元素设为touchmove.prevent,同级的元素是不会影响的,利用z-index区分开来。

    方法C:还是从改变顶层元素不让穿透的思想解决

    方法A的优化升级版

    下面是我们的部分修改方案(期间我们还会遇见一个问题,关于 touchomove 和 click 的问题)

    <template>
      <div class="wrap">
        <div class="main">
          <button @click="showDialog">出现吧弹窗</button>
        </div>
        <div class="dialog-wrapper" v-if="visible" @click="closeDialog">
          <div class="dialog-no-touch-area" @touchmove.prevent.stop>
          </div>
          <div class="dialog-content" touchmove.stop @click.stop>
            <header @click="closeDialog">隐藏弹窗</header>
            <ul>
              <li>很多元素,需要滚动</li>
              <li>很多元素,需要滚动</li>
              <li>很多元素,需要滚动</li>
              <li>很多元素,需要滚动</li>
              <li>很多元素,需要滚动</li>
            </ul>
          </div>
        </div>
    
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          visible: false,
        };
      },
      created() {},
      mounted() {},
      methods: {
        showDialog() {
          this.visible = true
        },
        closeDialog() {
          this.visible = false
        }
      }
    };
    </script>
    
    <style scoped lang="less">
    .wrap {
       100%;
      height: 100%;
      background: #088a9e;
      overflow: scroll;
      border: 5px solid #089e8a;
    
      .main {
         100%;
        height: 100%;
        background: #eee;
      }
    
      .dialog-wrapper {
        position: fixed;
        top: 0;
        left: 0;
        bottom: 0;
        right: 0;
        background: rgba(0, 0, 0, .5);
        .dialog-no-touch-area {
          z-index: 100;
          position: fixed;
           100%;
          height: 100%;
        }
        .dialog-content {
          z-index: 200;
          position: fixed;
          bottom: 0;
          left: 0;
           100%;
          height: 300px;
          overflow: scroll;
          background: seagreen;
    
          li {
            margin-top: 10px;
             100%;
            height: 150px;
            background: khaki;
          }
        }
      }
    }
    </style>
    

    但是你最后会发现一个 bug:在我们滑动灰色遮罩的部分的时候,我们发现触发了click事件,但是我们想要区分touchmove连携的click正常的click

    究其原因是:

    在移动端,手指点击一个元素,会经过:touchstart --> touchmove -> touchend --> click

    解决方案关键在于区分 click 事件和 touchmove 事件。

    这里提供我的方法(借鉴于某个阅读器项目的代码),网上的方法没有找到合适的。

    代码如下:

    <template>
      <div class="wrap">
        <div class="main">
          <button @click="showDialog">出现吧弹窗</button>
        </div>
        <div class="dialog-wrapper" v-if="visible" @click="closeDialog" @tap="closeDialog">
          <div class="dialog-no-touch-area"
            @touchstart="touchStart"
            @touchend="touchEnd"
            @touchmove.prevent.stop>
          </div>
          <div class="dialog-content" touchmove.stop @click.stop>
            <header @click="closeDialog">隐藏弹窗</header>
            <ul>
              <li>很多元素,需要滚动</li>
              <li>很多元素,需要滚动</li>
              <li>很多元素,需要滚动</li>
              <li>很多元素,需要滚动</li>
              <li>很多元素,需要滚动</li>
            </ul>
          </div>
        </div>
    
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          visible: false,
          // 用于检测是否是移动事件,通过间隔时间、间隔距离进行判断
          touchStartX: 0,
          touchStartTime: 0,
        };
      },
      created() {},
      mounted() {},
      methods: {
        showDialog() {
          this.visible = true
        },
        closeDialog() {
          console.log('click or tap')
          this.visible = false
        },
        touchStart($event) {
          this.touchStartX = $event.changedTouches[0].clientX
          // this.touchStartTime = $event.timeStamp
        },
        touchEnd($event) {
          const offsetX = $event.changedTouches[0].clientX - this.touchStartX
          // const diffTime = $event.timeStamp - this.touchStartTime
          // alert('差距时间' + diffTime)
          // 判断什么情况下是touchMove,什么情况是click
          if (Math.abs(offsetX) >= 20) {
            $event.preventDefault()
            $event.stopPropagation()
          }
        }
      }
    };
    </script>
    
    <style scoped lang="less">
    .wrap {
       100%;
      height: 100%;
      background: #088a9e;
      overflow: scroll;
      border: 5px solid #089e8a;
    
      .main {
         100%;
        height: 100%;
        background: #eee;
      }
    
      .dialog-wrapper {
        position: fixed;
        top: 0;
        left: 0;
        bottom: 0;
        right: 0;
        background: rgba(0, 0, 0, .5);
        .dialog-no-touch-area {
          z-index: 100;
          position: fixed;
           100%;
          height: 100%;
        }
        .dialog-content {
          z-index: 200;
          position: fixed;
          bottom: 0;
          left: 0;
           100%;
          height: 300px;
          overflow: scroll;
          background: seagreen;
    
          li {
            margin-top: 10px;
             100%;
            height: 150px;
            background: khaki;
          }
        }
      }
    }
    </style>
    
    

    完美解决。

    方法D:按照改变底层元素的思想解决

    我本以为方法B的代码,是可以通用在两种情况的,但经过几次测试发现并不行。

    需要小小的改动一下。

    其中会有一个问题,感觉就是聚焦的问题,当滑动了遮罩的部分,浏览器就聚焦在遮罩层了。
    这个时候需要再聚焦回来才能流畅地滑动。

    那么怎么解决呢!!!请看下方鄙人表演一个四两拨千斤

    .dialog-wrapper {
      touch-action: none;
    }
    

    给它的遮罩结构的类添加一个禁用浏览器所有平移、缩放手势的属性。并且查看MDN文档后,发现它不会被继承————即是说不会影响到我们需要滑动的子级。

    这样就不存在上方说的什么聚焦(只是我的说法)了,它压根没法被触碰。

    其他代码同方法B。仅多一句css。

    完美解决。

    参考文案

    HTML DOM addEventListener() 方法
    (MDN解释touch-action)[https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action]


    compete.

  • 相关阅读:
    【codecombat】 试玩全攻略 第二章 边远地区的森林 一步错
    【codecombat】 试玩全攻略 第十八关 最后的kithman族
    【codecombat】 试玩全攻略 第二章 边远地区的森林 woodlang cubbies
    【codecombat】 试玩全攻略 第二章 边远地区的森林 羊肠小道
    【codecombat】 试玩全攻略 第十七关 混乱的梦境
    【codecombat】 试玩全攻略 第二章 边远地区的森林 林中的死亡回避
    【codecombat】 试玩全攻略 特别关:kithguard斗殴
    【codecombat】 试玩全攻略 第二章 边远地区的森林 森林保卫战
    【codecombat】 试玩全攻略 第二章 边远地区的森林
    实验3 类和对象||
  • 原文地址:https://www.cnblogs.com/can-i-do/p/11837527.html
Copyright © 2011-2022 走看看