开源 wordpress shell Android 云计算 linux 程序员 java Python 编程 google php apache Firefox mysql 微软 centos Ubuntu nginx Windows

解決IScroll疑難雜癥

在去年,我們對IScroll的源碼進行了學習,並且分離出了一段代碼自己使用,在使用學習過程中發現幾個致命問題:

① 光標移位

② 文本框找不到(先讓文本框獲取焦點,再滑動一下,輸入文字便可重現)

③ 偶爾導致頭部消失,頭部可不是fixed哦

 

由於以上問題,加之去年我們團隊的工作量極大,和中間一些組織架構調整,這個事情一直被放到了今天,心裏一直對此耿耿於懷,因為IScroll讓人忘不了的好處

小釵堅信,IScroll可以帶來前端體驗上的革命,因為他可以解決以下問題

區域滑動順滑感的體驗
解決fixed移位問題
解決動畫過程中長短頁的問題,並且可以優化view切換動畫的順暢度
我們不能因為一兩個小問題而放棄如此牛逼的點子,所以我們要處理其中的問題,那麽這些問題是否真的不可解決,而引起這些問題的原因又到底是什麽,我們今天來一一探索

ps:該問題已有更好的解決方案,待續

抽離IScroll

第一步依舊是抽離IScroll核心邏輯,我們這裏先在簡單層面上探究問題,以免被旁枝末節的BUG困擾,這裏形成的一個庫只支持縱向滾動,代碼量比較少

Demo
核心代碼
代碼中引入了fastclick解決其移動端點擊問題,demo效果在此:

http://sandbox.runjs.cn/show/xq2fbetv

基本代碼出來了,我們現在來一個個埋坑,首先解決難的問題!

光標跳動/文本框消失

光標跳動是什麽現象大家都知道了,至於導致的原因又我們測試下來,即可確定罪魁禍首為:transform,於是我們看看滑動過程中發生了什麽

① 每次滑動會涉及到位置的變化

this._translate(0, newY);
② 每次變化會改變transform屬性

 1 //移動x,y這裏比較簡單就不分離y了
 2 _translate: function (x, y) {
 3   this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ;
 4 
 5   this.x = x;
 6   this.y = y;
 7 
 8   if (this.options.scrollbars) {
 9     this.indicator.updatePosition();
10   }
11 
12 },
我們這裏做一次剝離,將transform改成直接改變top值看看效果

this.scrollerStyle['top'] = y + 'px';
而事實證明,一旦去除transform屬性,我們這裏便不再有光標閃動的問題了。

更進一步的分析,實驗,你會發現其實引起的原因是這句:

//       this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ;
       this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' ;
沒錯,就是css3d加速引起的,他的優勢是讓動畫變得順暢,卻不曾想到會引起文本框光標閃爍的問題

針對ios閃爍有一個神奇的屬性是

-webkit-backface-visibility: hidden;
於是加入到,scroller元素上後觀察之,無效,舍棄該方案再來就是一些怪辦法了:

滑動隱藏虛擬鍵盤

文本獲取焦點的情況下,會隱藏虛擬鍵盤,連焦點都沒有了,這個問題自然不藥而愈,於是我們只要滑動便讓其失去焦點,這樣似乎狡猾的繞過了這個問題

在touchmove邏輯處加入以下邏輯

 1 //暫時只考慮input問題,有效再擴展
 2 var el = document.activeElement;
 3 if (el.nodeName.toLowerCase() == 'input') {
 4   el.blur();
 5   this.disable();
 6   setTimeout($.proxy(function () {
 7     this.enable();
 8   }, this), 250);
 9   return;
10 }
該方案最為簡單粗暴,他在我們意圖滑動時便直接導致虛擬鍵盤失效,從而根本不會滑動,便錯過了光標跳動的問題

甚至,解決了由於滾動導致的文本框消失問題!!!

其中有一個250ms的延時,這個期間是虛擬鍵盤隱藏所用時間,這個時間段將不可對IScroll進行操作,該方案實驗下來效果還行

其中這個延時在200-300之間比較符合人的操作習慣,不設置滾動區域會亂閃,取什麽值各位自己去嘗試,測試地址:

http://sandbox.runjs.cn/show/8nkmlmz5

