在一些项目中,用户总是要求自定义一下滚动条,以前一般用iscroll解决,但是发现iscroll有很多不方便的地方,而且也比较大,索性自己琢磨一个类似的插件吧!目的有两个:要足够小,易于上手使用;功能一定要足够实用,能满足广大H5开发者的基本需求。
介绍一下这个插件的主要功能:
1、隐藏或显示滚动条,自定义滚动条样式。
2、滚动dom的刷新:refresh;
3、滚动内容的懒加载;
4、子元素绑定tap事件;
5、支持scrolling、scrollEnd等插件内事件绑定;
6、scrollTo方法和其他的一些方法。
相比上一个测试版本(详见上一篇博客),我在这个版本支持了滚动动画,并且加入了Tap事件和destroy方法。总结一下以下技术难点:
1、支持用户自定义事件绑定到列表元素上,我采用用户传入dom和自定义的方法,利用tap接口传入插件,在插件中做tap的处理和回调。
2、当懒加载成功后,给加载的内容绑定自定义事件。这时需要执行refresh(刷新)方法,在插件内执行destroy方法,将removeEventListener放在this.events.destory中,利用sendEvent执行,这会销毁掉在tap中用户绑定的自定义方法。在刷星完毕后重新绑定就可以了。
3、利用requestAnimationFrame和css的transition-timing-function分段做列表的滚动动画。
使用说明:
1、自定义滚动条:
var scroll = new Dscroll(selector,{ scrollBar: true, barName: "myClassName", });
2、懒加载
//this.bottomHeight为底部未显示的高度,利用scrolling监听该值。 myTest.on("scrolling",function () { if (this.bottomHeight < 100 && !loaded) { loaded = true; createNewItem(); //刷新操作会清空子元素的绑定事件 myTest.refresh(); //刷新后统一绑定点击事件 bindTouch(); } });
3、子元素绑定点击事件
var i = 0, l = document.querySelectorAll("#myBox>div>p").length; for (; i < l; i++) { (function (k) { var dom = document.querySelectorAll("#myBox>div>p").item(k); myTest.tap(dom,function () { alert("您点击的是第" + (k + 1) + "个段落。"); }); })(i); }
插件使用实例:
<!DOCTYPE html> <html lang="zh_CN"> <head> <title>DeftScroll插件测试</title> <meta http-equiv="Content-Type" content="text/html;charset=utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"/> <meta name="apple-mobile-web-app-status-bar-style" content="black"/> <meta name="apple-mobile-web-app-title" content=""/> <meta name="apple-touch-fullscreen" content="YES" /> <meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="format-detection" content="telephone=no" /> <meta name="HandheldFriendly" content="true" /> <meta http-equiv="x-rim-auto-match" content="none" /> <meta name="format-detection" content="telephone=no" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <style> body { position: absolute; left: 0; right: 0; top: 0; bottom: 0; margin: auto; overflow: hidden; } #myBox { width: 90%; height: 90%; position: absolute; left: 0; right: 0; top: 0; bottom: 0; margin: auto; overflow: hidden; } #myBox p { margin: 5px auto; line-height: 60px; text-align: center; background: #ddd; } </style> </head> <body> <div id="myBox"> <div> <p>1</p> <p>2</p> <p>3</p> <p>4</p> <p>5</p> <p>6</p> <p>7</p> <p>8</p> <p>9</p> <p>10</p> <p>11</p> <p>12</p> <p>13</p> <p>14</p> <p>15</p> <p>16</p> <p>17</p> <p>18</p> <p>19</p> <p>20</p> </div> </div> <script type="text/javascript" src="DeftScroll.js"></script> <script type="text/javascript"> document.body.addBehavior("touchmove",function (e) { e.preventDefault(); },false); var box = document.querySelector("#myBox>div"); var loaded = false; var myTest = new DScroll("#myBox",{ scrollBar: true, }); //模拟ajax添加条目 function createNewItem() { var i = 0, l = 10; for ( ; i < l; i++) { var myDom = document.createElement("p"); myDom.innerText = "我是添加的条目" + (i + 1); box.appendChild(myDom); } }; //子元素绑定点击事件 function bindTouch() { var i = 0, l = document.querySelectorAll("#myBox>div>p").length; for (; i < l; i++) { (function (k) { var dom = document.querySelectorAll("#myBox>div>p").item(k); myTest.tap(dom,function () { alert("您点击的是第" + (k + 1) + "个段落。"); }); })(i); } }; myTest.on("scrolling",function () { if (this.bottomHeight < 100 && !loaded) { loaded = true; createNewItem(); //刷新操作会清空子元素的绑定事件 myTest.refresh(); //刷新后统一绑定点击事件 bindTouch(); } }); bindTouch(); </script> </body> </html>
插件源码:
/*** * 着手开发于2017-12-11 * author:一只神秘的猿 * name: DeftScroll */ /****1.2版本 * 开发于2017-12-21 */ (function (win,doc,Math) { var rAF = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) { window.setTimeout(callback, 1000 / 60); }; function DScroll(el,options) { this.height = 0;//里面框的高度 this.boxHeight = 0;//容器的高度 this.element = null; this.children = null; this.style = null; this.scrollBox = null;//滚动条框 this.scrollItem = null;//滚动条 this.options = options;//参数 this.overHeight = 0;//未显示的内容高度 this.bottomHeight = 0;//底部未显示的高度 this.events = {}; this.startY = 0; this.isAnimating = false; this.oStartY = 0; this.endY = 0; this.y = 0; if (typeof el === "string") { this.element = doc.querySelector(el); } else { throw "获取不到正确的dom。"; } if (this.element) { var child = this.element.children[0]; this.children = child; } else { throw "无法获取列表父级盒子。" } this._init(); this._eventHandle(); } DScroll.prototype = { _init: function () { if (this.children) { this.height = this.children.scrollHeight; this.boxHeight = this.element.offsetHeight; this.overHeight = this.height - this.boxHeight; this.style = this.children.style; } if (this.height > this.boxHeight) { if (!this.options || !this.options.scrollBar) { return; } this.scrollBox = doc.createElement("div"); this.scrollItem = doc.createElement("div"); this.scrollBox.appendChild(this.scrollItem); this.element.appendChild(this.scrollBox); //设置滚动条类名 if (this.options && typeof this.options.barName === "string") { this.scrollBox.className = "clipScrollBox " + this.options.barName; } else { this.scrollBox.className = "clipScrollBox"; } this.scrollItem.className = "clipScrollItem"; if (this.scrollBox.className === "clipScrollBox") { this.scrollBox.setAttribute( "style","position:absolute; width: 5px; height:100%; top: 0; right: 0; border: 1px solid #fff; background: rgba(255,255,255,.7); border-radius: 4px; overflow: hidden; z-index: 1000"); this.scrollItem.setAttribute("style","width: 100%; height: " + this.boxHeight * 100 / this.height + "%; background: #999; border-radius: 4px;") } else { this.scrollBox.setAttribute("style","position: absolute; height:100%; top: 0; right: 0; overflow: hidden; z-index: 1000"); this.scrollItem.setAttribute("style","width: 100%; height: " + this.boxHeight * 100 / this.height + "%;") } } }, transform: function (destY) { if (destY) { this.y = destY; } this.children.style.transform = "translate3d(0," + this.y + "px,0)"; }, changePosition: function () { var y = 0; if (this.y <= 0 && this.y >= -this.overHeight) { this.scrollItem.style.transform = "translate3d(0," + Math.abs(this.y) * (this.boxHeight - this.boxHeight * this.boxHeight / this.height) / (this.height - this.boxHeight) + "px,0)"; } else if (this.y > 0) { y = 0; this.scrollItem.style.transform = "translate3d(0," + Math.abs(y) * (this.boxHeight - this.boxHeight * this.boxHeight / this.height) / (this.height - this.boxHeight) + "px,0)"; } else { y = -this.overHeight; this.scrollItem.style.transform = "translate3d(0," + Math.abs(y) * (this.boxHeight - this.boxHeight * this.boxHeight / this.height) / (this.height - this.boxHeight) + "px,0)"; } }, //事件控制器 _eventHandle: function (e) { var self = this; this.element.addEventListener("touchstart",function (e) { self.startY = e.touches[0].pageY; self.oStartY = self.startY; self.startTime = utils.getTime(); self.isAnimating && self.stop(); },false); this.element.addEventListener("touchmove",function (e) { if (self.y > 0) { self.diffY = e.touches[0].pageY - self.startY; self.startY = e.touches[0].pageY; self.y += self.diffY * .3; } else if (self.y <= self.boxHeight - self.height) { self.diffY = e.touches[0].pageY - self.startY; self.startY = e.touches[0].pageY; self.y += self.diffY * .3; } else { self.diffY = e.touches[0].pageY - self.startY; self.startY = e.touches[0].pageY; self.y += self.diffY; if (self.options && self.options.scrollBar) { self.changePosition(); } } self.bottomHeight = self.overHeight + self.y; //利用requestAnimationFrame做transform的动画过程中,不允许添加DOM,个人猜测js机制不允许……暂时关闭scrolling接口 self._sendEvent("scrolling"); self.transform(); },false); this.element.addEventListener("touchend",function (e) { self.endTime = utils.getTime(); self.endY = e.changedTouches[0].pageY; self._end(e); },false); }, stop: function () { if (this.isAnimating) { this.isAnimating = false; } }, _end: function (e) { var duration = this.endTime - this.startTime, newY = Math.round(this.endY); if (duration < 300) { aniData = utils.momentum(newY,this.oStartY,duration,this.y,this.boxHeight,-this.overHeight); this.speed = aniData.speed; this.children.style.transitionTimingFunction = utils.ease.quadratic.style; this._animate(aniData.destination,aniData.duration,utils.ease.quadratic.fn,aniData.speed); } else if (this.y > 0) { this.scrollTo(0,20,200); } else if (this.y <= -this.overHeight) { this.scrollTo(-this.overHeight,20,200); } else { if (this.events["scrollEnd"]) { this._sendEvent("scrollEnd"); } } }, //刷新列表 refresh: function () { this._sendEvent("destroy"); this.events.destroy = []; if (this.children) { this.height = this.children.scrollHeight; this.boxHeight = this.element.offsetHeight; this.overHeight = this.height - this.boxHeight; this.style = this.children.style; } if (this.options && this.options.scrollBar) { if (this.scrollBox.className === "clipScrollBox") { this.scrollBox.setAttribute( "style","position:absolute; width: 5px; height:100%; top: 0; right: 0; border: 1px solid #fff; background: rgba(255,255,255,.7); border-radius: 4px; overflow: hidden; z-index: 1000"); this.scrollItem.setAttribute("style","width: 100%; height: " + this.boxHeight * 100 / this.height + "%; background: #999; border-radius: 4px;") } else { this.scrollBox.setAttribute("style","position: absolute; height:100%; top: 0; right: 0; overflow: hidden; z-index: 1000"); this.scrollItem.setAttribute("style","width: 100%; height: " + this.boxHeight * 100 / this.height + "%;") } this.changePosition(); } }, //事件绑定,实质就是自定义一个事件名称,将需要执行的方法存放在这个数组中,在代码需要的时候遍历这个事件数组,去执行里面的方法。 on: function (type,fn) { if (!this.events[type]) { this.events[type] = []; } this.events[type].push(fn); }, //事件触发器,在代码合适的地方调用该方法,这个方法会遍历events中的对应的事件名下的所有方法,并且依次执行。这里,我们的方法都是实例化改对象时候使用者写入的方法。 _sendEvent: function (type) { if (!this.events[type]) { this.events[type] = []; } var l = this.events[type].length,i = 0; for ( ; i < l; i++) { this.events[type][i].apply(this,[].slice.call(arguments, 1));//保证从第一个参数传递 } }, _animate: function (destY,duration,easingFn,speed) { var startTime = utils.getTime(), self = this, startY = this.y, destTime = startTime + duration, time = 0; function stepAnimation() { var now = utils.getTime(), newY, easing; if ( now >= destTime ) { self.isAnimating = false; // INSERT POINT: _end if ( destY > 0 ) { time = destY / speed; self.scrollTo(0, time,speed); } else if (destY < -self.overHeight) { time = (Math.abs(destY) - self.overHeight) / speed; self.scrollTo(-self.overHeight, time,speed); } else { self.transform(destY); self._sendEvent('scrollEnd'); } return; } self._sendEvent("scrolling"); now = (now - startTime) / duration; easing = easingFn(now); newY = (destY - startY) * easing + startY; self.transform(newY); self.bottomHeight = self.overHeight + self.y; if (self.options && self.options.scrollBar) { self.changePosition(); } if (self.isAnimating) { rAF(stepAnimation); } } this.isAnimating = true; stepAnimation(); }, scrollTo: function (position,time,speed) { this._animate(position,time * 15,utils.ease.quadratic.fn,speed / 15); }, tap: function (element,callBack) { var startY = 0, endY = 0, isMove = false, startTime = 0, endTime = 0, maxTime = 500; function start(e) { startY = e.touches[0].pageY; startTime = utils.getTime(); } function move(e) { isMove = true; } function end(e) { endTime = utils.getTime(); endY = e.changedTouches[0].pageY; if (Math.abs(endY - startY > 10)) { return; } if (isMove) { isMove = false; return; } if (endTime - startTime > maxTime) { return; } callBack(); } element.addEventListener("touchstart",start,false); element.addEventListener("touchmove",move,false); element.addEventListener("touchend",end,false); this.on("destroy",function () { element.removeEventListener("touchstart",start,false); element.removeEventListener("touchmove",move,false); element.removeEventListener("touchend",end,false); }); }, }; //工具对象 var utils = (function () { var me = {}; me.getTime = function () { return Date.now() || new Date().getTime(); }; //计算执行动画所需的参数 me.momentum = function (current,startY,time,y,wrapperSize,lowerMargin) { var deceleration = 0.0006, distance = current - startY, speed = Math.abs(distance / time), data = null; destination = y + ( speed * speed ) / ( 2 * deceleration ) * ( distance < 0 ? -1 : 1 ); duration = Math.round(Math.abs(speed / deceleration)); if (destination < lowerMargin) { destination = wrapperSize ? lowerMargin - ( wrapperSize / 2.5 * ( speed / 8 ) ) : lowerMargin; distance = Math.abs(destination - y); duration = distance / speed; } else if (destination > 0) { destination = wrapperSize ? wrapperSize / 2.5 * ( speed / 8 ) : 0; distance = Math.abs(y) + destination; duration = distance / speed; } data = { destination: Math.round(destination), duration: duration, speed: speed, }; return data; }; me.bounce = function (current,targetY,speed) { var distance = Math.abs(targetY - current), speed = speed * .6, time = distance / speed; return { time: time, speed: speed, }; }; me.extend = function (ease,obj) { for (var i in obj) { ease[i] = obj[i]; } }; me.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; })(); DScroll.utils = utils; if (typeof module != "undefined" && module.exports) { module.exports = DScroll; } else if ( typeof define == 'function' && define.amd ) { define( function () { return DScroll; } ); } else { window.DScroll = DScroll; } })(window,document,Math);