【iScroll源碼學習04】分離IScroll核心

前言

最近幾天咱們前先後後基本將iScroll源碼學的七七八八了,文章中未涉及的各位就要本身去看了javascript

咱們學習源碼的目的毫不是學習人家的源碼,而是由高手的代碼裏面學習思想,或者研究解決方案,就拿咱們此次學習iScroll,個人目的就是css

「抄襲」,我今天就針對本身的需求來抄襲iScroll的源碼,組成本身的庫,而後用於項目,而後高高興興的裝B.......html

關係圖

工具類

第一,咱們將iScroll的工具類搬離出來,可是咱們系統是必定支持CSS3動畫的,因此requestAnimationFrame方法咱們就不予理睬了java

第二,由於咱們的系統是依賴於zepto的,甚至還用到了backbone(可是backbone與underscore大有被移除的可能,因此只用zepto就好)node

屬性總覽

_elementStyle

用於後面檢測CSS3 兼容屬性web

_vendor

CSS3 兼容前綴chrome

hasTouch

是否支持touch事件ubuntu

style

幾個CSS3 動畫屬性,好比在chrome下面是這樣的瀏覽器

Object {
transform: "webkitTransform", 
transitionTimingFunction: "webkitTransitionTimingFunction",
transitionDuration: "webkitTransitionDuration",
transitionDelay: "webkitTransitionDelay", 
transformOrigin: "webkitTransformOrigin"
}

eventType

touch事件的話值就是1,mouse事件即是2,這個對後面有影響的緩存

ease

這個是CSS3的動畫參數,會造成動畫曲線,各位本身去看吧

_prefixStyle

會用到的私有方法,會返回CSS3兼容前綴

getTime

獲取當前時間戳

addEvent

爲dom綁定事件,注意其中的fn是對象,具體處理在handleEvent裏面

removeEvent

爲dom註銷事件,注意其中的fn是對象,具體處理在handleEvent裏面

momentum

這個對象中最難的一個BUG,也很重要,他會根據咱們的拖動返回運動的長度與耗時,這個是根據物理公式計算而出的,十分靠譜,哥是沒看懂用什麼公式的

樣式兼容

因而咱們開始吧,首先,由於各個瀏覽器問題,咱們須要作CSS3動畫的兼容

 1 var _elementStyle = document.createElement('div').style;
 2 //得到CSS3前綴
 3 var _vendor = (function () {
 4   var vendors = ['t', 'webkitT', 'MozT', 'msT', 'OT'];
 5   var transform;
 6   var i = 0;
 7   var l = vendors.length;
 8 
 9   for (; i < l; i++) {
10     transform = vendors[i] + 'ransform';
11     if (transform in _elementStyle) return vendors[i].substr(0, vendors[i].length - 1);
12   }
13   return false;
14 })();

這句代碼會返回須要作兼容的CSS屬性的前綴,而後提供一個方法特別的幹這個事情:

1 //獲取樣式(CSS3兼容)
2 function _prefixStyle(style) {
3   if (_vendor === false) return false;
4   if (_vendor === '') return style;
5   return _vendor + style.charAt(0).toUpperCase + style.substr(1);
6 }

獲取當前時間戳

得到當前時間戳這個方法比較簡單,這個會在計算動畫用到:

me.getTime = Date.now || function getTime() { return new Date().getTime(); };

動畫數據

 1 me.momentum = function (current, start, time, lowerMargin, wrapperSize) {
 2       var distance = current - start,
 3         speed = Math.abs(distance) / time,
 4         destination,
 5         duration,
 6         deceleration = 0.0006;
 7 
 8       destination = current + (speed * speed) / (2 * deceleration) * (distance < 0 ? -1 : 1);
 9       duration = speed / deceleration;
10 
11       if (destination < lowerMargin) {
12         destination = wrapperSize ? lowerMargin - (wrapperSize / 2.5 * (speed / 8)) : lowerMargin;
13         distance = Math.abs(destination - current);
14         duration = distance / speed;
15       } else if (destination > 0) {
16         destination = wrapperSize ? wrapperSize / 2.5 * (speed / 8) : 0;
17         distance = Math.abs(current) + destination;
18         duration = distance / speed;
19       }
20 
21       return {
22         destination: Math.round(destination),
23         duration: duration
24       };
25     };

這個方法尤爲關鍵,是iScroll動畫平滑的一大功臣,這個方法內部用到了物理一個計算速度的公式,我可恥的忘了是什麼了,這裏直接不予關注了,解釋下參數便可

current:當前鼠標位置
start:touchStart時候記錄的Y(多是X)的開始位置,可是在touchmove時候可能被重寫
time: touchstart到手指離開時候經歷的時間,一樣可能被touchmove重寫
lowerMargin:y可移動的最大距離,這個通常爲計算得出 this.wrapperHeight - this.scrollerHeight
wrapperSize:若是有邊界距離的話就是可拖動,否則碰到0的時候便中止

這個動畫方法,咱們後面用到還須要再說下

屬性檢測

 1 //咱們暫時只判斷touch 和 mouse便可
 2 me.extend(me.style = {}, {
 3   hasTouch: 'ontouchstart' in window,
 4   transform: _prefixStyle('transform'),
 5   transitionTimingFunction: _prefixStyle('transitionTimingFunction'),
 6   transitionDuration: _prefixStyle('transitionDuration'),
 7   transitionDelay: _prefixStyle('transitionDelay'),
 8   transformOrigin: _prefixStyle('transformOrigin')
 9 });
