前言

最近几天我们前前后后基本将iScroll源码学的七七八八了,文章中未涉及的各位就要自己去看了

我们学习源码的目的绝不是学习人家的源码,而是由高手的代码里面学习思想,或者研究解决方案,就拿我们这次学习iScroll,我的目的就是

“抄袭”,我今天就针对自己的需求来抄袭iScroll的源码,组成自己的库,然后用于项目,然后高高兴兴的装B.......

关系图

工具类

第一,我们将iScroll的工具类搬离出来,但是我们系统是一定支持CSS3动画的,所以requestAnimationFrame方法我们就不予理睬了

第二,因为我们的系统是依赖于zepto的,甚至还用到了backbone(但是backbone与underscore大有被移除的可能,所以只用zepto就好)

属性总览

_elementStyle

用于后面检测CSS3 兼容属性

_vendor

CSS3 兼容前缀

hasTouch

是否支持touch事件

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动画的兼容

 var _elementStyle = document.createElement('div').style;
//获得CSS3前缀
var _vendor = (function () {
var vendors = ['t', 'webkitT', 'MozT', 'msT', 'OT'];
var transform;
var i = 0;
var l = vendors.length; for (; i < l; i++) {
transform = vendors[i] + 'ransform';
if (transform in _elementStyle) return vendors[i].substr(0, vendors[i].length - 1);
}
return false;
})();

这句代码会返回需要做兼容的CSS属性的前缀,然后提供一个方法特别的干这个事情:

 //获取样式(CSS3兼容)
function _prefixStyle(style) {
if (_vendor === false) return false;
if (_vendor === '') return style;
return _vendor + style.charAt(0).toUpperCase + style.substr(1);
}

获取当前时间戳

获得当前时间戳这个方法比较简单,这个会在计算动画用到:

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

动画数据

 me.momentum = function (current, start, time, lowerMargin, wrapperSize) {
var distance = current - start,
speed = Math.abs(distance) / time,
destination,
duration,
deceleration = 0.0006; destination = current + (speed * speed) / (2 * deceleration) * (distance < 0 ? -1 : 1);
duration = speed / deceleration; if (destination < lowerMargin) {
destination = wrapperSize ? lowerMargin - (wrapperSize / 2.5 * (speed / 8)) : lowerMargin;
distance = Math.abs(destination - current);
duration = distance / speed;
} else if (destination > 0) {
destination = wrapperSize ? wrapperSize / 2.5 * (speed / 8) : 0;
distance = Math.abs(current) + destination;
duration = distance / speed;
} return {
destination: Math.round(destination),
duration: duration
};
};

这个方法尤其关键,是iScroll动画平滑的一大功臣,这个方法内部用到了物理一个计算速度的公式,我可耻的忘了是什么了,这里直接不予关注了,解释下参数即可

current:当前鼠标位置
start:touchStart时候记录的Y(可能是X)的开始位置,但是在touchmove时候可能被重写
time: touchstart到手指离开时候经历的时间,同样可能被touchmove重写
lowerMargin:y可移动的最大距离,这个一般为计算得出 this.wrapperHeight - this.scrollerHeight
wrapperSize:如果有边界距离的话就是可拖动,不然碰到0的时候便停止

这个动画方法,我们后面用到还需要再说下

属性检测

 //我们暂时只判断touch 和 mouse即可
me.extend(me.style = {}, {
hasTouch: 'ontouchstart' in window,
transform: _prefixStyle('transform'),
transitionTimingFunction: _prefixStyle('transitionTimingFunction'),
transitionDuration: _prefixStyle('transitionDuration'),
transitionDelay: _prefixStyle('transitionDelay'),
transformOrigin: _prefixStyle('transformOrigin')
}); me.extend(me.eventType = {}, {
touchstart: ,
touchmove: ,
touchend: , mousedown: ,
mousemove: ,
mouseup:
}); me.extend(me.ease = {}, {
quadratic: {
style: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
fn: function (k) {
return k * ( - k);
}
},
circular: {
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, )
fn: function (k) {
return Math.sqrt( - (--k * k));
}
},
back: {
style: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)',
fn: function (k) {
var b = ;
return (k = k - ) * k * ((b + ) * k + b) + ;
}
},
bounce: {
style: '',
fn: function (k) {
if ((k /= ) < ( / 2.75)) {
return 7.5625 * k * k;
} else if (k < ( / 2.75)) {
return 7.5625 * (k -= (1.5 / 2.75)) * k + 0.75;
} else if (k < (2.5 / 2.75)) {
return 7.5625 * (k -= (2.25 / 2.75)) * k + 0.9375;
} else {
return 7.5625 * (k -= (2.625 / 2.75)) * k + 0.984375;
}
}
},
elastic: {
style: '',
fn: function (k) {
var f = 0.22,
e = 0.4; if (k === ) { return ; }
if (k == ) { return ; } return (e * Math.pow(, - * k) * Math.sin((k - f / ) * ( * Math.PI) / f) + );
}
}
});

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尺寸,动画多是和尺寸打关系嘛

 refresh: function () {
var rf = this.wrapper.offsetHeight; // Force reflow this.wrapperHeight = this.wrapper.clientHeight;
this.scrollerHeight = this.scroller.offsetHeight;
this.maxScrollY = this.wrapperHeight - this.scrollerHeight; this.endTime = 0; var offset = $(this.wrapper).offset(); this.wrapperOffset = {
left: offset.left * (-1),
top: offset.top * (-1)
}; this._execEvent('refresh'); this.resetPosition(); },

