前言

最近几天我们前前后后基本将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. 如何避免javascript中的冲突

    [1]工程师甲编写功能A var a = 1; var b = 2; alert(a+b); [2]工程师乙添加新功能B var a = 2; var b = 1; alert(a-b); [3]上一 ...

  2. Docker之Linux UnionFS

    UnionFS UnionFS是一种为Linux,FreeBSD和NetBSD操作系统设计的把其他文件系统联合到一个联合挂载点的文件系统服务.它使用branch把不同文件系统的文件和目录"透 ...

  3. CSS命名

    CSS命名规范 CSS样式命名整理 页面结构 容器: container/wrap 整体宽度:wrapper 页头:header 内容:content 页面主体:main 页尾:footer 导航:n ...

  4. Canvas 示例:4种超炫的网站动画背景效果

    今天,我们想分享一些动画背景的灵感.全屏背景图片的网站头部是最新的网页设计趋势,已经持续了一段时间.最近人们一直在转向动画添加更多的视觉兴趣到他们的网站中,在这里我们想向您分享几个使用  JavaSc ...

  5. SQLServer学习笔记系列4

    一.写在前面的话 好多天没有记录sql学习笔记了,要坚持下去,坚信每一点的进步都是为在积蓄力量.今天看到一幅图,特此分享出来. 通过这幅图,我看到的是每人站在自己的角度看问题,感受是不一样的,就如同学 ...

  6. 别用symbolicatecrash来解析crash Log了

    今天突然发现了一个解析iOS crash log的好方法,忍不住来分享一下. 相信每个做iOS开发的TX都应该不会对symbolicatecrash陌生,我们第一次遇到真机上产生的崩溃日志时,在网上搜 ...

  7. Theano入门神经网络(二) 实现一个XOR门

    与非门的图片如下 示意图 详细解释: 1 定义变量的代码,包括了输入.权值.输出等.其中激活函数采用的是sigmod函数 # -*- coding: utf-8 -*- __author__ = 'A ...

  8. nodejs学习笔记二——链接mongodb

    a.安装mongoose库用来链接mongodb数据库 安装mongodb数据库参考mongodb安装 前言(怨言) 本来是想安装mongodb库来链接mongodb的,命令行到nodejs工程目录: ...

  9. 使用Microsoft Fakes进行单元测试(2)

    接上一篇使用Microsoft Fakes进行单元测试(1) 下面进行Shim的演示. 2.使用Shim替换静态方法 假设我们需要一个工具方法用来格式化当前时间为字符串,因为DateTime.Now一 ...

  10. VS2012 JSON、XML自动生成对应的类

    在VS编辑下拉框中,选择选择性粘贴(Paste Special)