10 
11 me.extend(me.eventType = {}, {
12   touchstart: 1,
13   touchmove: 1,
14   touchend: 1,
15 
16   mousedown: 2,
17   mousemove: 2,
18   mouseup: 2
19 });
20 
21 me.extend(me.ease = {}, {
22   quadratic: {
23     style: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
24     fn: function (k) {
25       return k * (2 - k);
26     }
27   },
28   circular: {
29     style: 'cubic-bezier(0.1, 0.57, 0.1, 1)', // Not properly "circular" but this looks better, it should be (0.075, 0.82, 0.165, 1)
30     fn: function (k) {
31       return Math.sqrt(1 - (--k * k));
32     }
33   },
34   back: {
35     style: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)',
36     fn: function (k) {
37       var b = 4;
38       return (k = k - 1) * k * ((b + 1) * k + b) + 1;
39     }
40   },
41   bounce: {
42     style: '',
43     fn: function (k) {
44       if ((k /= 1) < (1 / 2.75)) {
45         return 7.5625 * k * k;
46       } else if (k < (2 / 2.75)) {
47         return 7.5625 * (k -= (1.5 / 2.75)) * k + 0.75;
48       } else if (k < (2.5 / 2.75)) {
49         return 7.5625 * (k -= (2.25 / 2.75)) * k + 0.9375;
50       } else {
51         return 7.5625 * (k -= (2.625 / 2.75)) * k + 0.984375;
52       }
53     }
54   },
55   elastic: {
56     style: '',
57     fn: function (k) {
58       var f = 0.22,
59         e = 0.4;
60 
61       if (k === 0) { return 0; }
62       if (k == 1) { return 1; }
63 
64       return (e * Math.pow(2, -10 * k) * Math.sin((k - f / 4) * (2 * Math.PI) / f) + 1);
65     }
66   }
67 });
View Code

ease時動畫平滑度相關的東西,咱們暫時用着,後面看看能夠用zepto或者CSS3默認的屬性以減小代碼量,工具類到此爲止

IScroll

IScroll類是咱們的關鍵,貫穿其中的是兩個事件機制:

① 原生的touch(鼠標)事件

② 系統自建的異步事件模型

起始點是touchstart,其中的動畫又有不少開關,咱們下面會慢慢涉及到,這裏先說下他的簡單屬性

基本屬性

wrapper

基本外殼,這個dom的style屬性通常是 position: absolute; overflow: hidden;是咱們內部滾動條的外殼

scroller

咱們真正滾動的元素,咱們拖動的就是他

PS:這裏要求必須傳入這兩個DOM結構,否則就是錯

scrollerStyle

scroller 的 Style對象,經過set他的屬性改變樣式

options

設置的基本參數信息

this.options = {
  //是否具備滾動條
  scrollbars: true,
  // 其實時期Y的位置
  startY: 0,
  //超出邊界還原時間點
  bounceTime: 600,
  //超出邊界返回的動畫
  bounceEasing: utils.ease.circular,

  //超出邊界時候是否還能拖動
  bounce: true,

  //當window觸發resize事件60ms後還原
  resizePolling: 60,
  startX: 0,
  startY: 0
};

這裏的startY值得關注,我這裏實際上是想將X相關屬性去掉,可是尚未徹底去掉,這個後面點重構代碼時候搞掉吧

x/y

當前屬性的x/y座標,用於後面touch事件

_events

保存系統中註冊的事件,這裏咱們作個統計,系統一個註冊了這些事件:

this.on('scrollEnd', function () {
  this.indicator.fade();
});

var scope = this;
this.on('scrollCancel', function () {
  scope.indicator.fade();
});

this.on('scrollStart', function () {
  scope.indicator.fade(1);
});

this.on('beforeScrollStart', function () {
  scope.indicator.fade(1, true);
});

this.on('refresh', function () {
  scope.indicator.refresh();
});

① scrollEnd

這個事件在系統中主要用於改變滾動條的透明度

在touchstart時候由於會中止動畫因此會觸發,可是由於touchstart結束後又會觸發move因此滾動條仍是會顯示

在touchend時候會中止動畫,固然也會觸發

transitionend事件觸發時候,動畫真正的中止,scrollEnd也會觸發

② scrollCancel

當touchend時候如果認定是一次點擊事件便會觸發該事件,將滾動條透明度設置爲透明

③ scrollStart

當咱們手指開始滑動時候,就會觸發該事件,將滾動條透明度設置爲可見

④ beforeScrollStart

當手指觸屏屏幕便會將透明度設置爲非透明

這個有一個判斷點就是連續拖動纔會設置非透明,若是首次觸屏必定要拖動纔會顯示

⑤ refresh

在觸發系統refresh方法時候會該事件,這個事件會觸發滾動條的refresh事件,改變其位置,以及相關的尺寸(由於滾動條的尺寸根據他而來)

_initEvents

這個爲IScroll滾動區域設定了基本的DOM事件,有方向改變和尺寸改變的resize事件
而後註冊了中止動畫時候觸發的事件,以及基礎三個事件點touchstart/touchmove/touchend

_start/_move/_end

touch時候執行的方法,這幾個方法咱們後面詳細解析

_resize

當咱們初始化時候會計算wrapper與scroller的尺寸,resize時候也會執行,這裏不予關注

_transitionTime

該方法會設置運動時間,不傳的話運動時間爲0 ,即沒有動畫

getComputedPosition

得到一個DOM的實時樣式樣式,在touchstart時候保留DOM樣式狀態十分有用

_initIndicator

該方法用於初始化滾動條(這個方法位置其實該靠前),他會初始化一個滾動條,而後在本身五個事件狀態下依次執行滾動條的一個方法

_translate

該方法比較重要
他會根據傳入的x,y值設置translate屬性,而後就會產生動畫,完了調用滾動條的updatePosition方法更新其位置

resetPosition

該方法用於當超出邊界時候要還原scroller位置的方法

scrollTo

該方法比較關鍵,實際上是由他調用_translate改變scroller具體位置,其中能夠傳入一些時間以及動畫曲線的參數進行動畫
這個時候若是touchstart觸屏了話便會中止動畫,實現方式是_transitionTime()

disable/enable

統一的開關

on

這個用於註冊事件,咱們以前作過介紹

_execEvent

觸發註冊的事件

destroy

銷燬註冊的DOM事件

_transitionEnd

CSS3動畫結束時候會觸發的事件,這個裏面會將isInTransition設置爲false,這是一個動畫的重要參數依據,咱們後面要詳細說

handleEvent

具體事件執行的地方,可是真正的邏輯又分散到了上述的各個方法

至此,IScroll的屬性就介紹完畢了,咱們下面抽出核心的作介紹

初始化方法

refresh

