在作flutter内webview内嵌h5的时候,遇到了在ios下滑动页面会出现橡皮筋效果,而在android下不会出现该问题。这样的话在ios下滑动会意外的触发会感觉页面整体在上下弹动。

后面继续开发中发现次方案只能解决首次手势问题,比如手指一直没离开屏幕而先向上又向下依然会出现弹性滚动,在移动过程中不能中断,目前没找到更好的办法解决

方案1:在移动端h5中我们可以监听touchstart(触摸开始),touchmove(触摸移动),touchend(触摸结束)等触摸事件,来判断当前是否该滑动。
方案2: 若是引入的第三方滚动组件,大多数第三方组件都是使用transform来实现滚动的,所以只需要设置css: touch-action:none;则不会触发触摸。

我这里使用的webview自带的浏览器滚动条来实现的原生滚动,目前使用起还是比较顺手。

  • 思路
    最主要的就是我会递归去找我的滚动容器,看二级滚动容器是否大于一级滚动容器。若大于则肯定存在会出现滚动,若小于则在页面内并不需要滚动。
  • mixin一个原生滚动条(并隐藏滚动条)
// 局部使用浏览器自带滚动条
@mixin useScrollByBrowser($h: 1px) {
  overflow-x: hidden;
  overflow-y: auto;
  height: calc(100% - #{$h});
  &::-webkit-scrollbar {
    display: none;
  }
}
  • 全局封装一个函数式滚动组件
<template functional>
  <!-- 创建滚动区域 滚动一级-->
  <div class="area-scroll" data-scroll-root="true">
    <!-- 滚动二级 主要获取其高度与滚动一级比较 -->
    <div class="area-scroll-content">
      <slot></slot>
    </div>
  </div>
</template>
  • touchstart记录起始点
Touch._startX = e.touches[0].pageX;
Touch._startY = e.touches[0].pageY;
  • touchmove记录移动点位
    在touchmove中判断不在touchend中判断是为了短时间可以一直移动屏幕,而不抬起手指。
let endX = e.touches[0].pageX;
let endY = e.touches[0].pageY;
  • 判断手指上移还是下移
let distanceX = endX - Touch._startX;
let distanceY = endY - Touch._startY;

let finalDirction = 'none';
if (Math.abs(distanceX) < Math.abs(distanceY) && distanceY > 0) {
  finalDirction = 'down';
} else if (Math.abs(distanceX) < Math.abs(distanceY) && distanceY < 0) {
  finalDirction = 'up';
}
  • 临界点
    当滚动距离为0且手势往下 需禁止
if (curNodeParent.scrollTop === 0 && finalDirction === 'down') {
  // 在最顶层且往下滑动 禁止
  return false;
}

当滚动到底且手势往上 需禁止

if (
  Math.ceil(curNodeParent.clientHeight + curNodeParent.scrollTop) ==
    curNodeParent.scrollHeight &&
  finalDirction === 'up'
) {
  // 在最下面且往往上滑动 禁止
  return false;
}
  • 递归逻辑
    我会以当前点击位置起开始找寻它的parentNode并且判断当前parentNode是否是我的滚动组件的一级滚动容器,若不是则继续找寻,若是,则通过firstElementChild获取当前二级滚动容器的高度,并判断是否大于一级容器;
    还有一种情况就是当我页面内不存在滚动容器则此时若parentNode找到body标签就退出了。
_findParent: (pNode, finalDirction) => {
/**
 * @func 递归找上级函数
 * @param  父级元素node
 * @param  当前用户手势操作
 */
const curNodeParent = pNode.parentNode;
if (curNodeParent && curNodeParent.nodeName === 'BODY') {
  // 证明当前点击的元素不是滚动区域内的元素
  return false;
} else {
  // 判断当前父级元素是否为一级滚动区域
  if (
    curNodeParent &&
    curNodeParent.getAttribute('data-scroll-root') === 'true'
  ) {
    // 若找到当前父级元素为一级 则需要判断其二级滚动区域高度是否大于一级父级元素
    const curNodeParentHeight = curNodeParent.offsetHeight; // 当前父元素高度
    const curNodeParentChild = curNodeParent.firstElementChild || null; // 当前一级滚动下属二级滚动区域
    const curNodeParentChildHeight =
      curNodeParentChild && curNodeParentChild.offsetHeight; // 二级滚动区域高度
    if (curNodeParentChildHeight > curNodeParentHeight) {
      if (curNodeParent.scrollTop === 0 && finalDirction === 'down') {
        // 在最顶层且往下滑动 禁止
        return false;
      } else if (
        Math.ceil(curNodeParent.clientHeight + curNodeParent.scrollTop) ==
          curNodeParent.scrollHeight &&
        finalDirction === 'up'
      ) {
        // 在最下面且往往上滑动 禁止
        return false;
      } else {
        // 其余情况 可滚动
        return true;
      }
    }
  } else {
    // 未到body则继续往上遍历
    return Touch._findParent(curNodeParent, finalDirction);
  }
}
  • 使用
    在app.vue中引入事件监听
// 在顶层vue模板中引入触摸事件
import { onInitTouchEvent, onRemoveTouchEvent } from 'Utils/touch'
// script中
created() {
  onInitTouchEvent()
},
destroyed() {
  onRemoveTouchEvent()
}

页面内直接使用全局滚动组件

<!-- 局部滚动组件 -->
<area-scroll>
  <!-- 您的业务dom -->
  <ul>
    <li v-for="index in 124" :key="index">{{index}}</li>
  </ul>
</area-scroll>
// css
/deep/ .area-scroll {
 @include useScrollByBrowser(60px);
}

其实还可以优化一点就是针对ios才走逻辑若是安卓机则直接跳过,这样更合理些。

  • touch.js
/**
 *  @module 针对ios滑动带动webview触摸事件封装
 *  @author ljxin
 *  @tips Touch对象
 *        私有变量 startX,startY
 *        私有方法:_onMove,_onStart,_onEnd,_findParent
 *        暴露方法: onInitTouchEvent onRemoveTouchEvent
 */
const Touch = {
  _startX: 0, // 手指起始触摸位置x
  _startY: 0, // 手指起始触摸位置y
  _onMove: (e) => {
    /**
     * @func touchmove事件响应函数
     * @param  e 系统事件对象
     */
    let endX = e.touches[0].pageX;
    let endY = e.touches[0].pageY;

    let distanceX = endX - Touch._startX;
    let distanceY = endY - Touch._startY;

    let finalDirction = 'none';
    if (Math.abs(distanceX) < Math.abs(distanceY) && distanceY > 0) {
      finalDirction = 'down';
    } else if (Math.abs(distanceX) < Math.abs(distanceY) && distanceY < 0) {
      finalDirction = 'up';
    }

    // 1. 先判断当前vue页面下是否存在可滚动区域
    if (!!document.querySelector('.area-scroll')) {
      // 2. 再判断点击的该节点元素是否隶属于滚动区域
      const curNode = e.target;
      if (Touch._findParent(curNode, finalDirction)) {
        // console.log('可滚动');
        return null;
      }
    }
    // 在cancalable为true得时候 来阻止冒泡及默认事件 及一定要return 不然会报个错
    if (e.cancelable) {
      e.stopPropagation();
      e.preventDefault();
      // console.log('不能滚动');
    }
    return null;
  },
  _onStart: (e) => {
    /**
     * @func touchstart事件响应函数
     * @param  e
     */
    Touch._startX = e.touches[0].pageX;
    Touch._startY = e.touches[0].pageY;
  },
  _onEnd: (e) => {
    /**
     * @func touchend事件响应函数 (暂未使用)
     * @param  e
     */
  },
  _findParent: (pNode, finalDirction) => {
    /**
     * @func 递归找上级函数
     * @param  父级元素node
     * @param  当前用户手势操作
     */
    const curNodeParent = pNode.parentNode;
    if (curNodeParent && curNodeParent.nodeName === 'BODY') {
      // 证明当前点击的元素不是滚动区域内的元素
      return false;
    } else {
      // 判断当前父级元素是否为一级滚动区域
      if (
        curNodeParent &&
        curNodeParent.getAttribute('data-scroll-root') === 'true'
      ) {
        // 若找到当前父级元素为一级 则需要判断其二级滚动区域高度是否大于一级父级元素
        const curNodeParentHeight = curNodeParent.offsetHeight; // 当前父元素高度
        const curNodeParentChild = curNodeParent.firstElementChild || null; // 当前一级滚动下属二级滚动区域
        const curNodeParentChildHeight =
          curNodeParentChild && curNodeParentChild.offsetHeight; // 二级滚动区域高度
        if (curNodeParentChildHeight > curNodeParentHeight) {
          if (curNodeParent.scrollTop === 0 && finalDirction === 'down') {
            // 在最顶层且往下滑动 禁止
            return false;
          } else if (
            Math.ceil(curNodeParent.clientHeight + curNodeParent.scrollTop) ==
              curNodeParent.scrollHeight &&
            finalDirction === 'up'
          ) {
            // 在最下面且往往上滑动 禁止
            return false;
          } else {
            // 其余情况 可滚动
            return true;
          }
        }
      } else {
        // 未到body则继续往上遍历
        return Touch._findParent(curNodeParent, finalDirction);
      }
    }
  },
};

/**
 * @func 暴露给vue使用的初始化接口
 */
const onInitTouchEvent = () => {
  document.body.addEventListener('touchstart', Touch._onStart, {
    passive: false,
  });
  document.body.addEventListener('touchmove', Touch._onMove, {
    passive: false,
  });
  document.body.addEventListener('touchend', Touch._onEnd, { passive: false });
};

/**
 * @func 暴露给vue使用的卸载监听接口
 */
const onRemoveTouchEvent = () => {
  document.body.removeEventListener('touchstart', () => {}, false);
  document.body.removeEventListener('touchmove', () => {}, false);
  document.body.removeEventListener('touchend', () => {}, false);
};

export { onInitTouchEvent, onRemoveTouchEvent };

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