转载

微信里面防止下拉"露底"组件

前言

在微信里面浏览页面的时候,有一个很管用的方法可以区分这个页面是原生的还是H5形式的。随便打开一个页面,用力往下扯的时候,如果页面上方出现了“黑底”,黑底上有一行诸如 网页由game.weixin.qq.com提供 的文字,就表明这个页面是H5形式的。这带来的问题是,如果一个页面可滚动区域很小,随便一拉,页面下方出现了黑底,然后你又轻轻往上一拉,上面的黑底又出来了,个人表示非常难受啊!

于是乎,折腾了一番,写了一个简单的组件来实现禁止这种拉动页面出现黑底的特性。

实现原理

首先需要说明的是,由于Android和IOS的webview存在差异,这个组件对于IOS是比较友好的,安卓下并不能做到完美避免,下面一一分析。

简述touch事件

智能手机和平板电脑一类的移动设备通常会有一个电容式触摸屏(capacitive touch-sensitive screen),以捕捉用户的手指所做的交互。有三种在规范中列出并获得跨移动设备广泛实现的基本触摸事件:

  • touchstart :手指放在一个DOM元素上
  • touchmove :手指拖曳一个DOM元素
  • touchend :手指从一个DOM元素上移开

其中每一个触摸事件都会包含三个触摸列表:

  • touches :当前位于屏幕上的所有手指的一个列表。
  • targetTouches :位于当前DOM元素上的手指的一个列表。
  • changedTouches :涉及当前事件的手指的一个列表。

这些列表由包含了触摸信息的对象组成:

  • identifier :一个数值,唯一标识触摸会话(touch session)中的当前手指。
  • target :DOM元素,是动作所针对的目标。
  • 客户/页面/屏幕坐标 :动作在屏幕上发生的位置。
  • 半径坐标和 rotationAngle :画出大约相当于手指形状的椭圆形。
    在jsfiddle里面写一个简单的小demo就一目了然了:
    http://jsfiddle.net/yuanzm/ws9j4v1v/2/

在这个组件中,我们只需要用到 e.touches[0].clientY 属性就够了:在开始触摸的时候,记录触摸点的起始位置,在 手指 移动过程中,不断获取最新的 clientY ,与起始位置的 clientY 比较,就能获知拉动页面的方向。

与height相关的几个属性

  • scrollHeight: 是计量元素内容高度的只读属性,包括overflow样式属性导致的视图中不可见内容。没有垂直滚动条的情况下,scrollHeight值与元素视图填充所有内容所需要的最小值clientHeight相同。包括元素的padding,但不包括元素的margin.
  • offsetHeight:是一个只读属性,它返回该元素的像素高度,高度包含该元素的垂直内边距和边框,且是一个整数。
  • scrollTop:设置获取读取元素向上滚动了多少像素。对于可滚动的元素,这个值是可见区域顶部和不可见区域顶部的距离。如果元素不能滚动,这个值默认为0。

这三个属性是用来计算元素处于页面的哪个位置的,考虑下面两种情况:

  • 元素的offsetHeight 大于等于 scrollHeight,无纵向滚动条出现,这个元素是不能滚动的。如果一个元素不能滚动了,就会尝试外层的元素能不能滚动,一层一层往外冒泡。在webview里面,最外面一层就是这个webview容器了,按照微信的设置,这一层的“滚动”就是露出下面的黑底。所以为了避免露出黑底,我们要在当前元素不能滚动的时候及时禁止掉冒泡,这样就不会触发到上一层的滚动。
  • 如果一个元素设置了高度,并且设置了overflow: scroll,当元素内的内容可滚动的时候,scrollHeight的值就会明显大于offsetheight,那我们怎么判断元素内的内容下拉到底部了呢?这就需要综合offsetHeight和scrollTop的值了,如果offsetHeight的值加上srcollTop的值大于等于scrollHeight的值,就表明内容已经滑动底部了。和第一点一样,当我们知道了临界条件后,及时阻止掉冒泡就ok了。

结合touch和height属性

通过上面两点,我们已经知道要达到禁止出现黑底的效果,努力的方向是 在知道滑动方向的条件下,在与height相关的属性达到临界值的时候及时阻止事件冒泡。 只有三种简单的情况:

  • (内容)向下拉到底部,不能往下拉,但是可以往上拉
  • (内容)向上拉到顶部,不能往上拉,但是可以往下拉
  • (内容)既不能往下拉也不能往下拉

总结起来如下表(1为允许,0为禁止,高位表示向上方向,低位表示向下方向)

可以拉的方向(height) 拉的方向(touch) 能否继续拉
00 10 0
00 01 0
01 10 0
01 01 1
10 10 1
10 01 0

从表中我们可以得出一个结论是,能否在该方向上继续拉其实就是对两种条件做一个 & 运算!话不多说,上核心源码

  // 防止过分拉动   preventMove: function(e) {    // 高位表示向上滚动, 底位表示向下滚动: 1容许 0禁止    var status = '11',      e = e || window.event, // 使用 || 运算取得event对象     ele = this,     currentY = e.touches[0].clientY,     startY = startMoveYmap[ele.id],     scrollTop = ele.scrollTop,     offsetHeight = ele.offsetHeight,     scrollHeight = ele.scrollHeight;    if (scrollTop === 0) {     // 如果内容小于容器则同时禁止上下滚动     status = offsetHeight >= scrollHeight ? '00' : '01';    } else if (scrollTop + offsetHeight >= scrollHeight) {     // 已经滚到底部了只能向上滚动     status = '10';    }    if (status != '11') {     // 判断当前的滚动方向     var direction = currentY - startY > 0 ? '10' : '01';     // console.log(direction);     // 操作方向和当前允许状态求与运算,运算结果为0,就说明不允许该方向滚动,则禁止默认事件,阻止滚动     if (!(parseInt(status, 2) & parseInt(direction, 2))) {      e.preventDefault();      e.stopPropagation();      return;     }    }   }, 