首先做了一步操作让下面的操作不会发生重绘

var rf = this.wrapper.offsetHeight;

然后初始化了几个关键DOM的高度,并且获得maxScrollY(真正使用的时候会/3)

接下来重置了endtime为0,并且设置一些基本参数后触发refresh事件(会触发滚动条的refresh)
当然,如果超出了边界的话,会resetPosition,重置位置
refresh本身并不复杂,但是对后面的影响比较大,算是初始化第一步的工作

_initIndicator

根据该方法会初始化一个滚动条,当然我们可以配置参数不让滚动条出现

 _initIndicator: function () {
//滚动条
var el = createDefaultScrollbar();
this.wrapper.appendChild(el);
this.indicator = new Indicator(this, { el: el }); 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();
}); },

因为滚动条的类,我们后面会详细说,这里便不关注了

_resize

在我们窗口变化时候,或者竖着的屏幕横着时候便会触发该事件,他会执行我们的refresh方法,重置页面信息
PS:其实就动画一块,IScroll逻辑是很严密的

 _resize: function () {
var that = this; clearTimeout(this.resizeTimeout); this.resizeTimeout = setTimeout(function () {
that.refresh();
}, this.options.resizePolling);
},

resetPosition

 resetPosition: function (time) {
var x = this.x,
y = this.y; time = time || 0; if (this.y > 0) {
y = 0;
} else if (this.y < this.maxScrollY) {
y = this.maxScrollY;
} if (y == this.y) {
return false;
} this.scrollTo(x, y, time, this.options.bounceEasing); return true;
},

在refresh时候会触发(因为可能导致scroller超出边界),在touchend时候超出边界也会使用该方法,这个时候重置边界会有动画(600ms)

动画结束如果检测到超出边界也会执行

运动相关

_transitionTime

 _transitionTime: function (time) {
time = time || 0;
this.scrollerStyle[utils.style.transitionDuration] = time + 'ms'; //滚动条,我们这里只会出现一个滚动条就不搞那么复杂了
this.indicator && this.indicator.transitionTime(time); }, getComputedPosition: function () {
var matrix = window.getComputedStyle(this.scroller, null), x, y; matrix = matrix[utils.style.transform].split(')')[0].split(', ');
x = +(matrix[12] || matrix[4]);
y = +(matrix[13] || matrix[5]); return { x: x, y: y };
},

该方法用于重置CSS3的时间参数属性,设置为0的话就不会具有动画,有一点需要注意的是,滚动条总是与他是同步的

_transitionTimingFunction

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

该方法用于重置CSS3的时间曲线,这个我没搞懂,滚动条同样是同步的

_translate

 _translate: function (x, y) {

   this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ;

   this.x = x;
this.y = y; if (this.options.scrollbars) {
this.indicator.updatePosition();
} },

该方法会改变DOM的样式,但是其动画由CSS3的 transitionDuration 控制,这里改变了scroller样式会同步其滚动条的

scrollTo

该方法是以上三个方法的合集,除了_transitionTime会在外部经常出现以停止动画外,其它方法基本在该方法内部使用

 scrollTo: function (x, y, time, easing) {
easing = easing || utils.ease.circular; this.isInTransition = time > 0; if (!time || easing.style) {
this._transitionTimingFunction(easing.style);
this._transitionTime(time);
this._translate(x, y);
}
},

因为滚动条的同步在各个子类方法中了,所以这个方法比较简单了,其逻辑进入了各个子方法

如此整个继承的方法介绍结束,我们开始对三大核心事件进行解析

三大事件核心

说到三大核心就必须将isInTransition单独提出来,他只有0,1两个值,但是是记录DOM是否处于运动的关键

start

 _start: function (e) {
if (!this.enabled || (this.initiated && utils.eventType[e.type] !== this.initiated)) {
return;
} var point = e.touches ? e.touches[0] : e, pos;
this.initiated = utils.eventType[e.type]; this.moved = false; this.distY = 0; //开启动画时间,如果之前有动画的话,便要停止动画,这里因为没有传时间,所以动画便直接停止了
this._transitionTime(); this.startTime = utils.getTime(); //如果正在进行动画,需要停止,并且触发滑动结束事件
if (this.isInTransition) {
this.isInTransition = false;
pos = this.getComputedPosition(); //移动过去
this._translate(Math.round(pos.x), Math.round(pos.y));
this._execEvent('scrollEnd');
} this.startX = this.x;
this.startY = this.y;
this.absStartX = this.x;
this.absStartY = this.y;
this.pointX = point.pageX;
this.pointY = point.pageY; this._execEvent('beforeScrollStart'); e.preventDefault(); },

