在一些项目中,用户总是要求自定义一下滚动条,以前一般用 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 下载地址: www.baidu.com
来源: http://www.bubuko.com/infodetail-2440203.html