該方法其實應該屬於系統第一步,這個方法會讓緩存IScroll中的幾個DOM尺寸,動畫可能是和尺寸打關係嘛

 1 refresh: function () {
 2   var rf = this.wrapper.offsetHeight;     // Force reflow
 3 
 4   this.wrapperHeight = this.wrapper.clientHeight;
 5   this.scrollerHeight = this.scroller.offsetHeight;
 6   this.maxScrollY = this.wrapperHeight - this.scrollerHeight;
 7 
 8   this.endTime = 0;
 9 
10   var offset = $(this.wrapper).offset();
11 
12   this.wrapperOffset = {
13     left: offset.left * (-1),
14     top: offset.top * (-1)
15   };
16 
17   this._execEvent('refresh');
18 
19   this.resetPosition();
20 
21 },

首先作了一步操做讓下面的操做不會發生重繪

var rf = this.wrapper.offsetHeight;

而後初始化了幾個關鍵DOM的高度,而且得到maxScrollY(真正使用的時候會/3)

接下來重置了endtime爲0,而且設置一些基本參數後觸發refresh事件(會觸發滾動條的refresh)
固然,若是超出了邊界的話,會resetPosition,重置位置
refresh自己並不複雜,可是對後面的影響比較大,算是初始化第一步的工做

_initIndicator

根據該方法會初始化一個滾動條,固然咱們能夠配置參數不讓滾動條出現

 1 _initIndicator: function () {
 2   //滾動條
 3   var el = createDefaultScrollbar();
 4   this.wrapper.appendChild(el);
 5   this.indicator = new Indicator(this, { el: el });
 6 
 7   this.on('scrollEnd', function () {
 8     this.indicator.fade();
 9   });
10 
11   var scope = this;
12   this.on('scrollCancel', function () {
13     scope.indicator.fade();
14   });
15 
16   this.on('scrollStart', function () {
17     scope.indicator.fade(1);
18   });
19 
20   this.on('beforeScrollStart', function () {
21     scope.indicator.fade(1, true);
22   });
23 
24   this.on('refresh', function () {
25     scope.indicator.refresh();
26   });
27 
28 },

由於滾動條的類,咱們後面會詳細說,這裏便不關注了

_resize

在咱們窗口變化時候,或者豎着的屏幕橫着時候便會觸發該事件,他會執行咱們的refresh方法,重置頁面信息
PS:其實就動畫一塊,IScroll邏輯是很嚴密的

1 _resize: function () {
2   var that = this;
3 
4   clearTimeout(this.resizeTimeout);
5 
6   this.resizeTimeout = setTimeout(function () {
7     that.refresh();
8   }, this.options.resizePolling);
9 },

resetPosition

 1 resetPosition: function (time) {
 2   var x = this.x,
 3 y = this.y;
 4 
 5   time = time || 0;
 6 
 7   if (this.y > 0) {
 8     y = 0;
 9   } else if (this.y < this.maxScrollY) {
10     y = this.maxScrollY;
11   }
12 
13   if (y == this.y) {
14     return false;
15   }
16 
17   this.scrollTo(x, y, time, this.options.bounceEasing);
18 
19   return true;
20 },

在refresh時候會觸發(由於可能致使scroller超出邊界),在touchend時候超出邊界也會使用該方法,這個時候重置邊界會有動畫(600ms)

動畫結束若是檢測到超出邊界也會執行


運動相關

_transitionTime

 1 _transitionTime: function (time) {
 2   time = time || 0;
 3   this.scrollerStyle[utils.style.transitionDuration] = time + 'ms';
 4 
 5   //滾動條,咱們這裏只會出現一個滾動條就不搞那麼複雜了
 6   this.indicator && this.indicator.transitionTime(time);
 7 
 8 },
 9 
10 getComputedPosition: function () {
11   var matrix = window.getComputedStyle(this.scroller, null), x, y;
12 
13   matrix = matrix[utils.style.transform].split(')')[0].split(', ');
14   x = +(matrix[12] || matrix[4]);
15   y = +(matrix[13] || matrix[5]);
16 
17   return { x: x, y: y };
18 },

該方法用於重置CSS3的時間參數屬性,設置爲0的話就不會具備動畫,有一點須要注意的是,滾動條老是與他是同步的

_transitionTimingFunction

1 _transitionTimingFunction: function (easing) {
2   this.scrollerStyle[utils.style.transitionTimingFunction] = easing;
3 
4   this.indicator && this.indicator.transitionTimingFunction(easing);
5 },

該方法用於重置CSS3的時間曲線,這個我沒搞懂,滾動條一樣是同步的

_translate

 1 _translate: function (x, y) {
 2 
 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 },

該方法會改變DOM的樣式,可是其動畫由CSS3的 transitionDuration 控制,這裏改變了scroller樣式會同步其滾動條的

scrollTo

該方法是以上三個方法的合集,除了_transitionTime會在外部常常出現以中止動畫外,其它方法基本在該方法內部使用

 1 scrollTo: function (x, y, time, easing) {
 2   easing = easing || utils.ease.circular;
 3 
 4   this.isInTransition = time > 0;
 5 
 6   if (!time || easing.style) {
 7     this._transitionTimingFunction(easing.style);
 8     this._transitionTime(time);
 9     this._translate(x, y);
10   }
11 },

由於滾動條的同步在各個子類方法中了,因此這個方法比較簡單了,其邏輯進入了各個子方法

如此整個繼承的方法介紹結束,咱們開始對三大核心事件進行解析

三大事件核心

說到三大核心就必須將isInTransition單獨提出來,他只有0,1兩個值,可是是記錄DOM是否處於運動的關鍵

start

 1 _start: function (e) {
 2   if (!this.enabled || (this.initiated && utils.eventType[e.type] !== this.initiated)) {
 3     return;
 4   }
 5 
 6   var point = e.touches ? e.touches[0] : e, pos;
 7   this.initiated = utils.eventType[e.type];
 8 
 9   this.moved = false;
10 
11   this.distY = 0;
12 
13   //開啓動畫時間,若是以前有動畫的話,便要中止動畫,這裏由於沒有傳時間,因此動畫便直接中止了
14   this._transitionTime();
15 
16   this.startTime = utils.getTime();
17 
18   //若是正在進行動畫,須要中止,而且觸發滑動結束事件
19   if (this.isInTransition) {
20     this.isInTransition = false;
21     pos = this.getComputedPosition();
22 
23     //移動過去
24     this._translate(Math.round(pos.x), Math.round(pos.y));
25     this._execEvent('scrollEnd');
26   }
27 
28   this.startX = this.x;
29   this.startY = this.y;
30   this.absStartX = this.x;
31   this.absStartY = this.y;
32   this.pointX = point.pageX;
33   this.pointY = point.pageY;
34 
35   this._execEvent('beforeScrollStart');
36 
37   e.preventDefault();
38 
39 },