在开始阶段,首先将moved属性设置为false,然后停止动画,如果当前isInTransition值为1,需要将他设置为false并且保持当前的状态

PS:这一步是停止动画并且保持当前状态的关键,这里会触发scrollEnd事件,滚动条会有所联动

在做一些初始化操作后,该方法结束,这个里面的核心就是停止动画并且做初始化操作

move

该阶段是第二核心,这里会根据移动获得新的坐标开始移动,如果超出边界会做一个处理,移动的值不会超过1/3个MaxY
一个关键点是会触发scrollStart事件,让滚动条可见
另一个关键点是没过300ms会重置开始状态值,所以就算使劲按着屏幕不放,突然放开,DOM仍然会移动很远

end

 _end: function (e) {
if (!this.enabled || utils.eventType[e.type] !== this.initiated) {
return;
} var point = e.changedTouches ? e.changedTouches[0] : e,
momentumY,
duration = utils.getTime() - this.startTime,
newX = Math.round(this.x),
newY = Math.round(this.y),
distanceX = Math.abs(newX - this.startX),
distanceY = Math.abs(newY - this.startY),
time = 0,
easing = ''; this.isInTransition = 0;
this.initiated = 0;
this.endTime = utils.getTime(); if (this.resetPosition(this.options.bounceTime)) {
return;
} this.scrollTo(newX, newY);
if (!this.moved) {
//click 的情况 this._execEvent('scrollCancel');
return;
} if (duration < 300) { momentumY = utils.momentum(this.y, this.startY, duration, this.maxScrollY, this.options.bounce ? this.wrapperHeight : 0);
// newX = momentumX.destination;
newY = momentumY.destination;
time = Math.max(momentumY.duration);
this.isInTransition = 1;
} if (newY != this.y) {
if (newY > 0 || newY < this.maxScrollY) {
easing = utils.ease.quadratic;
}
this.scrollTo(newX, newY, time, easing);
return;
} this._execEvent('scrollEnd');
},

end阶段首先会将isInTransition设置为0,如果超出边界会进行处理,进行动画又会将 isInTransition 设置为1,在动画事件结束后设置为0

期间还会处理一些其他情况,我们不予理睬,重点就是使用momentum获取了当前运动参数
然后开始运动,此处逻辑全部分解开了,这里反而显得不难了

至此,整个核心块IScroll便分解结束了,我们最后看看滚动条相关

Indicator

因为我们对滚动条的需求变动简单,我们的滚动条现在就真的很简单了,完全与scroller是一个从属关系,随着scroller而变化
有兴趣的朋友自己去看看吧,我这里就不管他了

