在微信里面浏览页面的时候,有一个很管用的方法可以区分这个页面是原生的还是H5形式的。随便打开一个页面,用力往下扯的时候,如果页面上方出现了“黑底”,黑底上有一行诸如 网页由game.weixin.qq.com提供
的文字,就表明这个页面是H5形式的。这带来的问题是,如果一个页面可滚动区域很小,随便一拉,页面下方出现了黑底,然后你又轻轻往上一拉,上面的黑底又出来了,个人表示非常难受啊!
于是乎,折腾了一番,写了一个简单的组件来实现禁止这种拉动页面出现黑底的特性。
首先需要说明的是,由于Android和IOS的webview存在差异,这个组件对于IOS是比较友好的,安卓下并不能做到完美避免,下面一一分析。
智能手机和平板电脑一类的移动设备通常会有一个电容式触摸屏(capacitive touch-sensitive screen),以捕捉用户的手指所做的交互。有三种在规范中列出并获得跨移动设备广泛实现的基本触摸事件:
其中每一个触摸事件都会包含三个触摸列表:
这些列表由包含了触摸信息的对象组成:
在这个组件中,我们只需要用到 e.touches[0].clientY
属性就够了:在开始触摸的时候,记录触摸点的起始位置,在 手指 移动过程中,不断获取最新的 clientY
,与起始位置的 clientY
比较,就能获知拉动页面的方向。
这三个属性是用来计算元素处于页面的哪个位置的,考虑下面两种情况:
通过上面两点,我们已经知道要达到禁止出现黑底的效果,努力的方向是 在知道滑动方向的条件下,在与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; } } },
开始的时候,我以为上面的代码就万事大吉了,经过实践和摸索,结论是:简直是天真。
异步的概念之所以首先在Web2.0中火起来,是因为在浏览器中JavaScript在单线程上执行,而且它还与UI渲染共用一个UI线程。这意味着JavaScript在执行的时候UI渲染和响应是处于停滞状态的。 ----《深入浅出nodejs》
这意味这什么呢?当我们的UI线程在进行渲染的时候,JavaScript代码也是处于停滞状态的!不信的话可以在一个可以滑动的页面上引入下面这段代码:
var count = 0; setInterval(functiong() { console.log(++count); }, 100);
刷新页面的时候,控制台会一直打印不断变大的数字,但是只要你用手指开始拖动页面,打印终止,等你把手放开的时候,打印继续,而且数字会承接打印停止前那个数字。也就是UI在渲染的时候,js保存了状态,在UI渲染停止的时候,js又可以继续运行。这对我们的组件带来的影响是什么呢?几乎是毁灭性的,场景如下:
在寻求最终的解决方案之前,我们先来讨论一下overflow这个属性。
传统 pc 端中,子容器高度超出父容器高度,通常使用 overflow:auto 可出现滚动条拖动显示溢出的内容,而移动web开发中,由于浏览器厂商的系统不同、版本不同,导致有部分机型不支持对弹性滚动,从而在开发中制造了所谓的 BUG。
从本人这两个月移动Web实践的经验来看,微信的webview里面 overflow: scroll
和 overflow: auto
的滑动效果无论是在安卓还是IOS下的体验都很一般,有明显的卡顿现象,在安卓下面还会出现滑动过快的时候在页面停下来之后滚动条才 闪到 相应位置的现象。
在IOS5之后,出现了一个新的属性: -webkit-overflow-scrolling
,用来控制元素在移动设备上是否使用滚动回弹效果。它的取值有两个:
实验表明,在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