前言
最近几天我们前前后后基本将iScroll源码学的七七八八了,文章中未涉及的各位就要自己去看了
- 1. 【iScroll源码学习03】iScroll事件机制与滚动条的实现
- 2. 【iScroll源码学习02】分解iScroll三个核心事件点
- 3. 【iScroll源码学习01】准备阶段
- 4. 【iScroll源码学习00】模拟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动画的兼容
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的时候便停止
这个动画方法,我们后面用到还需要再说下
属性检测
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 });
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
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 },
end阶段首先会将isInTransition设置为0,如果超出边界会进行处理,进行动画又会将 isInTransition 设置为1,在动画事件结束后设置为0
期间还会处理一些其他情况,我们不予理睬,重点就是使用momentum获取了当前运动参数
然后开始运动,此处逻辑全部分解开了,这里反而显得不难了
至此,整个核心块IScroll便分解结束了,我们最后看看滚动条相关
Indicator
因为我们对滚动条的需求变动简单,我们的滚动条现在就真的很简单了,完全与scroller是一个从属关系,随着scroller而变化
有兴趣的朋友自己去看看吧,我这里就不管他了
源码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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>
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 += ';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);
http://sandbox.runjs.cn/show/naryr1vy
结语
我们这次抄袭了IScroll核心代码,形成了一个简单的拖动库,我们对IScroll的学习可能暂时就到这里了,接下来一段时间我们的重心会放到nodeJS上面
中途可能会涉及到requireJS的源码学习,但是都不会偏离nodeJS的核心
注意,请看最后的代码......