源码:

 <!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0, minimum-scale=1.0, maximum-scale=1.0"> <title>iScroll demo: scrollbars</title> <style type="text/css">
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
} html {
-ms-touch-action: none;
} body,ul,li {
padding: 0;
margin: 0;
border: 0;
} body {
font-size: 12px;
font-family: ubuntu, helvetica, arial;
overflow: hidden; /* this is important to prevent the whole page to bounce */
} #header {
position: absolute;
z-index: 2;
top: 0;
left: 0;
width: 100%;
height: 45px;
line-height: 45px;
background: #CD235C;
padding: 0;
color: #eee;
font-size: 20px;
text-align: center;
font-weight: bold;
} #footer {
position: absolute;
z-index: 2;
bottom: 0;
left: 0;
width: 100%;
height: 48px;
background: #444;
padding: 0;
border-top: 1px solid #444;
} #wrapper {
position: absolute;
z-index: 1;
top: 45px;
bottom: 48px;
left: 0;
width: 100%;
background: #ccc;
overflow: hidden;
} #scroller {
position: absolute;
z-index: 1;
-webkit-tap-highlight-color: rgba(0,0,0,0);
width: 100%;
-webkit-transform: translateZ(0);
-moz-transform: translateZ(0);
-ms-transform: translateZ(0);
-o-transform: translateZ(0);
transform: translateZ(0);
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-text-size-adjust: none;
-moz-text-size-adjust: none;
-ms-text-size-adjust: none;
-o-text-size-adjust: none;
text-size-adjust: none;
} #scroller ul {
list-style: none;
padding: 0;
margin: 0;
width: 100%;
text-align: left;
} #scroller li {
padding: 0 10px;
height: 40px;
line-height: 40px;
border-bottom: 1px solid #ccc;
border-top: 1px solid #fff;
background-color: #fafafa;
font-size: 14px;
} </style>
</head>
<body >
<div id="header">抄袭IScroll</div> <div id="wrapper">
<div id="scroller">
<ul>
<li>Pretty row 1</li>
<li>Pretty row 2</li>
<li>Pretty row 3</li>
<li>Pretty row 4</li>
<li>Pretty row 5</li>
<li>Pretty row 6</li>
<li>Pretty row 7</li>
<li>Pretty row 8</li>
<li>Pretty row 9</li>
<li>Pretty row 10</li>
<li>Pretty row 11</li>
<li>Pretty row 12</li>
<li>Pretty row 13</li>
<li>Pretty row 14</li>
<li>Pretty row 15</li>
<li>Pretty row 16</li>
<li>Pretty row 17</li>
<li>Pretty row 18</li>
<li>Pretty row 19</li>
<li>Pretty row 20</li>
<li>Pretty row 21</li>
<li>Pretty row 22</li>
<li>Pretty row 23</li>
<li>Pretty row 24</li>
<li>Pretty row 25</li>
<li>Pretty row 26</li>
<li>Pretty row 27</li>
<li>Pretty row 28</li>
<li>Pretty row 29</li>
<li>Pretty row 30</li>
<li>Pretty row 31</li>
<li>Pretty row 32</li>
<li>Pretty row 33</li>
<li>Pretty row 34</li>
<li>Pretty row 35</li>
<li>Pretty row 36</li>
<li>Pretty row 37</li>
<li>Pretty row 38</li>
<li>Pretty row 39</li>
<li>Pretty row 40</li>
<li>Pretty row 41</li>
<li>Pretty row 42</li>
<li>Pretty row 43</li>
<li>Pretty row 44</li>
<li>Pretty row 45</li>
<li>Pretty row 46</li>
<li>Pretty row 47</li>
<li>Pretty row 48</li>
<li>Pretty row 49</li>
<li>Pretty row 50</li>
</ul>
</div>
</div> <div id="footer"></div> <script src="zepto.js" type="text/javascript"></script>
<script src="MyIscroll.js" type="text/javascript"></script> <script type="text/javascript"> var s = new IScroll({
wrapper: $('#wrapper'),
scroller: $('#scroller')
}); </script>
</body>
</html>
 (function (window, document, Math) {

   var utils = (function () {
var me = {};
var _elementStyle = document.createElement('div').style; //获得需要兼容CSS3前缀
var _vendor = (function () {
var vendors = ['t', 'webkitT', 'MozT', 'msT', 'OT'];
var transform;
var i = 0;
var l = vendors.length; for (; i < l; i++) {
transform = vendors[i] + 'ransform';
if (transform in _elementStyle) return vendors[i].substr(0, vendors[i].length - 1);
}
return false;
})(); //获取样式(CSS3兼容)
function _prefixStyle(style) {
if (_vendor === false) return false;
if (_vendor === '') return style;
return _vendor + style.charAt(0).toUpperCase() + style.substr(1);
} me.getTime = Date.now || function getTime() { return new Date().getTime(); }; me.addEvent = function (el, type, fn, capture) {
if (el[0]) el = el[0];
el.addEventListener(type, fn, !!capture);
}; me.removeEvent = function (el, type, fn, capture) {
if (el[0]) el = el[0];
el.removeEventListener(type, fn, !!capture);
}; /*
current:当前鼠标位置
start:touchStart时候记录的Y(可能是X)的开始位置,但是在touchmove时候可能被重写
time: touchstart到手指离开时候经历的时间,同样可能被touchmove重写
lowerMargin:y可移动的最大距离,这个一般为计算得出 this.wrapperHeight - this.scrollerHeight
wrapperSize:如果有边界距离的话就是可拖动,不然碰到0的时候便停止
*/
me.momentum = function (current, start, time, lowerMargin, wrapperSize) {
var distance = current - start,
speed = Math.abs(distance) / time,
destination,
duration,
deceleration = 0.0006; destination = current + (speed * speed) / (2 * deceleration) * (distance < 0 ? -1 : 1);
duration = speed / deceleration; if (destination < lowerMargin) {
destination = wrapperSize ? lowerMargin - (wrapperSize / 2.5 * (speed / 8)) : lowerMargin;
distance = Math.abs(destination - current);
duration = distance / speed;
} else if (destination > 0) {
destination = wrapperSize ? wrapperSize / 2.5 * (speed / 8) : 0;
distance = Math.abs(current) + destination;
duration = distance / speed;
} return {
destination: Math.round(destination),
duration: duration
};
}; $.extend(me, {
hasTouch: 'ontouchstart' in window
}); //我们暂时只判断touch 和 mouse即可
$.extend(me.style = {}, {
transform: _prefixStyle('transform'),
transitionTimingFunction: _prefixStyle('transitionTimingFunction'),
transitionDuration: _prefixStyle('transitionDuration'),
transitionDelay: _prefixStyle('transitionDelay'),
transformOrigin: _prefixStyle('transformOrigin')
}); $.extend(me.eventType = {}, {
touchstart: 1,
touchmove: 1,
touchend: 1, mousedown: 2,
mousemove: 2,
mouseup: 2
}); $.extend(me.ease = {}, {
quadratic: {
style: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
fn: function (k) {
return k * (2 - k);
}
},
circular: {
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)
fn: function (k) {
return Math.sqrt(1 - (--k * k));
}
},
back: {
style: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)',
fn: function (k) {
var b = 4;
return (k = k - 1) * k * ((b + 1) * k + b) + 1;
}
},
bounce: {
style: '',
fn: function (k) {
if ((k /= 1) < (1 / 2.75)) {
return 7.5625 * k * k;
} else if (k < (2 / 2.75)) {
return 7.5625 * (k -= (1.5 / 2.75)) * k + 0.75;
} else if (k < (2.5 / 2.75)) {
return 7.5625 * (k -= (2.25 / 2.75)) * k + 0.9375;
} else {
return 7.5625 * (k -= (2.625 / 2.75)) * k + 0.984375;
}
}
},
elastic: {
style: '',
fn: function (k) {
var f = 0.22,
e = 0.4; if (k === 0) { return 0; }
if (k == 1) { return 1; } return (e * Math.pow(2, -10 * k) * Math.sin((k - f / 4) * (2 * Math.PI) / f) + 1);
}
}
});
return me;
})(); function IScroll(opts) {
this.wrapper = typeof opts.wrapper == 'string' ? $(opts.wrapper) : opts.wrapper;
this.scroller = typeof opts.scroller == 'string' ? $(opts.scroller) : opts.scroller;
if (!opts.wrapper[0] || !opts.scroller[0]) throw 'param error'; this.wrapper = this.wrapper[0];
this.scroller = this.scroller[0]; //这个属性会被动态改变的,如果这里
this.scrollerStyle = this.scroller.style; this.options = {
//是否具有滚动条
scrollbars: true,
// 其实时期Y的位置
startY: 0,
//超出边界还原时间点
bounceTime: 600,
//超出边界返回的动画
bounceEasing: utils.ease.circular, //超出边界时候是否还能拖动
bounce: true, //当window触发resize事件60ms后还原
resizePolling: 60,
startX: 0,
startY: 0
}; for (var i in opts) {
this.options[i] = opts[i];
} this.translateZ = ' translateZ(0)'; this.x = 0;
this.y = 0;
this._events = {};
this._init(); //更新滚动条位置
this.refresh(); //更新本身位置
this.scrollTo(this.options.startX, this.options.startY); this.enable(); }; IScroll.prototype = {
_init: function () {
this._initEvents(); //初始化滚动条,滚动条此处需要做重要处理
if (this.options.scrollbars) {
this._initIndicator();
}
},
refresh: function () {
var rf = this.wrapper.offsetHeight; // Force reflow this.wrapperHeight = this.wrapper.clientHeight;
this.scrollerHeight = this.scroller.offsetHeight;
this.maxScrollY = this.wrapperHeight - this.scrollerHeight; this.endTime = 0; this._execEvent('refresh'); this.resetPosition(); },
_initEvents: function (remove) {
var eventType = remove ? utils.removeEvent : utils.addEvent;
var target = this.options.bindToWrapper ? this.wrapper : window; eventType(window, 'orientationchange', this);
eventType(window, 'resize', this); if (utils.hasTouch) {
eventType(this.wrapper, 'touchstart', this);
eventType(target, 'touchmove', this);
eventType(target, 'touchcancel', this);
eventType(target, 'touchend', this);
} else {
eventType(this.wrapper, 'mousedown', this);
eventType(target, 'mousemove', this);
eventType(target, 'mousecancel', this);
eventType(target, 'mouseup', this);
} eventType(this.scroller, 'transitionend', this);
eventType(this.scroller, 'webkitTransitionEnd', this);
eventType(this.scroller, 'oTransitionEnd', this);
eventType(this.scroller, 'MSTransitionEnd', this);
},
_start: function (e) {
if (!this.enabled || (this.initiated && utils.eventType[e.type] !== this.initiated)) {
return;
} var point = e.touches ? e.touches[0] : e, pos;
this.initiated = utils.eventType[e.type]; this.moved = false; this.distY = 0; //开启动画时间,如果之前有动画的话,便要停止动画,这里因为没有传时间,所以动画便直接停止了
this._transitionTime(); this.startTime = utils.getTime(); //如果正在进行动画,需要停止,并且触发滑动结束事件
if (this.isInTransition) {
this.isInTransition = false;
pos = this.getComputedPosition(); //移动过去
this._translate(Math.round(pos.x), Math.round(pos.y));
this._execEvent('scrollEnd');
} this.startX = this.x;
this.startY = this.y;
this.absStartX = this.x;
this.absStartY = this.y;
this.pointX = point.pageX;
this.pointY = point.pageY; this._execEvent('beforeScrollStart'); e.preventDefault(); }, _move: function (e) {
if (!this.enabled || utils.eventType[e.type] !== this.initiated) {
return;
} e.preventDefault(); var point = e.touches ? e.touches[0] : e,
deltaX = point.pageX - this.pointX,
deltaY = point.pageY - this.pointY,
timestamp = utils.getTime(),
newX, newY,
absDistX, absDistY; this.pointX = point.pageX;
this.pointY = point.pageY; this.distX += deltaX;
this.distY += deltaY;
absDistX = Math.abs(this.distX);
absDistY = Math.abs(this.distY); // 如果一直按着没反应的话这里就直接返回了
if (timestamp - this.endTime > 300 && (absDistX < 10 && absDistY < 10)) {
return;
} newY = this.y + deltaY; if (newY > 0 || newY < this.maxScrollY) {
newY = this.options.bounce ? this.y + deltaY / 3 : newY > 0 ? 0 : this.maxScrollY;
} if (!this.moved) {
this._execEvent('scrollStart');
} this.moved = true; this._translate(0, newY); if (timestamp - this.startTime > 300) {
this.startTime = timestamp;
this.startX = this.x;
this.startY = this.y;
} },
_end: function (e) {
if (!this.enabled || utils.eventType[e.type] !== this.initiated) {
return;
} var point = e.changedTouches ? e.changedTouches[0] : e,
momentumY,
duration = utils.getTime() - this.startTime,
newX = Math.round(this.x),
newY = Math.round(this.y),
distanceX = Math.abs(newX - this.startX),
distanceY = Math.abs(newY - this.startY),
time = 0,
easing = ''; this.isInTransition = 0;
this.initiated = 0;
this.endTime = utils.getTime(); if (this.resetPosition(this.options.bounceTime)) {
return;
} this.scrollTo(newX, newY);
if (!this.moved) {
//click 的情况 this._execEvent('scrollCancel');
return;
} if (duration < 300) { momentumY = utils.momentum(this.y, this.startY, duration, this.maxScrollY, this.options.bounce ? this.wrapperHeight : 0);
// newX = momentumX.destination;
newY = momentumY.destination;
time = Math.max(momentumY.duration);
this.isInTransition = 1;
} if (newY != this.y) {
if (newY > 0 || newY < this.maxScrollY) {
easing = utils.ease.quadratic;
}
this.scrollTo(newX, newY, time, easing);
return;
} this._execEvent('scrollEnd');
}, _resize: function () {
var that = this; clearTimeout(this.resizeTimeout); this.resizeTimeout = setTimeout(function () {
that.refresh();
}, this.options.resizePolling);
}, _transitionTimingFunction: function (easing) {
this.scrollerStyle[utils.style.transitionTimingFunction] = easing; this.indicator && this.indicator.transitionTimingFunction(easing);
}, //开始或者停止动画
_transitionTime: function (time) {
time = time || 0;
this.scrollerStyle[utils.style.transitionDuration] = time + 'ms'; //滚动条,我们这里只会出现一个滚动条就不搞那么复杂了
this.indicator && this.indicator.transitionTime(time); }, getComputedPosition: function () {
var matrix = window.getComputedStyle(this.scroller, null), x, y; matrix = matrix[utils.style.transform].split(')')[0].split(', ');
x = +(matrix[12] || matrix[4]);
y = +(matrix[13] || matrix[5]); return { x: x, y: y };
}, _initIndicator: function () {
//滚动条
var el = createDefaultScrollbar();
this.wrapper.appendChild(el);
this.indicator = new Indicator(this, { el: el }); 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();
}); }, //移动x,y这里比较简单就不分离y了
_translate: function (x, y) { this.scrollerStyle[utils.style.transform] = 'translate(' + x + 'px,' + y + 'px)' + this.translateZ; this.x = x;
this.y = y; if (this.options.scrollbars) {
this.indicator.updatePosition();
} }, resetPosition: function (time) {
var x = this.x,
y = this.y; time = time || 0; if (this.y > 0) {
y = 0;
} else if (this.y < this.maxScrollY) {
y = this.maxScrollY;
} if (y == this.y) {
return false;
} this.scrollTo(x, y, time, this.options.bounceEasing); return true;
}, //移动
scrollTo: function (x, y, time, easing) {
easing = easing || utils.ease.circular; this.isInTransition = time > 0; if (!time || easing.style) {
this._transitionTimingFunction(easing.style);
this._transitionTime(time);
this._translate(x, y);
}
}, //统一的关闭接口
disable: function () {
this.enabled = false;
},
//统一的open接口
enable: function () {
this.enabled = true;
}, on: function (type, fn) {
if (!this._events[type]) {
this._events[type] = [];
} this._events[type].push(fn);
}, _execEvent: function (type) {
if (!this._events[type]) {
return;
} var i = 0,
l = this._events[type].length; if (!l) {
return;
} for (; i < l; i++) {
this._events[type][i].call(this);
}
},
destroy: function () {
this._initEvents(true); this._execEvent('destroy');
}, _transitionEnd: function (e) {
if (e.target != this.scroller || !this.isInTransition) {
return;
} this._transitionTime();
if (!this.resetPosition(this.options.bounceTime)) {
this.isInTransition = false;
this._execEvent('scrollEnd');
}
}, //事件具体触发点
handleEvent: function (e) {
switch (e.type) {
case 'touchstart':
case 'mousedown':
this._start(e);
break;
case 'touchmove':
case 'mousemove':
this._move(e);
break;
case 'touchend':
case 'mouseup':
case 'touchcancel':
case 'mousecancel':
this._end(e);
break;
case 'orientationchange':
case 'resize':
this._resize();
break;
case 'transitionend':
case 'webkitTransitionEnd':
case 'oTransitionEnd':
case 'MSTransitionEnd':
this._transitionEnd(e);
break;
}
} }; function createDefaultScrollbar() {
var scrollbar = document.createElement('div'),
indicator = document.createElement('div'); scrollbar.style.cssText = 'position:absolute;z-index:9999';
scrollbar.style.cssText += ';width:7px;bottom:2px;top:2px;right:1px';
scrollbar.style.cssText += ';overflow:hidden'; 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';
indicator.style.width = '100%'; scrollbar.appendChild(indicator); return scrollbar;
} function Indicator(scroller, opts) {
this.wrapper = typeof opts.el == 'string' ? document.querySelector(opts.el) : opts.el;
this.indicator = this.wrapper.children[0]; this.wrapperStyle = this.wrapper.style;
this.indicatorStyle = this.indicator.style;
this.scroller = scroller; this.sizeRatioY = 1;
this.maxPosY = 0; this.wrapperStyle[utils.style.transform] = this.scroller.translateZ;
this.wrapperStyle[utils.style.transitionDuration] = '0ms';
this.wrapperStyle.opacity = '0'; } Indicator.prototype = {
transitionTime: function (time) {
time = time || 0;
this.indicatorStyle[utils.style.transitionDuration] = time + 'ms';
},
transitionTimingFunction: function (easing) {
this.indicatorStyle[utils.style.transitionTimingFunction] = easing;
},
refresh: function () { this.transitionTime(); var r = this.wrapper.offsetHeight; // force refresh this.wrapperHeight = this.wrapper.clientHeight; this.indicatorHeight = Math.max(Math.round(this.wrapperHeight * this.wrapperHeight / (this.scroller.scrollerHeight || this.wrapperHeight || 1)), 8);
this.indicatorStyle.height = this.indicatorHeight + 'px'; this.maxPosY = this.wrapperHeight - this.indicatorHeight;
this.sizeRatioY = (this.scroller.maxScrollY && (this.maxPosY / this.scroller.maxScrollY)); this.updatePosition();
},
updatePosition: function () {
var y = Math.round(this.sizeRatioY * this.scroller.y) || 0;
this.y = y; //不需要兼容方式了
this.indicatorStyle[utils.style.transform] = 'translate(0px,' + y + 'px)' + this.scroller.translateZ; },
fade: function (val, hold) {
if (hold && !this.visible) {
return;
} clearTimeout(this.fadeTimeout);
this.fadeTimeout = null; var time = val ? 250 : 500,
delay = val ? 0 : 300; val = val ? '1' : '0'; this.wrapperStyle[utils.style.transitionDuration] = time + 'ms'; this.fadeTimeout = setTimeout((function (val) {
this.wrapperStyle.opacity = val;
this.visible = +val;
}).bind(this, val), delay); }
}; IScroll.utils = utils; window.IScroll = IScroll; })(window, document, Math);

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