与UI共用的线程

开始的时候,我以为上面的代码就万事大吉了,经过实践和摸索,结论是:简直是天真。

异步的概念之所以首先在Web2.0中火起来,是因为在浏览器中JavaScript在单线程上执行,而且它还与UI渲染共用一个UI线程。这意味着JavaScript在执行的时候UI渲染和响应是处于停滞状态的。 ----《深入浅出nodejs》

这意味这什么呢?当我们的UI线程在进行渲染的时候,JavaScript代码也是处于停滞状态的!不信的话可以在一个可以滑动的页面上引入下面这段代码:

var count = 0; setInterval(functiong() {  console.log(++count); }, 100);

刷新页面的时候,控制台会一直打印不断变大的数字,但是只要你用手指开始拖动页面,打印终止,等你把手放开的时候,打印继续,而且数字会承接打印停止前那个数字。也就是UI在渲染的时候,js保存了状态,在UI渲染停止的时候,js又可以继续运行。这对我们的组件带来的影响是什么呢?几乎是毁灭性的,场景如下:

  • 如果页面内容不足一屏,按照组件的设定,既不能上拉也不能下拉,这种情况不会受影响。
  • 如果页面内容多于一屏,按照组件的设定,这时候可以往下拉不能往上拉,在尝试上拉的时候,组件会阻止冒泡。但如果先下拉一点然后使劲往上拉,本来拉到顶之后组件会阻止事件冒泡,但是一旦下拉之后,线程就归属于UI了,上拉的过程中组件的判断完全插不进手,还是无情漏出了黑底!GG!

可爱的IOS5新特性

在寻求最终的解决方案之前,我们先来讨论一下overflow这个属性。

传统 pc 端中,子容器高度超出父容器高度,通常使用 overflow:auto 可出现滚动条拖动显示溢出的内容,而移动web开发中,由于浏览器厂商的系统不同、版本不同,导致有部分机型不支持对弹性滚动,从而在开发中制造了所谓的 BUG。

从本人这两个月移动Web实践的经验来看,微信的webview里面 overflow: scrolloverflow: auto 的滑动效果无论是在安卓还是IOS下的体验都很一般,有明显的卡顿现象,在安卓下面还会出现滑动过快的时候在页面停下来之后滚动条才 闪到 相应位置的现象。

在IOS5之后,出现了一个新的属性: -webkit-overflow-scrolling ,用来控制元素在移动设备上是否使用滚动回弹效果。它的取值有两个:

  • auto:使用普通滚动, 当手指从触摸屏上移开,滚动会立即停止。
  • touch:使用具有回弹效果的滚动, 当手指从触摸屏上移开,内容会继续保持一段时间的滚动效果。继续滚动的速度和持续的时间和滚动手势的强烈程度成正比。同时也会创建一个新的堆栈上下文。

实验表明,在IOS下,对一个元素设置了 overflow:scroll 的基础上再添加 -webkit-overflow-scrolling: touch; 会让滑动又如丝般顺滑。

这个属性和我们解决之前的问题有什么联系呢?秘密就在这弹性滚动效果。

原始场景

页面中 body 元素的内容超过一屏,页面可以往下滑动(手指往上拉)。按照我们组件的设定,手指开始的时候是不能往下拉的,但是如果手指的方向是先往上拉一小段,在手指不离开屏幕的基础上再往下拉,当页面拉到顶部的时候,会相继出现黑底,因为UI在渲染,js没法去阻止事件冒泡。

改进场景

现在我们把组件的作用元素设定为body内最外围的div元素,并且给这个元素添加两个CSS属性 overflow:scroll-webkit-overflow-scrolling: touch; ,那么上面的场景就会变成:

页面中body内最外围的div标签内容超过一屏,其内容可以往下滑动(手指往上拉)。按照我们组件的设定,手指开始的时候是不能往下拉的。和之前一样,手指先往上拉一小段,在手指不离开该元素的基础上再往下拉,当元素内容到顶之后,因为UI在渲染,js本插不上手, 但是 该元素内部的内容设置了弹性滚动,要实现弹性滚动,基本要求就是这个div容器是不动的,可以理解成因为弹性滚动,自动就禁止掉了事件冒泡,也就不会出现黑底了。

肯定有人要问了,既然自动禁止了事件冒泡,那还要这个组件何用?当然有用,会禁止掉事件冒泡的前提是内容在滚动。依照上面的场景,如果一开始手指直接往下拉,没有组件的限制,还是会露出黑底,因而,要实现比较好的效果,是需要这两个属性和组件配合的。至于安卓嘛,因为没有这个属性,暂时只能一边凉快去吧。

小结

多说无用,看源码吧:

https://github.com/yuanzm/preventoverscrolljs

参考

正文到此结束
Loading...