在開始階段,首先將moved屬性設置爲false,而後中止動畫,若是當前isInTransition值爲1,須要將他設置爲false而且保持當前的狀態

PS:這一步是中止動畫而且保持當前狀態的關鍵,這裏會觸發scrollEnd事件,滾動條會有所聯動

在作一些初始化操做後,該方法結束,這個裏面的核心就是中止動畫而且作初始化操做

move

該階段是第二核心,這裏會根據移動得到新的座標開始移動,若是超出邊界會作一個處理,移動的值不會超過1/3個MaxY
一個關鍵點是會觸發scrollStart事件,讓滾動條可見
另外一個關鍵點是沒過300ms會重置開始狀態值,因此就算使勁按着屏幕不放,忽然放開,DOM仍然會移動很遠

end

 1 _end: function (e) {
 2   if (!this.enabled || utils.eventType[e.type] !== this.initiated) {
 3     return;
 4   }
 5 
 6   var point = e.changedTouches ? e.changedTouches[0] : e,
 7   momentumY,
 8   duration = utils.getTime() - this.startTime,
 9   newX = Math.round(this.x),
10   newY = Math.round(this.y),
11   distanceX = Math.abs(newX - this.startX),
12   distanceY = Math.abs(newY - this.startY),
13   time = 0,
14   easing = '';
15 
16   this.isInTransition = 0;
17   this.initiated = 0;
18   this.endTime = utils.getTime();
19 
20   if (this.resetPosition(this.options.bounceTime)) {
21     return;
22   }
23 
24   this.scrollTo(newX, newY);
25   if (!this.moved) {
26     //click 的狀況
27 
28     this._execEvent('scrollCancel');
29     return;
30   }
31 
32   if (duration < 300) {
33 
34     momentumY = utils.momentum(this.y, this.startY, duration, this.maxScrollY, this.options.bounce ? this.wrapperHeight : 0);
35     //      newX = momentumX.destination;
36     newY = momentumY.destination;
37     time = Math.max(momentumY.duration);
38     this.isInTransition = 1;
39   }
40 
41   if (newY != this.y) {
42     if (newY > 0 || newY < this.maxScrollY) {
43       easing = utils.ease.quadratic;
44     }
45     this.scrollTo(newX, newY, time, easing);
46     return;
47   }
48 
49   this._execEvent('scrollEnd');
50 },
View Code

end階段首先會將isInTransition設置爲0,若是超出邊界會進行處理,進行動畫又會將 isInTransition 設置爲1,在動畫事件結束後設置爲0

期間還會處理一些其餘狀況,咱們不予理睬,重點就是使用momentum獲取了當前運動參數
而後開始運動,此處邏輯所有分解開了,這裏反而顯得不難了

至此,整個核心塊IScroll便分解結束了,咱們最後看看滾動條相關

Indicator

由於咱們對滾動條的需求變更簡單,咱們的滾動條如今就真的很簡單了,徹底與scroller是一個從屬關係,隨着scroller而變化
有興趣的朋友本身去看看吧,我這裏就無論他了

源碼:

  1 <!DOCTYPE html>
  2 <html>
  3 <head>
  4 <meta charset="utf-8">
  5 <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0, minimum-scale=1.0, maximum-scale=1.0">
  6 
  7 <title>iScroll demo: scrollbars</title>
  8 
  9 
 10 <style type="text/css">
 11 * {
 12     -webkit-box-sizing: border-box;
 13     -moz-box-sizing: border-box;
 14     box-sizing: border-box;
 15 }
 16 
 17 html {
 18     -ms-touch-action: none;
 19 }
 20 
 21 body,ul,li {
 22     padding: 0;
 23     margin: 0;
 24     border: 0;
 25 }
 26 
 27 body {
 28     font-size: 12px;
 29     font-family: ubuntu, helvetica, arial;
 30     overflow: hidden; /* this is important to prevent the whole page to bounce */
 31 }
 32 
 33 #header {
 34     position: absolute;
 35     z-index: 2;
 36     top: 0;
 37     left: 0;
 38     width: 100%;
 39     height: 45px;
 40     line-height: 45px;
 41     background: #CD235C;
 42     padding: 0;
 43     color: #eee;
 44     font-size: 20px;
 45     text-align: center;
 46     font-weight: bold;
 47 }
 48 
 49 #footer {
 50     position: absolute;
 51     z-index: 2;
 52     bottom: 0;
 53     left: 0;
 54     width: 100%;
 55     height: 48px;
 56     background: #444;
 57     padding: 0;
 58     border-top: 1px solid #444;
 59 }
 60 
 61 #wrapper {
 62     position: absolute;
 63     z-index: 1;
 64     top: 45px;
 65     bottom: 48px;
 66     left: 0;
 67     width: 100%;
 68     background: #ccc;
 69     overflow: hidden;
 70 }
 71 
 72 #scroller {
 73     position: absolute;
 74     z-index: 1;
 75     -webkit-tap-highlight-color: rgba(0,0,0,0);
 76     width: 100%;
 77     -webkit-transform: translateZ(0);
 78     -moz-transform: translateZ(0);
 79     -ms-transform: translateZ(0);
 80     -o-transform: translateZ(0);
 81     transform: translateZ(0);
 82     -webkit-touch-callout: none;
 83     -webkit-user-select: none;
 84     -moz-user-select: none;
 85     -ms-user-select: none;
 86     user-select: none;
 87     -webkit-text-size-adjust: none;
 88     -moz-text-size-adjust: none;
 89     -ms-text-size-adjust: none;
 90     -o-text-size-adjust: none;
 91     text-size-adjust: none;
 92 }
 93 
 94 #scroller ul {
 95     list-style: none;
 96     padding: 0;
 97     margin: 0;
 98     width: 100%;
 99     text-align: left;
