前言
在微信里面浏览页面的时候,有一个很管用的方法可以区分这个页面是原生的还是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: scroll
和overflow: 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