這個方案是我覺得最優的方案,其是否接受還要看產品態度

死磕-重寫_translate

_translate是IScroll滑動的總控,這裏默認是使用transform進行移動,但若是獲取焦點的情況下我們可以具有不一樣的方案

在文本框具有焦點是,我們使用top代替transform!

PS:這是個爛方法不建議采用

 1 //移動x,y這裏比較簡單就不分離y了
 2 _translate: function (x, y) {
 3 
 4   var el = document.activeElement;
 5   if (el.nodeName.toLowerCase() == 'input') {
 6     this.scrollerStyle['top'] = y + 'px';
 7   } else {
 8     this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ;
 9   }
10 
11   this.x = x;
12   this.y = y;
13 
14   if (this.options.scrollbars) {
15     this.indicator.updatePosition();
16   }
17 
18 },
該方案被測試確實可行,不會出現光標閃的現象,但是有一個問題卻需要我們處理,便是一旦文本框失去焦點,我們要做top與transform的換算

所以這是一個爛方法!!!這裏換算事實上也不難,就是將top值重新歸還transform,但是整個這個邏輯卻是讓人覺得別扭

而且我們這裏還需要一個定時器去做計算,判斷何時文本框失去焦點,整個這個邏輯就是一個字 坑!

 1 //移動x,y這裏比較簡單就不分離y了
 2 _translate: function (x, y) {
 3 
 4   var el = document.activeElement;
 5   if (el.nodeName.toLowerCase() == 'input') {
 6     this.scrollerStyle['top'] = y + 'px';
 7 
 8     //便需要做距離換算相關清理,一旦文本框事情焦點,我們要做top值還原
 9     if (!this.TimerSrc) {
10       this.TimerSrc = setInterval($.proxy(function () {
11         var el = document.activeElement;
12         if (el.nodeName.toLowerCase() != 'input') {
13 
14           pos = this.getComputedPosition();
15 
16           var top = $(scroller).css('top');
17           this.scrollerStyle['top'] = '0px';
18           console.log(pos);
19 
20           var _x = Math.round(pos.x);
21           var _y = Math.round(pos.y);
22           _y = _y + parseInt(top);
23 
24           //移動過去
25           this._translate(_x, _y);
26 
27           clearInterval(this.TimerSrc);
28           this.TimerSrc = null;
29         }
30       }, this), 20);
31     }
32   } else {
33     this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ;
34   }
35 
36   this.x = x;
37   this.y = y;
38 
39   if (this.options.scrollbars) {
40     this.indicator.updatePosition();
41   }
42 
43 },
經測試,該代碼可以解決光標跳動問題,但是坑不坑大家心裏有數,一旦需要被迫使用定時器的地方,必定會有點坑!測試地址

http://sandbox.runjs.cn/show/v9pno9d8

死磕-文本框消失

文本框消息是由於滾動中產生動畫,將文本框搞到區域外了,這個時候一旦我們輸入文字,導致input change,系統便會自動將文本定位到中間,而出現文本不可見問題

該問題的處理最好的方案,依舊是方案一,若是這裏要死磕,又會有許多問題,方案無非是給文本設置changed事件,或者定時器什麽的,當變化時,自動將文本元素

至於IScroll可視區域,樓主這裏就不獻醜了,因為我基本決定使用方案一了。

步長移動

所謂步長移動便是我一次必須移動一定距離,這個與圖片橫向輪播功能有點類似,而這類需求在移動端數不勝數,那我們的IScroll應該如何處理才能加上這一偉大特性呢?

去看IScroll的源碼,人家都已經實現了,居然人家都實現了,哎,但是我們這裏不管他,照舊做我們的事情吧,加入步長功能

PS:這裏有點小小的失落,我以為沒有實現呢,這樣我搞出來肯定沒有官方的優雅了!

思路

思路其實很簡單,我們若是設置了一個步長屬性,暫時我們認為他是一個數字(其實可以是bool值,由庫自己計算該值),然後每次移動時候便必須強制移動該屬性的整數倍即可,比如:

1 var s = new IScroll({
2   wrapper: $('#wrapper'),
3   scroller: $('#scroller'),
4   setp: 40
5 });
這個便要求每次都得移動10px的步長,那麽這個如何實現呢?其實實現點,依然是_translate處,我們這裏需要一點處理

 1 //移動x,y這裏比較簡單就不分離y了
 2 _translate: function (x, y, isStep) {
 3 
 4   //處理步長
 5   if (this.options.setp && !isStep) {
 6     var flag2 = y > 0 ? 1 : -1; //這個會影響後面的計算結果
 7     var top = Math.abs(y);
 8     var mod = top % this.options.setp;
 9     top = (parseInt(top / this.options.setp) * this.options.setp + (mod > (this.options.setp/2) ? this.options.setp : 0)) * flag2;
10     y = top;
11   }
12 
13   this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ;
14 
15   this.x = x;
16   this.y = y;
17 
18   if (this.options.scrollbars) {
19     this.indicator.updatePosition();
20   }
21 
22 },
這樣一改後,每次便要求移動40px的步長,當然,我這裏代碼寫的不是太好,整個效果在此

這裏唯一需要處理的就是touchmove了,每次move的時候,我們不應該對其進行步長控制,而後皆可以,這種控制步長的效果有什麽用呢?請看這個例子:


雙IScroll的問題

所謂雙IScroll,便是一個頁面出現了兩個IScroll組件的問題,這個問題前段時間發生在了我們一個團隊身上,其狀況具體為

他在一個view上面有兩個地方使用了IScroll,結果就是感覺滑動很卡,並且不能很好的定位原因,其實導致這個原因的主要因素是:

他將事件綁定到了document上,而不是具體包裹層元素上,這樣的話,就算一個IScroll隱藏了,他滑動時候已經執行了兩個邏輯,從而出現了卡的現象

當然,一個IScroll隱藏的話其實應該釋放其中的事件句柄,當時他沒有那麽做,所以以後大家遇到對應的功能,需要將事件綁定對位置

我們這裏舉個例子:

 View Code
這裏,我們將滑動事件綁定到了各個wrapper上,所以不會出現卡的現象,以後各位自己要註意:

 

異步DOM加載,不可滑動

這個問題其實比較簡單,只需要每次操作後執行一次refresh,方法即可,這裏重啟一行有點坑爹了 

大殺器

往往最後介紹的方法最為牛B,不錯,小釵還有一招大殺器可以解決以上問題,

http://sandbox.runjs.cn/show/s3dqvlfk

 1 _start: function (e) {
 2   if (!this.enabled || (this.initiated && utils.eventType[e.type] !== this.initiated)) {
 3     return;
 4   }
 5 
 6 
 7   //暫時只考慮input問題,有效再擴展
 8   var el = document.activeElement;
 9   if (el.nodeName.toLowerCase() == 'input') {
10     return;
11   }
12 
13 
14   var point = e.touches ? e.touches[0] : e, pos;
15   this.initiated = utils.eventType[e.type];
16 
17   this.moved = false;
18 
19   this.distY = 0;
20 
21   //開啟動畫時間,如果之前有動畫的話,便要停止動畫,這裏因為沒有傳時間,所以動畫便直接停止了
22   this._transitionTime();
23 
24   this.startTime = utils.getTime();
25 
26   //如果正在進行動畫,需要停止,並且觸發滑動結束事件
27   if (this.isInTransition) {
28     this.isInTransition = false;
29     pos = this.getComputedPosition();
30     var _x = Math.round(pos.x);
31     var _y = Math.round(pos.y);
32 
33     if (_y < 0 && _y > this.maxScrollY && this.options.adjustXY) {
34       _y = this.options.adjustXY.call(this, _x, _y).y;
35     }
36 
37     //移動過去
38     this._translate(_x, _y);
39     this._execEvent('scrollEnd');
40   }
41 
42   this.startx = this.x;
43   this.startY = this.y;
44   this.absStartX = this.x;
45   this.absStartY = this.y;
46   this.pointX = point.pageX;
47   this.pointY = point.pageY;
48 
49   this._execEvent('beforeScrollStart');
50 
51   e.preventDefault();
52 
53 },
每次touchStart的時候若是發現當前獲得焦點的是input,便不予理睬了,這個時候滑動效果是系統滑動,各位可以一試

結語

關於IScroll的研究暫時告一段落,希望此文對各位有幫助,經過這次的深入學習同時也對小釵的一些問題得到了處理

延伸阅读

    评论