前言
在微信里面瀏覽頁面的時(shí)候,,有一個(gè)很管用的方法可以區(qū)分這個(gè)頁面是原生的還是H5形式的。隨便打開一個(gè)頁面,,用力往下扯的時(shí)候,,如果頁面上方出現(xiàn)了“黑底”,黑底上有一行諸如網(wǎng)頁由game.weixin.qq.com提供 的文字,,就表明這個(gè)頁面是H5形式的,。這帶來的問題是,如果一個(gè)頁面可滾動(dòng)區(qū)域很小,,隨便一拉,,頁面下方出現(xiàn)了黑底,然后你又輕輕往上一拉,,上面的黑底又出來了,,個(gè)人表示非常難受啊,! 于是乎,,折騰了一番,,寫了一個(gè)簡單的組件來實(shí)現(xiàn)禁止這種拉動(dòng)頁面出現(xiàn)黑底的特性。
實(shí)現(xiàn)原理
首先需要說明的是,,由于Android和IOS的webview存在差異,,這個(gè)組件對于IOS是比較友好的,安卓下并不能做到完美避免,,下面一一分析,。
簡述touch事件
智能手機(jī)和平板電腦一類的移動(dòng)設(shè)備通常會(huì)有一個(gè)電容式觸摸屏(capacitive touch-sensitive screen),以捕捉用戶的手指所做的交互,。有三種在規(guī)范中列出并獲得跨移動(dòng)設(shè)備廣泛實(shí)現(xiàn)的基本觸摸事件:
touchstart :手指放在一個(gè)DOM元素上
touchmove :手指拖曳一個(gè)DOM元素
touchend :手指從一個(gè)DOM元素上移開
其中每一個(gè)觸摸事件都會(huì)包含三個(gè)觸摸列表:
touches :當(dāng)前位于屏幕上的所有手指的一個(gè)列表,。
targetTouches :位于當(dāng)前DOM元素上的手指的一個(gè)列表。
changedTouches :涉及當(dāng)前事件的手指的一個(gè)列表,。
這些列表由包含了觸摸信息的對象組成:
identifier :一個(gè)數(shù)值,,唯一標(biāo)識觸摸會(huì)話(touch session)中的當(dāng)前手指。
target :DOM元素,,是動(dòng)作所針對的目標(biāo),。
客戶/頁面/屏幕坐標(biāo) :動(dòng)作在屏幕上發(fā)生的位置。
半徑坐標(biāo)和 rotationAngle :畫出大約相當(dāng)于手指形狀的橢圓形,。 在jsfiddle里面寫一個(gè)簡單的小demo就一目了然了:
http:///yuanzm/ws9j4v1v/2/
在這個(gè)組件中,,我們只需要用到e.touches[0].clientY 屬性就夠了:在開始觸摸的時(shí)候,記錄觸摸點(diǎn)的起始位置,,在手指移動(dòng)過程中,,不斷獲取最新的clientY ,與起始位置的clientY 比較,,就能獲知拉動(dòng)頁面的方向,。
與height相關(guān)的幾個(gè)屬性
scrollHeight: 是計(jì)量元素內(nèi)容高度的只讀屬性,包括overflow樣式屬性導(dǎo)致的視圖中不可見內(nèi)容,。沒有垂直滾動(dòng)條的情況下,scrollHeight值與元素視圖填充所有內(nèi)容所需要的最小值clientHeight相同,。包括元素的padding,,但不包括元素的margin.
offsetHeight:是一個(gè)只讀屬性,它返回該元素的像素高度,,高度包含該元素的垂直內(nèi)邊距和邊框,,且是一個(gè)整數(shù)。
scrollTop:設(shè)置獲取讀取元素向上滾動(dòng)了多少像素,。對于可滾動(dòng)的元素,,這個(gè)值是可見區(qū)域頂部和不可見區(qū)域頂部的距離。如果元素不能滾動(dòng),,這個(gè)值默認(rèn)為0,。
這三個(gè)屬性是用來計(jì)算元素處于頁面的哪個(gè)位置的,考慮下面兩種情況:
元素的offsetHeight大于等于scrollHeight,無縱向滾動(dòng)條出現(xiàn),,這個(gè)元素是不能滾動(dòng)的,。如果一個(gè)元素不能滾動(dòng)了,就會(huì)嘗試外層的元素能不能滾動(dòng),,一層一層往外冒泡,。在webview里面,最外面一層就是這個(gè)webview容器了,,按照微信的設(shè)置,,這一層的“滾動(dòng)”就是露出下面的黑底。所以為了避免露出黑底,,我們要在當(dāng)前元素不能滾動(dòng)的時(shí)候及時(shí)禁止掉冒泡,,這樣就不會(huì)觸發(fā)到上一層的滾動(dòng)。
如果一個(gè)元素設(shè)置了高度,,并且設(shè)置了overflow: scroll,,當(dāng)元素內(nèi)的內(nèi)容可滾動(dòng)的時(shí)候,scrollHeight的值就會(huì)明顯大于offsetheight,,那我們怎么判斷元素內(nèi)的內(nèi)容下拉到底部了呢,?這就需要綜合offsetHeight和scrollTop的值了,如果offsetHeight的值加上srcollTop的值大于等于scrollHeight的值,,就表明內(nèi)容已經(jīng)滑動(dòng)底部了,。和第一點(diǎn)一樣,當(dāng)我們知道了臨界條件后,,及時(shí)阻止掉冒泡就ok了,。
結(jié)合touch和height屬性
通過上面兩點(diǎn),我們已經(jīng)知道要達(dá)到禁止出現(xiàn)黑底的效果,,努力的方向是在知道滑動(dòng)方向的條件下,,在與height相關(guān)的屬性達(dá)到臨界值的時(shí)候及時(shí)阻止事件冒泡。只有三種簡單的情況:
(內(nèi)容)向下拉到底部,,不能往下拉,,但是可以往上拉
(內(nèi)容)向上拉到頂部,不能往上拉,,但是可以往下拉
(內(nèi)容)既不能往下拉也不能往下拉
總結(jié)起來如下表(1為允許,,0為禁止,高位表示向上方向,,低位表示向下方向)
可以拉的方向(height) |
拉的方向(touch) |
能否繼續(xù)拉 |
00 |
10 |
0 |
00 |
01 |
0 |
01 |
10 |
0 |
01 |
01 |
1 |
10 |
10 |
1 |
10 |
01 |
0 |
從表中我們可以得出一個(gè)結(jié)論是,,能否在該方向上繼續(xù)拉其實(shí)就是對兩種條件做一個(gè)& 運(yùn)算!話不多說,,上核心源碼
// 防止過分拉動(dòng)
preventMove: function(e) {
// 高位表示向上滾動(dòng), 底位表示向下滾動(dòng): 1容許 0禁止
var status = '11',
e = e || window.event, // 使用 || 運(yùn)算取得event對象
ele = this,
currentY = e.touches[0].clientY,
startY = startMoveYmap[ele.id],
scrollTop = ele.scrollTop,
offsetHeight = ele.offsetHeight,
scrollHeight = ele.scrollHeight;
if (scrollTop === 0) {
// 如果內(nèi)容小于容器則同時(shí)禁止上下滾動(dòng)
status = offsetHeight >= scrollHeight ? '00' : '01';
} else if (scrollTop + offsetHeight >= scrollHeight) {
// 已經(jīng)滾到底部了只能向上滾動(dòng)
status = '10';
}
if (status != '11') {
// 判斷當(dāng)前的滾動(dòng)方向
var direction = currentY - startY > 0 ? '10' : '01';
// console.log(direction);
// 操作方向和當(dāng)前允許狀態(tài)求與運(yùn)算,,運(yùn)算結(jié)果為0,,就說明不允許該方向滾動(dòng),則禁止默認(rèn)事件,,阻止?jié)L動(dòng)
if (!(parseInt(status, 2) & parseInt(direction, 2))) {
e.preventDefault();
e.stopPropagation();
return;
}
}
},
與UI共用的線程
開始的時(shí)候,,我以為上面的代碼就萬事大吉了,經(jīng)過實(shí)踐和摸索,,結(jié)論是:簡直是天真,。
異步的概念之所以首先在Web2.0中火起來,是因?yàn)樵跒g覽器中JavaScript在單線程上執(zhí)行,,而且它還與UI渲染共用一個(gè)UI線程,。這意味著JavaScript在執(zhí)行的時(shí)候UI渲染和響應(yīng)是處于停滯狀態(tài)的。 ----《深入淺出nodejs》
這意味這什么呢,?當(dāng)我們的UI線程在進(jìn)行渲染的時(shí)候,,JavaScript代碼也是處于停滯狀態(tài)的!不信的話可以在一個(gè)可以滑動(dòng)的頁面上引入下面這段代碼:
var count = 0;
setInterval(functiong() {
console.log(++count);
}, 100);
刷新頁面的時(shí)候,,控制臺(tái)會(huì)一直打印不斷變大的數(shù)字,,但是只要你用手指開始拖動(dòng)頁面,打印終止,,等你把手放開的時(shí)候,,打印繼續(xù),而且數(shù)字會(huì)承接打印停止前那個(gè)數(shù)字,。也就是UI在渲染的時(shí)候,,js保存了狀態(tài),在UI渲染停止的時(shí)候,,js又可以繼續(xù)運(yùn)行,。 這對我們的組件帶來的影響是什么呢?幾乎是毀滅性的,,場景如下:
如果頁面內(nèi)容不足一屏,,按照組件的設(shè)定,既不能上拉也不能下拉,,這種情況不會(huì)受影響,。
如果頁面內(nèi)容多于一屏,按照組件的設(shè)定,,這時(shí)候可以往下拉不能往上拉,在嘗試上拉的時(shí)候,,組件會(huì)阻止冒泡,。但如果先下拉一點(diǎn)然后使勁往上拉,本來拉到頂之后組件會(huì)阻止事件冒泡,,但是一旦下拉之后,,線程就歸屬于UI了,,上拉的過程中組件的判斷完全插不進(jìn)手,還是無情漏出了黑底,!GG,!
可愛的IOS5新特性
在尋求最終的解決方案之前,我們先來討論一下overflow這個(gè)屬性,。
傳統(tǒng) pc 端中,,子容器高度超出父容器高度,通常使用 overflow:auto 可出現(xiàn)滾動(dòng)條拖動(dòng)顯示溢出的內(nèi)容,,而移動(dòng)web開發(fā)中,,由于瀏覽器廠商的系統(tǒng)不同、版本不同,,導(dǎo)致有部分機(jī)型不支持對彈性滾動(dòng),,從而在開發(fā)中制造了所謂的 BUG。
從本人這兩個(gè)月移動(dòng)Web實(shí)踐的經(jīng)驗(yàn)來看,,微信的webview里面overflow: scroll 和overflow: auto 的滑動(dòng)效果無論是在安卓還是IOS下的體驗(yàn)都很一般,,有明顯的卡頓現(xiàn)象,在安卓下面還會(huì)出現(xiàn)滑動(dòng)過快的時(shí)候在頁面停下來之后滾動(dòng)條才閃到相應(yīng)位置的現(xiàn)象,。 在IOS5之后,,出現(xiàn)了一個(gè)新的屬性: -webkit-overflow-scrolling ,用來控制元素在移動(dòng)設(shè)備上是否使用滾動(dòng)回彈效果。它的取值有兩個(gè):
auto:使用普通滾動(dòng), 當(dāng)手指從觸摸屏上移開,,滾動(dòng)會(huì)立即停止,。
touch:使用具有回彈效果的滾動(dòng), 當(dāng)手指從觸摸屏上移開,內(nèi)容會(huì)繼續(xù)保持一段時(shí)間的滾動(dòng)效果,。繼續(xù)滾動(dòng)的速度和持續(xù)的時(shí)間和滾動(dòng)手勢的強(qiáng)烈程度成正比,。同時(shí)也會(huì)創(chuàng)建一個(gè)新的堆棧上下文。
實(shí)驗(yàn)表明,,在IOS下,,對一個(gè)元素設(shè)置了overflow:scroll 的基礎(chǔ)上再添加-webkit-overflow-scrolling: touch; 會(huì)讓滑動(dòng)又如絲般順滑。 這個(gè)屬性和我們解決之前的問題有什么聯(lián)系呢,?秘密就在這彈性滾動(dòng)效果,。
原始場景
頁面中body 元素的內(nèi)容超過一屏,頁面可以往下滑動(dòng)(手指往上拉),。按照我們組件的設(shè)定,,手指開始的時(shí)候是不能往下拉的,但是如果手指的方向是先往上拉一小段,,在手指不離開屏幕的基礎(chǔ)上再往下拉,,當(dāng)頁面拉到頂部的時(shí)候,會(huì)相繼出現(xiàn)黑底,,因?yàn)閁I在渲染,,js沒法去阻止事件冒泡,。
改進(jìn)場景
現(xiàn)在我們把組件的作用元素設(shè)定為body內(nèi)最外圍的div元素,并且給這個(gè)元素添加兩個(gè)CSS屬性overflow:scroll 和-webkit-overflow-scrolling: touch; ,,那么上面的場景就會(huì)變成: 頁面中body內(nèi)最外圍的div標(biāo)簽內(nèi)容超過一屏,,其內(nèi)容可以往下滑動(dòng)(手指往上拉)。按照我們組件的設(shè)定,,手指開始的時(shí)候是不能往下拉的,。和之前一樣,手指先往上拉一小段,,在手指不離開該元素的基礎(chǔ)上再往下拉,,當(dāng)元素內(nèi)容到頂之后,因?yàn)閁I在渲染,,js本插不上手,,但是該元素內(nèi)部的內(nèi)容設(shè)置了彈性滾動(dòng),要實(shí)現(xiàn)彈性滾動(dòng),,基本要求就是這個(gè)div容器是不動(dòng)的,,可以理解成因?yàn)閺椥詽L動(dòng),自動(dòng)就禁止掉了事件冒泡,也就不會(huì)出現(xiàn)黑底了,。
肯定有人要問了,,既然自動(dòng)禁止了事件冒泡,那還要這個(gè)組件何用,?當(dāng)然有用,,會(huì)禁止掉事件冒泡的前提是內(nèi)容在滾動(dòng)。依照上面的場景,,如果一開始手指直接往下拉,,沒有組件的限制,還是會(huì)露出黑底,,因而,,要實(shí)現(xiàn)比較好的效果,是需要這兩個(gè)屬性和組件配合的,。 至于安卓嘛,,因?yàn)闆]有這個(gè)屬性,暫時(shí)只能一邊涼快去吧,。
小結(jié)
多說無用,,看源碼吧: https://github.com/yuanzm/preventoverscrolljs
參考
|