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


前言

在微信里面瀏覽頁面的時候,有一個很管用的方法可以區分這個頁面是原生的還是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

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM