在一些项目中,用户总是要求自定义一下滚动条,以前一般用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);
}
插件使用实例:
```c
```c
```c
```javascript
<!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);
github下载地址:https://github.com/definedUserName/DeftScroll.js