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.

  • 相关阅读:
    20181113-2 每周例行报告
    20181030-4 每周例行报告
    20180925-5 代码规范,结对要求
    20181023-3 每周例行报告
    20181016-10 每周例行报告
    PSP总结报告
    作业要求 20181204-1 每周例行报告
    公开感谢
    附加作业 软件工程原则的应用实例分析
    作业要求 20181127-2每周例行报告
  • 原文地址:https://www.cnblogs.com/can-i-do/p/11837527.html
Copyright © 2011-2022 走看看