结语

我们这次抄袭了IScroll核心代码,形成了一个简单的拖动库,我们对IScroll的学习可能暂时就到这里了,接下来一段时间我们的重心会放到nodeJS上面
中途可能会涉及到requireJS的源码学习,但是都不会偏离nodeJS的核心

注意,请看最后的代码......

【iScroll源码学习04】分离IScroll核心的更多相关文章

  1. 【iScroll源码学习03】iScroll事件机制与滚动条的实现

    前言 想不到又到周末了,周末的时间要抓紧学习才行,前几天我们学习了iScroll几点基础知识: 1. [iScroll源码学习02]分解iScroll三个核心事件点 2. [iScroll源码学习01 ...

  2. 【iScroll源码学习02】分解iScroll三个核心事件点

    前言 最近两天看到很多的总结性发言,我想想今年好像我的变化挺大的,是不是该晚上来水一发呢?嗯,决定了,晚上来水一发! 上周六,我们简单模拟了下iScroll的实现,周日我们开始了学习iScroll的源 ...

  3. 【iScroll源码学习00】模拟iScroll

    前言 相信对移动端有了解的朋友对iScroll这个库非常熟悉吧,今天我们就来说下我们移动页面的iScroll化 iScroll是我们必学框架之一,我们这次先根据iScroll功能自己实现其功能,然后再 ...

  4. 【iScroll源码学习01】准备阶段 - 叶小钗

    [iScroll源码学习01]准备阶段 - 叶小钗 时间 2013-12-29 18:41:00 博客园-原创精华区 原文  http://www.cnblogs.com/yexiaochai/p/3 ...

  5. Spring 源码学习 04:初始化容器与 DefaultListableBeanFactory

    前言 在前一篇文章:创建 IoC 容器的几种方式中,介绍了四种方式,这里以 AnnotationConfigApplicationContext 为例,跟进代码,看看 IoC 的启动流程. 入口 从 ...

  6. 【iScroll源码学习01】准备阶段

    前言 我们昨天初步了解了为什么会出现iScroll:[SPA]移动站点APP化研究之上中下页面的iScroll化(上),然后简单的写了一个demo来模拟iScroll,其中了解到了以下知识点: ① v ...

  7. iscroll源码学习(1)

    iscroll是移端端开发的两大利器之一(另一个是fastclick),为了将它整合的avalon,需要对它认真学习一番.下面是我的笔记. 第一天看的是它的工具类util.js //用于做函数节流 v ...

  8. zepto源码学习-04 event

    之前说完$(XXX),然后还有很多零零碎碎的东西需要去分析,结果一看代码,发现zepto的实现都相对简单,没有太多可分析的.直接略过了一些实现,直接研究Event模块,相比JQuery的事件系统,ze ...

  9. ABP 框架从源码学习——abp框架启动核心类AbpBootstrapper(2)

    在AbpBootstrapper中的两个至关重要的属性:IIocManager 和 IAbpModuleManager  public class AbpBootstrapper : IDisposa ...

随机推荐

  1. 探秘重编译(Recompilations)(2/2)

    在上一篇文章里,我讨论了使用临时表如何引起SQL Server里的重编译.在文章最后我提到,今天这篇文章我会聚焦表变量(Table Variables)的更多信息,它可以避免重编译的昂贵开销.我们来详 ...

  2. c++中char*\wchar_t*\string\wstring之间的相互转换

    string U2A(const wstring& str)//Unicode字符转Ascii字符 { string strDes; if ( str.empty() ) goto __end ...

  3. TCP 粘包/拆包问题

    简介    TCP 是一个’流’协议,所谓流,就是没有界限的一串数据. 大家可以想想河里的流水,是连成一片的.期间并没有分界线, TCP 底层并不了解上层业务数据的具体含义 ,它会根据 TCP 缓冲区 ...

  4. Android(java)学习笔记229:服务(service)之绑定服务调用服务里面的方法 (采用接口隐藏代码内部实现)

    1.接口 接口可以隐藏代码内部的细节,只暴露程序员想暴露的方法 2.利用上面的思想优化之前的案例:服务(service)之绑定服务调用服务里面的方法,如下: (1)这里MainActivity.jav ...

  5. Linux_jdk

    先查看下 yum list java* yum install java-1.7.0-openjdk* -y 环境变量应该是会自动配置的 或者手动配置编辑/etc/profile #vi /etc/p ...

  6. 转:设置Loadrunner负载机临时文件目录

    最近在跑稳定性测试 3 X 24小时的时候,发现负载机产生的日志还运行记录等等竟然有100多G! C盘空间不足,但是D盘还有700多G空间呢,怎么让临时文件转移到D盘? 此处分两种情况: 一. 修改本 ...

  7. lpc1768usb端点响应以及描述符定义

    #ifndef __USBEP1_H_ #define __USBEP1_H_ #include "usb.h" #include "usbhw.h" exte ...

  8. Linux yum安装和源码安装

    转载注明出处:原文地址 Linux Yum 在线安装 在线:Yum配置地址:/etc/yum.repos.d/CentOS-Base.repo 离线:光盘搭建Yum源 挂载光盘 使在线Yum源失效:m ...

  9. Java-IO之PipedReader和PipedWriter

    PipedReader和PipedWriter与PipedInputStream和PipedOutputStream一样,都可以用于管道通信.PipedWriter是字符管道输出流,继承于Writer ...

  10. asd短片数篇

    黄乙己 黄乙己是站着AK而正常的唯一的人.他身材挺高大:蜡黄脸色,眼角间时常夹着些饼干屑:一副黑色的眼镜.虽然挺正常,可是他有良好的饮食习惯,似乎十多个月都是吃的牛奶泡饭,也没有洗饭盒.他对人说话,总 ...