100 }
101 
102 #scroller li {
103     padding: 0 10px;
104     height: 40px;
105     line-height: 40px;
106     border-bottom: 1px solid #ccc;
107     border-top: 1px solid #fff;
108     background-color: #fafafa;
109     font-size: 14px;
110 }
111 
112 </style>
113 </head>
114 <body  >
115 <div id="header">抄襲IScroll</div>
116 
117 <div id="wrapper">
118     <div id="scroller">
119         <ul>
120             <li>Pretty row 1</li>
121             <li>Pretty row 2</li>
122             <li>Pretty row 3</li>
123             <li>Pretty row 4</li>
124             <li>Pretty row 5</li>
125             <li>Pretty row 6</li>
126             <li>Pretty row 7</li>
127             <li>Pretty row 8</li>
128             <li>Pretty row 9</li>
129             <li>Pretty row 10</li>
130             <li>Pretty row 11</li>
131             <li>Pretty row 12</li>
132             <li>Pretty row 13</li>
133             <li>Pretty row 14</li>
134             <li>Pretty row 15</li>
135             <li>Pretty row 16</li>
136             <li>Pretty row 17</li>
137             <li>Pretty row 18</li>
138             <li>Pretty row 19</li>
139             <li>Pretty row 20</li>
140             <li>Pretty row 21</li>
141             <li>Pretty row 22</li>
142             <li>Pretty row 23</li>
143             <li>Pretty row 24</li>
144             <li>Pretty row 25</li>
145             <li>Pretty row 26</li>
146             <li>Pretty row 27</li>
147             <li>Pretty row 28</li>
148             <li>Pretty row 29</li>
149             <li>Pretty row 30</li>
150             <li>Pretty row 31</li>
151             <li>Pretty row 32</li>
152             <li>Pretty row 33</li>
153             <li>Pretty row 34</li>
154             <li>Pretty row 35</li>
155             <li>Pretty row 36</li>
156             <li>Pretty row 37</li>
157             <li>Pretty row 38</li>
158             <li>Pretty row 39</li>
159             <li>Pretty row 40</li>
160             <li>Pretty row 41</li>
161             <li>Pretty row 42</li>
162             <li>Pretty row 43</li>
163             <li>Pretty row 44</li>
164             <li>Pretty row 45</li>
165             <li>Pretty row 46</li>
166             <li>Pretty row 47</li>
167             <li>Pretty row 48</li>
168             <li>Pretty row 49</li>
169             <li>Pretty row 50</li>
170         </ul>
171     </div>
172 </div>
173 
174 <div id="footer"></div>
175 
176   <script src="zepto.js" type="text/javascript"></script>
177   <script src="MyIscroll.js" type="text/javascript"></script>
178 
179   <script type="text/javascript">
180 
181     var s = new IScroll({
182       wrapper: $('#wrapper'),
183       scroller: $('#scroller')
184     });
185 
186   </script>
187 </body>
188 </html>
View Code
  1 (function (window, document, Math) {
  2 
  3   var utils = (function () {
  4     var me = {};
  5     var _elementStyle = document.createElement('div').style;
  6 
  7     //得到須要兼容CSS3前綴
  8     var _vendor = (function () {
  9       var vendors = ['t', 'webkitT', 'MozT', 'msT', 'OT'];
 10       var transform;
 11       var i = 0;
 12       var l = vendors.length;
 13 
 14       for (; i < l; i++) {
 15         transform = vendors[i] + 'ransform';
 16         if (transform in _elementStyle) return vendors[i].substr(0, vendors[i].length - 1);
 17       }
 18       return false;
 19     })();
 20 
 21     //獲取樣式(CSS3兼容)
 22     function _prefixStyle(style) {
 23       if (_vendor === false) return false;
 24       if (_vendor === '') return style;
 25       return _vendor + style.charAt(0).toUpperCase() + style.substr(1);
 26     }
 27 
 28     me.getTime = Date.now || function getTime() { return new Date().getTime(); };
 29 
 30     me.addEvent = function (el, type, fn, capture) {
 31       if (el[0]) el = el[0];
 32       el.addEventListener(type, fn, !!capture);
 33     };
 34 
 35     me.removeEvent = function (el, type, fn, capture) {
 36       if (el[0]) el = el[0];
 37       el.removeEventListener(type, fn, !!capture);
 38     };
 39 
 40     /*
 41     current:當前鼠標位置
 42     start:touchStart時候記錄的Y(多是X)的開始位置,可是在touchmove時候可能被重寫
 43     time: touchstart到手指離開時候經歷的時間,一樣可能被touchmove重寫
 44     lowerMargin:y可移動的最大距離,這個通常爲計算得出 this.wrapperHeight - this.scrollerHeight
 45     wrapperSize:若是有邊界距離的話就是可拖動,否則碰到0的時候便中止
 46     */
 47     me.momentum = function (current, start, time, lowerMargin, wrapperSize) {
 48       var distance = current - start,
 49         speed = Math.abs(distance) / time,
 50         destination,
 51         duration,
 52         deceleration = 0.0006;
 53 
 54       destination = current + (speed * speed) / (2 * deceleration) * (distance < 0 ? -1 : 1);
 55       duration = speed / deceleration;
 56 
 57       if (destination < lowerMargin) {
 58         destination = wrapperSize ? lowerMargin - (wrapperSize / 2.5 * (speed / 8)) : lowerMargin;
 59         distance = Math.abs(destination - current);
 60         duration = distance / speed;
 61       } else if (destination > 0) {
 62         destination = wrapperSize ? wrapperSize / 2.5 * (speed / 8) : 0;
 63         distance = Math.abs(current) + destination;
 64         duration = distance / speed;
 65       }
 66 
 67       return {
 68         destination: Math.round(destination),
 69         duration: duration
 70       };
 71     };
 72 
 73     $.extend(me, {
 74       hasTouch: 'ontouchstart' in window
 75     });
 76 
 77 
 78     //咱們暫時只判斷touch 和 mouse便可
 79     $.extend(me.style = {}, {
 80       transform: _prefixStyle('transform'),
 81       transitionTimingFunction: _prefixStyle('transitionTimingFunction'),
 82       transitionDuration: _prefixStyle('transitionDuration'),
 83       transitionDelay: _prefixStyle('transitionDelay'),
 84       transformOrigin: _prefixStyle('transformOrigin')
 85     });
 86 
 87     $.extend(me.eventType = {}, {
 88       touchstart: 1,
 89       touchmove: 1,
 90       touchend: 1,
 91 
 92       mousedown: 2,
 93       mousemove: 2,
 94       mouseup: 2
 95     });
 96 
 97     $.extend(me.ease = {}, {
 98       quadratic: {
 99         style: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
100         fn: function (k) {
101           return k * (2 - k);
102         }
103       },
104       circular: {
105         style: 'cubic-bezier(0.1, 0.57, 0.1, 1)', // Not properly "circular" but this looks better, it should be (0.075, 0.82, 0.165, 1)
106         fn: function (k) {
107           return Math.sqrt(1 - (--k * k));
108         }
109       },
110       back: {
111         style: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)',
112         fn: function (k) {
113           var b = 4;
114           return (k = k - 1) * k * ((b + 1) * k + b) + 1;
115         }
116       },
117       bounce: {
118         style: '',
119         fn: function (k) {
120           if ((k /= 1) < (1 / 2.75)) {
121             return 7.5625 * k * k;
122           } else if (k < (2 / 2.75)) {
123             return 7.5625 * (k -= (1.5 / 2.75)) * k + 0.75;
124           } else if (k < (2.5 / 2.75)) {
125             return 7.5625 * (k -= (2.25 / 2.75)) * k + 0.9375;
126           } else {
127             return 7.5625 * (k -= (2.625 / 2.75)) * k + 0.984375;
128           }
129         }
130       },
131       elastic: {
132         style: '',
133         fn: function (k) {
134           var f = 0.22,
135         e = 0.4;
136 
137           if (k === 0) { return 0; }
138           if (k == 1) { return 1; }
139 
140           return (e * Math.pow(2, -10 * k) * Math.sin((k - f / 4) * (2 * Math.PI) / f) + 1);
141         }
142       }
143     });
144     return me;
145   })();
146 
147   function IScroll(opts) {
148     this.wrapper = typeof opts.wrapper == 'string' ? $(opts.wrapper) : opts.wrapper;
149     this.scroller = typeof opts.scroller == 'string' ? $(opts.scroller) : opts.scroller;
150     if (!opts.wrapper[0] || !opts.scroller[0]) throw 'param error';
151 
152     this.wrapper = this.wrapper[0];
153     this.scroller = this.scroller[0];
154 
155     //這個屬性會被動態改變的,若是這裏
156     this.scrollerStyle = this.scroller.style;
157 
158     this.options = {
159       //是否具備滾動條
160       scrollbars: true,
161       // 其實時期Y的位置
162       startY: 0,
163       //超出邊界還原時間點
164       bounceTime: 600,
165       //超出邊界返回的動畫
166       bounceEasing: utils.ease.circular,
167 
168       //超出邊界時候是否還能拖動
169       bounce: true,
170 
171       //當window觸發resize事件60ms後還原
172       resizePolling: 60,
173       startX: 0,
174       startY: 0
175     };
176 
177     for (var i in opts) {
178       this.options[i] = opts[i];
179     }
180 
181     this.translateZ = ' translateZ(0)';
182 
183     this.x = 0;
184     this.y = 0;
185     this._events = {};
186     this._init();
187 
188     //更新滾動條位置
189     this.refresh();
190 
191     //更新自己位置
192     this.scrollTo(this.options.startX, this.options.startY);
193 
194     this.enable();
195 
196   };
197 
198   IScroll.prototype = {
199     _init: function () {
200       this._initEvents();
201 
202       //初始化滾動條,滾動條此處須要作重要處理
203       if (this.options.scrollbars) {
204         this._initIndicator();
205       }
206     },
207     refresh: function () {
208       var rf = this.wrapper.offsetHeight;     // Force reflow
209 
210       this.wrapperHeight = this.wrapper.clientHeight;
211       this.scrollerHeight = this.scroller.offsetHeight;
212       this.maxScrollY = this.wrapperHeight - this.scrollerHeight;
213 
214       this.endTime = 0;
215 
216       this._execEvent('refresh');
217 
218       this.resetPosition();
219 
220     },
221     _initEvents: function (remove) {
222       var eventType = remove ? utils.removeEvent : utils.addEvent;
223       var target = this.options.bindToWrapper ? this.wrapper : window;
224 
225       eventType(window, 'orientationchange', this);
226       eventType(window, 'resize', this);
227 
228       if (utils.hasTouch) {
229         eventType(this.wrapper, 'touchstart', this);
230         eventType(target, 'touchmove', this);
231         eventType(target, 'touchcancel', this);
232         eventType(target, 'touchend', this);
233       } else {
234         eventType(this.wrapper, 'mousedown', this);
235         eventType(target, 'mousemove', this);
236         eventType(target, 'mousecancel', this);
237         eventType(target, 'mouseup', this);
238       }
239 
240       eventType(this.scroller, 'transitionend', this);
241       eventType(this.scroller, 'webkitTransitionEnd', this);
242       eventType(this.scroller, 'oTransitionEnd', this);
243       eventType(this.scroller, 'MSTransitionEnd', this);
244     },
245     _start: function (e) {
246       if (!this.enabled || (this.initiated && utils.eventType[e.type] !== this.initiated)) {
247         return;
248       }
249 
250       var point = e.touches ? e.touches[0] : e, pos;
251       this.initiated = utils.eventType[e.type];
252 
253       this.moved = false;
254 
255       this.distY = 0;
256 
257       //開啓動畫時間,若是以前有動畫的話,便要中止動畫,這裏由於沒有傳時間,因此動畫便直接中止了
258       this._transitionTime();
259 
260       this.startTime = utils.getTime();
261 
262       //若是正在進行動畫,須要中止,而且觸發滑動結束事件
263       if (this.isInTransition) {
264         this.isInTransition = false;
265         pos = this.getComputedPosition();
266 
267         //移動過去
268         this._translate(Math.round(pos.x), Math.round(pos.y));
269         this._execEvent('scrollEnd');
270       }
271 
272       this.startX = this.x;
273       this.startY = this.y;
274       this.absStartX = this.x;
275       this.absStartY = this.y;
276       this.pointX = point.pageX;
277       this.pointY = point.pageY;
278 
279       this._execEvent('beforeScrollStart');
280 
281       e.preventDefault();
282 
283     },
284 
285     _move: function (e) {
286       if (!this.enabled || utils.eventType[e.type] !== this.initiated) {
287         return;
288       }
289 
290       e.preventDefault();
291 
292       var point = e.touches ? e.touches[0] : e,
293       deltaX = point.pageX - this.pointX,
294       deltaY = point.pageY - this.pointY,
295       timestamp = utils.getTime(),
296       newX, newY,
297       absDistX, absDistY;
298 
299       this.pointX = point.pageX;
300       this.pointY = point.pageY;
301 
302       this.distX += deltaX;
303       this.distY += deltaY;
304       absDistX = Math.abs(this.distX);
305       absDistY = Math.abs(this.distY);
306 
307       // 若是一直按着沒反應的話這裏就直接返回了
308       if (timestamp - this.endTime > 300 && (absDistX < 10 && absDistY < 10)) {
309         return;
310       }
311 
312       newY = this.y + deltaY;
313 
314       if (newY > 0 || newY < this.maxScrollY) {
315         newY = this.options.bounce ? this.y + deltaY / 3 : newY > 0 ? 0 : this.maxScrollY;
316       }
317 
318       if (!this.moved) {
319         this._execEvent('scrollStart');
320       }
321 
322       this.moved = true;
323 
324       this._translate(0, newY);
325 
326       if (timestamp - this.startTime > 300) {
327         this.startTime = timestamp;
328         this.startX = this.x;
329         this.startY = this.y;
330       }
331 
332 
333     },
334     _end: function (e) {
335       if (!this.enabled || utils.eventType[e.type] !== this.initiated) {
336         return;
337       }
338 
339       var point = e.changedTouches ? e.changedTouches[0] : e,
340       momentumY,
341       duration = utils.getTime() - this.startTime,
342       newX = Math.round(this.x),
343       newY = Math.round(this.y),
344       distanceX = Math.abs(newX - this.startX),
345       distanceY = Math.abs(newY - this.startY),
346       time = 0,
347       easing = '';
348 
349       this.isInTransition = 0;
350       this.initiated = 0;
351       this.endTime = utils.getTime();
352 
353       if (this.resetPosition(this.options.bounceTime)) {
354         return;
355       }
356 
357       this.scrollTo(newX, newY);
358       if (!this.moved) {
359         //click 的狀況
360 
361         this._execEvent('scrollCancel');
362         return;
363       }
364 
365       if (duration < 300) {
366 
367         momentumY = utils.momentum(this.y, this.startY, duration, this.maxScrollY, this.options.bounce ? this.wrapperHeight : 0);
368         //      newX = momentumX.destination;
369         newY = momentumY.destination;
370         time = Math.max(momentumY.duration);
371         this.isInTransition = 1;
372       }
373 
374       if (newY != this.y) {
375         if (newY > 0 || newY < this.maxScrollY) {
376           easing = utils.ease.quadratic;
377         }
378         this.scrollTo(newX, newY, time, easing);
379         return;
380       }
381 
382       this._execEvent('scrollEnd');
383     },
384 
385     _resize: function () {
386       var that = this;
387 
388       clearTimeout(this.resizeTimeout);
389 
390       this.resizeTimeout = setTimeout(function () {
391         that.refresh();
392       }, this.options.resizePolling);
393     },
394 
395     _transitionTimingFunction: function (easing) {
396       this.scrollerStyle[utils.style.transitionTimingFunction] = easing;
397 
398       this.indicator && this.indicator.transitionTimingFunction(easing);
399     },
400 
401     //開始或者中止動畫
402     _transitionTime: function (time) {
403       time = time || 0;
404       this.scrollerStyle[utils.style.transitionDuration] = time + 'ms';
405 
406       //滾動條,咱們這裏只會出現一個滾動條就不搞那麼複雜了
407       this.indicator && this.indicator.transitionTime(time);
408 
409     },
410 
411     getComputedPosition: function () {
412       var matrix = window.getComputedStyle(this.scroller, null), x, y;
413 
414       matrix = matrix[utils.style.transform].split(')')[0].split(', ');
415       x = +(matrix[12] || matrix[4]);
416       y = +(matrix[13] || matrix[5]);
417 
418       return { x: x, y: y };
419     },
420 
421     _initIndicator: function () {
422       //滾動條
423       var el = createDefaultScrollbar();
424       this.wrapper.appendChild(el);
425       this.indicator = new Indicator(this, { el: el });
426 
427       this.on('scrollEnd', function () {
428         this.indicator.fade();
429       });
430 
431       var scope = this;
432       this.on('scrollCancel', function () {
433         scope.indicator.fade();
434       });
435 
436       this.on('scrollStart', function () {
437         scope.indicator.fade(1);
438       });
439 
440       this.on('beforeScrollStart', function () {
441         scope.indicator.fade(1, true);
442       });
443 
444       this.on('refresh', function () {
445         scope.indicator.refresh();
446       });
447 
448     },
449 
450     //移動x,y這裏比較簡單就不分離y了
451     _translate: function (x, y) {
452 
453       this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ;
454 
455       this.x = x;
456       this.y = y;
457 
458       if (this.options.scrollbars) {
459         this.indicator.updatePosition();
460       }
461 
462     },
463 
464     resetPosition: function (time) {
465       var x = this.x,
466         y = this.y;
467 
468       time = time || 0;
469 
470       if (this.y > 0) {
471         y = 0;
472       } else if (this.y < this.maxScrollY) {
473         y = this.maxScrollY;
474       }
475 
476       if (y == this.y) {
477         return false;
478       }
479 
480       this.scrollTo(x, y, time, this.options.bounceEasing);
481 
482       return true;
483     },
484 
485     //移動
486     scrollTo: function (x, y, time, easing) {
487       easing = easing || utils.ease.circular;
488 
489       this.isInTransition = time > 0;
490 
491       if (!time || easing.style) {
492         this._transitionTimingFunction(easing.style);
493         this._transitionTime(time);
494         this._translate(x, y);
495       }
496     },
497 
498     //統一的關閉接口
499     disable: function () {
500       this.enabled = false;
501     },
502     //統一的open接口
503     enable: function () {
504       this.enabled = true;
505     },
506 
507     on: function (type, fn) {
508       if (!this._events[type]) {
509         this._events[type] = [];
510       }
511 
512       this._events[type].push(fn);
513     },
514 
515     _execEvent: function (type) {
516       if (!this._events[type]) {
517         return;
518       }
519 
520       var i = 0,
521             l = this._events[type].length;
522 
523       if (!l) {
524         return;
525       }
526 
527       for (; i < l; i++) {
528         this._events[type][i].call(this);
529       }
530     },
531     destroy: function () {
532       this._initEvents(true);
533 
534       this._execEvent('destroy');
535     },
536 
537     _transitionEnd: function (e) {
538       if (e.target != this.scroller || !this.isInTransition) {
539         return;
540       }
541 
542       this._transitionTime();
543       if (!this.resetPosition(this.options.bounceTime)) {
544         this.isInTransition = false;
545         this._execEvent('scrollEnd');
546       }
547     },
548 
549     //事件具體觸發點
550     handleEvent: function (e) {
551       switch (e.type) {
552         case 'touchstart':
553         case 'mousedown':
554           this._start(e);
555           break;
556         case 'touchmove':
557         case 'mousemove':
558           this._move(e);
559           break;
560         case 'touchend':
561         case 'mouseup':
562         case 'touchcancel':
563         case 'mousecancel':
564           this._end(e);
565           break;
566         case 'orientationchange':
567         case 'resize':
568           this._resize();
569           break;
570         case 'transitionend':
571         case 'webkitTransitionEnd':
572         case 'oTransitionEnd':
573         case 'MSTransitionEnd':
574           this._transitionEnd(e);
575           break;
576       }
577     }
578 
579   };
580 
581   function createDefaultScrollbar() {
582     var scrollbar = document.createElement('div'),
583         indicator = document.createElement('div');
584 
585     scrollbar.style.cssText = 'position:absolute;z-index:9999';
586     scrollbar.style.cssText += ';width:7px;bottom:2px;top:2px;right:1px';
587     scrollbar.style.cssText += ';overflow:hidden';
588 
589     indicator.style.cssText = '-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;position:absolute;background:rgba(0,0,0,0.5);border:1px solid rgba(255,255,255,0.9);border-radius:3px';
590     indicator.style.width = '100%';
591 
592     scrollbar.appendChild(indicator);
593 
594     return scrollbar;
595   }
596 
597   function Indicator(scroller, opts) {
598     this.wrapper = typeof opts.el == 'string' ? document.querySelector(opts.el) : opts.el;
599     this.indicator = this.wrapper.children[0];
600 
601     this.wrapperStyle = this.wrapper.style;
602     this.indicatorStyle = this.indicator.style;
603     this.scroller = scroller;
604 
605     this.sizeRatioY = 1;
606     this.maxPosY = 0;
607 
608     this.wrapperStyle[utils.style.transform] = this.scroller.translateZ;
609     this.wrapperStyle[utils.style.transitionDuration] = '0ms';
610     this.wrapperStyle.opacity = '0';
611 
612   }
613 
614   Indicator.prototype = {
615     transitionTime: function (time) {
616       time = time || 0;
617       this.indicatorStyle[utils.style.transitionDuration] = time + 'ms';
618     },
619     transitionTimingFunction: function (easing) {
620       this.indicatorStyle[utils.style.transitionTimingFunction] = easing;
621     },
622     refresh: function () {
623 
624       this.transitionTime();
625 
626       var r = this.wrapper.offsetHeight; // force refresh
627 
628       this.wrapperHeight = this.wrapper.clientHeight;
629 
630 
631       this.indicatorHeight = Math.max(Math.round(this.wrapperHeight * this.wrapperHeight / (this.scroller.scrollerHeight || this.wrapperHeight || 1)), 8);
632       this.indicatorStyle.height = this.indicatorHeight + 'px';
633 
634 
635       this.maxPosY = this.wrapperHeight - this.indicatorHeight;
636       this.sizeRatioY = (this.scroller.maxScrollY && (this.maxPosY / this.scroller.maxScrollY));
637 
638       this.updatePosition();
639     },
640     updatePosition: function () {
641       var y = Math.round(this.sizeRatioY * this.scroller.y) || 0;
642       this.y = y;
643 
644       //不須要兼容方式了
645       this.indicatorStyle[utils.style.transform] = 'translate(0px,' + y + 'px)' + this.scroller.translateZ;
646 
647     },
648     fade: function (val, hold) {
649       if (hold && !this.visible) {
650         return;
651       }
652 
653       clearTimeout(this.fadeTimeout);
654       this.fadeTimeout = null;
655 
656       var time = val ? 250 : 500,
657             delay = val ? 0 : 300;
658 
659       val = val ? '1' : '0';
660 
661       this.wrapperStyle[utils.style.transitionDuration] = time + 'ms';
662 
663       this.fadeTimeout = setTimeout((function (val) {
664         this.wrapperStyle.opacity = val;
665         this.visible = +val;
666       }).bind(this, val), delay);
667 
668     }
669   };
670 
671   IScroll.utils = utils;
672 
673 
674   window.IScroll = IScroll;
675 
676 
677 })(window, document, Math);
View Code

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

結語

咱們此次抄襲了IScroll核心代碼,造成了一個簡單的拖動庫,咱們對IScroll的學習可能暫時就到這裏了,接下來一段時間咱們的重心會放到nodeJS上面
中途可能會涉及到requireJS的源碼學習,可是都不會偏離nodeJS的核心

注意,請看最後的代碼......

相關文章
相關標籤/搜索