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