全面让你了解和打造本身的自定义滚动条(提供组件

前言

最近在封装一个自定义滚动条容器,打算之后用它来取代经常使用的div标签,由于在Window上的浏览器的确比较丑,为了跟mac里的滚动条尽可能保持一致,本身动手封一个。css

写该篇文章目的有俩html

  1. 方便之后本身再作相似的工做好来个回顾,避免频繁查阅各类资料
  2. 在动手时发现现有网络资源的一些不足之处,在这里加以补充和描述,但愿后来之人在查阅资料时能看到这篇文章就能知足所需。

简单说下目前一些网络资料待增强的地方,我这里会针对这些问题弥补一下vue

  • 只有容器内滚动(如鼠标滚轮滚动引发),自定义滚动条按比例移动的实现方案
  • 只有点击自定义滚动条拖动,容器内容要滚动的实现方案
    • 以上两个方案没有合并一块儿提供完整方案
  • 各类计算标准,眼花缭乱,最好提供一更好理解的一些标准计算
  • 有些方案是针对特定的场景,没有考虑全面,例如仅针对bod页面垂直滚动条,咱们这里要考虑的是把全部滚动条都变成自定义
  • 没有针对内容变化,继而改变滚动条高度
  • 没有作一些兼容处理

组件

开始文章以前,我想介绍一下我用vue封装的一个自定义容器组件。方即可能有些人可能只想找这么一个现成的解决方案组件,而不想细看其中的前因后果。web

请戳 npm地址npm

并且,这个组件比下面讲解的解放方案会有更多优化工做,毕竟为了方便你们理解,过多的拓展优化我就不在这篇文章里一一介绍。浏览器

特色

  • 针对滚动条区域不占用内容自己空间,影响尺寸的浏览器滚动条,采用原生滚动条,组件最终也只会渲染成一个div标签。
    • mac系统上的绝大部分浏览器(暂时没遇到不是的),它的原生滚动条自己交互效果仍是挺好且好看的,不须要自定义滚动条
    • 除上述MAC的状况外,因为方案的实现问题,对这类浏览器的滚动条不作自定义处理,如window系统上的浏览器,这种状况比较少见(暂时没遇到)。因此不为这种少数的状况作处理,增长方案复杂度。
    • 自定义滚动条会渲染成几个嵌套结构,增长DOM,因此能不用就不用了
  • 针对非上述两种状况下的浏览器,通常为window系统的浏览器,若是是webkit内核的浏览器,组件就会利用-webkit-scrollbar等css方式自定义原生滚动条样式,最终渲染成一个div标签。——这个选择是用户可选的,能够不用这个效果。
  • 除了上述状况,都会采用自定义滚动条方式,这样分状况来渲染不一样的结果,能够最大程度上采用最简单的方式,来知足好看的滚动条样式。
  • 组件是包含横向和垂直滚动条

简而言之,组件会采起“最优”的方案,在知足滚动条样式可观的状况下,采用渲染结构最简单,组件性能最好的方案。微信

下面文章只讲自定义滚动条的部分,不展开讲述兼容判断。网络

核心思想

首先要明确实现的目标:弃用浏览器提供的默认滚动条,咱们自行用DOM元素来模拟滚动条行为函数

咱们从一个正常的浏览器滚动条现象来模型化。咱们以垂直方向的滚动条为例子说明post

如图这是一个带有滚动条的容器的状况

图一:

蓝色部分为内容实际高度

图二:

咱们把上述的现象抽象成如下图

图三:

  • 实际内容区域:即现象图里的蓝色部分
  • 内容的可视区域:即图一灰色框区域
  • 滚动条所能游动区域:即滚动条容器区域,不是仅仅浮标高度,其实是等于内容的可视区域
  • 滚动条浮标的高度:即你拖动滚动条上下移动的那块,即滚动条容器里的深灰色部分高度

好了。咱们理解完相关“区域”,接下来来文字化滚动条的交互行为。

滚动条交互行为

如下的描述并非真正的行为本质,可是从现象上咱们能够按照下述的效果来理解。

容器发生滚动时,实际内容区域向上/下移动;而滚动条浮标也会跟着向下/上移动。

这里提出一个问题:内容滚动的距离,跟滚动条浮标移动的距离,有什么关系?即内容滚了多少,滚动条浮标应该对应移动多少?

为何有这个问题呢,由于当内容滚动到底,浮标也要到达底部,即内容所能滚动的距离,跟浮标所能移动距离,是有按照必定比例来协调的。

比例关系

咱们看着图三来理解,容器发生滚动时,实际内容区域向上/下移动,就比如内容可视区域向下/上移动;而滚动条浮标也会跟着向下/上移动。

有没有发现,容器可视区域的移动行为和滚动条浮标的移动行为是很类似的。

咱们把“实际内容区域”看做“滚动条所能游动的区域”,“内容的可视区域”看做“滚动条浮标的高度”,若是这样来看待滚动行为的话,他们的比例关系就一目了然了。

实际内容区域 / 内容的可视区域 = 滚动条浮标的高度 / 滚动条所能游动的区域

此外还有其余比例关系的公式,但都遵循一个原则,就是在内容区域行为上表现一致的,跟在滚动条区域表现一致的是构成一个比例的。如

实际内容区域移动距离 / 内容的可视区域 = 滚动条浮标移动距离 / 滚动条所能游动的区域

因为浏览器默认提供的滚动条,它的浮标高度已是计算好了,咱们日常也没多大关心。可是如今咱们要写自定义的滚动条,因此要计算出这个浮标的高度,咱们根据上述第一个比例公式就能够算出浮标的高度了。

咱们先把文字公式,转化成代码公式,

根据第一个比例关系公式:

scrollHeight / clientHeight = h / clientHeight

“滚动条所能游动的区域”其实是等于“内容的可视区域”,h表明“滚动条浮标的高度”

根据第二个比例关系公式:

scrollTop / scrollHeight = top / clientHeight

top表明“滚动条浮标移动距离”,所以根据该公式就能够算出滚动条浮标移动距离了。

小结

上面花了那么多文字来一步步得出比例关系,就是为了让你们了解清楚比例关系,这样的话后续要进行的各种计算,都能驾轻就熟。

不想了解前因后果的的话,能够先记住两个公式

实际内容区域 / 内容的可视区域 = 滚动条浮标的高度 / 滚动条所能游动的区域

scrollHeight / clientHeight = h / clientHeight

实际内容区域移动距离 / 内容的可视区域 = 滚动条浮标移动距离 / 滚动条所能游动的区域

scrollTop / scrollHeight = top / clientHeight

方案详讲

首先咱们明确一个大目标,这里的方案会用一段html元素组合来表示一个“滚动容器(滚动条不是原生的,是自定义的)”。如,本来的实现是创建一个div容器,里面的内容会引发滚动,此时,你想要自定义的滚动条,那么要用这里方案的一段html组合来替换这个div

是的,无疑这个样式上的优化会换来DOM增长的代价(其实不仅仅这个代价),固然有纯css的方式修改原生滚动条样式,可是有兼容性问题,很显然,不是这篇文章的重点,可是,标榜着“全面”方案两字,我必须得考虑到尽可能使用性能好的css手段(具体后面说),这里仍是集中在利用js手段实现。

你们能够不像按照我这么用,能够经过我方案里的这段html代码为例子,学习自定义滚动条的实现方案。

html & CSS

<!--html-->
<div class="scroll-div">
    <div class="scroll-div-view"></div>
    <div class="scroll-div-y">
        <div class="scroll-div-y-bar"></div>
    </div>
</div>
复制代码

其中,.scroll-div-view就是提供滚动条的容器,也就是你的内容区域;.scroll-div-y为滚动条所在区域,.scroll-div-y-bar为滚动条浮标。

接下来看下css的状况

.scroll-div {
    position: relative;
    display: inline-block;
    overflow: hidden;
    user-select: none;
}
.scroll-div-view {
    margin-left: -17px;
    margin-bottom: -17px;
    overflow: scroll;
    /**宽高的设置是示例,方便你们理解,事实上不该该写死的。**/
    width: 400px; 
    height: 100px;
}
.scroll-div-y {
    position: absolute;
    right: 1px;
    top: 0;
    height: 100%;
    width: 7px;
}
.scroll-y-bar {
    width: 7px;
    border-radius: 7px;
    background-color:rgba(0, 0, 0, .5);
    cursor: pointer;
    opacity: 0;
    transition: opacity .5s ease 0s;
}
.scroll-y-bar.is-show {
    opacity: 1;
    transition: opacity 0s ease 0s;
}
复制代码

简单描述一下上述样式的做用。

首先父元素.scroll-div设置了display:inline-block,具备“包裹性”,沿用内容区域.scroll-div-view的宽高。而.scroll-div-view设置了overflow:scroll,不论怎样都会显示滚动条,咱们的目的是看不到原生滚动条,因此设置了margin-leftmargin-right都是-17px(window下的浏览器的滚动条通常为17px,这里先这么写着,后续要有方法计算出每一个浏览器各自的滚动条宽度),这样设置以后就会超出父元素的宽高,可是随着父元素.scroll-div设置overflow:hidden,就能把子元素.scroll-div-view超出的内容给隐藏了,即把超出的滚动条区域给hidden掉了。

而滚动条所在区域.scroll-div-y是相对父元素.scroll-div作绝对定位,定位在右边,高度和父元素同样。

而后咱们设置滚动条浮标.scroll-div-y-bar的样式,能够看到,我设置了opacity,这里是用透明度来控制滚动条浮标的隐藏和显示,而不是用displayvisibility,有如下缘由:

  • 我想让滚动条消失是渐变的,即有动画效果的,用display控制隐藏消失不能应用动画效果transition
  • visibility会引发重绘,而用opacity则不会。

js脚本控制滚动条

这里主要是实现:

  • 拖动滚动条浮标,引发滚动
  • 进行滚动操做(如滚动鼠标滚轮),滚动条浮标随着移动

要实现的滚动条交互效果,是参考mac系统浏览器上的交互状况,第一,我以为mac系统的交互效果还蛮好的,第二,为了尽可能让用户感觉统一,即在macwindow系统上,交互效果能尽可能统一,好让用户习惯。所以,在这里的脚本,除了实现上述两个主要目的外,还会附带一些实现这些交互效果的功能脚本。

初始化时

初始化时,获取各个html元素对象,且根据容器的实际宽高状况动态计算滚动条浮标的高度;最后为内容容器进行滚动监听,为滚动条区域添加鼠标移入监听(即悬浮效果);各自绑定事件函数以及这里一开头定义的一些变量后面会具体讲其用途。

const scrollTop = 0; // 记录最新一次滚动的scrollTop,用于判断滚动方向
const timer = null; // 滚动条消失定时器
const startY = 0; // 记录最新一次点击滚动条时的pageY
const distanceY = 0; // 记录每次点击滚动条浮标时的内容容器此刻的scrollTop
const scrollContainer = document.querySelector('.scroll-div-view');
const scrollY = document.querySelector('.scroll-div-y');
const scrollYBar = document.querySelector('.scroll-div-y-bar');
calcSize(); // 计算滚动条浮标高度
scrollContainer.addEventListener('scroll', handleScroll);
scrollY.addEventListener('mouseover', hoverSrollYBar);

/** * 计算垂直滚动条的高度 */
function calcSize () {
    const clientAreaValue = scrollContainer.clientHeight;
    // 根据公式一算出高度
    scrollYBar.style.height = clientAreaValue * clientAreaValue / scrollContainer.scrollHeight + 'px';
}
复制代码

内容滚动时

当你进行滚动操做,如滚动鼠标滚轮,或触摸板上进行上下滚动操做时,触发内容容器.scroll-div-view绑定的scroll事件,该事件绑定如下函数(具体有注释解释)

/** * 处理内容滚动事件 */
handleScroll (el) {
    const e = el || event;
    const target = e.target || e.srcElement;
    // 若是最新一次滚动的scrollTop跟上一次不一样,即发生了垂直滚动
    // 主要是为了区分是垂直滚动仍是横向滚动,由于这里暂时不写横向滚动条,因此这里注释,为了一个提醒
    // if (target.scrollTop !== scrollTop) {}
    const scrollAreaValue = scrollContainer.scrollHeight;
    const clientAreaValue = scrollContainer.clientHeight;
    const scrollValue = scrollContainer.scrollTop;
    scrollYBar.className += ' is-show'; // 展现滚动条浮标
    timer && clearTimeout(timer);
    calcSize(); // 每次滚动的时候从新计算滚动条尺寸,以避免容器内容发生变化后,滚动条尺寸不匹配变化后的容器宽高
    const distance = scrollValue * clientAreaValue / scrollAreaValue; // 根据公式二计算滚动条浮标应该移动距离
    scrollYBar.style.transform = `translateY(${distance}px)`;
    timer = setTimeout(() => {
        scrollYBar.className = scrollYBar.className.replace(' is-show', ''); // 隐藏滚动条浮标
    }, 800);
    scrollTop = target.scrollTop;
}
复制代码

总结下上述函数的做用:内容发生滚动时,根据公式二,计算滚动条浮标应该移动距离,求出以后套用在transform: translateY()样式里,样式上就能看出滚动在移动了。

其实这个应该是个很简单的函数才对,根据公式计算而后赋值样式。

<!-- 关键代码,只写这两个就能实现滚动内容时,滚动条浮标跟着移动 -->

const distance = scrollValue * clientAreaValue / scrollAreaValue; // 根据公式二计算滚动条浮标应该移动距离
scrollYBar.style.transform = `translateY(${distance}px)`;
复制代码

为何上述看起来那么多代码呢?由于以前说了,要模拟mac系统的交互效果:

  1. 默认不展现滚动条,滚动时才会出现滚动条
  2. 滚动以后限定时间内不继续滚动,滚动条会消失

因此其他代码主要是为了实现这两个效果,其中还有一行代码是计算滚动条浮标高度的。这么作的目的是,当你内容发生变化时,如请求一些数据以后,内容变多变少了,滚动条高度是须要从新计算的,否则根据公式计算就不许了。

我我的以为这种交互挺好的,在每次滚动时就从新展现滚动并计算最新的高度。可是当你不想用这种交互时,你想当内容尺寸大于可视区域,一直出现滚动条时,如如今的window系统的滚动条交互,这样的话,你就须要监听好内容的状况了,当发生变化后,须要从新计算滚动条浮标高度。这样的话,就会致使另外一个问题的产生了,若是监听内容?暂时个人脑海中所想到的方法是使用MutationObserver,可是这家伙是有兼容性问题的,在不考虑ie的状况,其实也还好。可参考 此文章 ,这是题外话,我这里的方案没有包括这个。

拖动滚动条进行内容滚动

悬浮滚动条

在初始化的时候咱们看到,对scrollY绑定了mouseover事件。如今咱们看看这个事件作了啥。

我这里添加鼠标悬浮事件的是滚动条所在区域,而不只仅是滚动条浮标,由于当你滚动一段距离后,浮标隐藏了你很难知道本来移动在哪里,因此干脆就直接对整个滚动条所在区域进行悬浮监听。

当知足显示滚动条条件时,还要从新滚动条浮标高度,确保跟内容高度按比例协调。

注意我是触发了mouseover以后才对滚动条浮标绑定mousedown事件以及滚动条所在区域绑定mouseout事件。这样确保是在显示出滚动条才进行监听,减小频繁的触发没必要要的事件,减小性能损耗。

/** * 鼠标移入(悬浮)滚动条或滚动条所在区域 */
function hoverScrollBar () {
    const sA = scrollContainer.scrollHeight;
    const cA = scrollContainer.clientHeight;
    // 达到展现滚动条条件时
    if (sA > cA) {
        scrollYBar.style[style] = cA * cA / sA + 'px'; // 设置滚动条长度
        scrollYBar.className += ' is-show';
        scrollYBar.addEventListener('mousedown', clickStart);
        scrollY.addEventListener('mouseout', hoverOutSroll);
    }
}
复制代码
按住滚动条

下面是当点击滚动条浮标时的处理

/** * 点击垂直滚动条 */
function clickStart (el) {
    const e = el || event;
    const target = e.target || e.srcElement;
    startY = e.pageY; // 记录此刻点击时的pageY,用于后面拖动鼠标计算移动了多少距离
    distanceY = scrollContainer.scrollTop; // 记录此刻点击时的内容容器的scrollTop,用于后面根据拖动鼠标移动距离计算得出的内容容器对应滚动比例,进行相加操做,得出最终的scrollTop
    scrollY.removeEventListener('mouseout', hoverOutSroll);
    document.addEventListener('mousemove', moveScrollYBar);
    document.addEventListener('mouseup', clickEnd);
}
复制代码

上面该函数主要是记录一些后面用于拖动鼠标计算的开始值,以及对页面绑定鼠标移动和鼠标松开事件,这样确保在点击了滚动条后才触发监听,否则直接对文档进行这两个高频事件监听,是不可取的。

这里为何要对页面文档自己作事件监听而不是对滚动条自己监听呢。由于有一个场景,就是拖动滚动条有时候会离开滚动区域的,这时候在未松开鼠标前,应该仍是得显示滚动条浮标以及还能拖动。一个图说明这种状况

这种状况就是鼠标已经不在滚动条上了,因此要在document上监听,且还要移除本来对滚动条区域监听的mouseout事件

移出滚动条区域

下面咱们再看下监听的mouseout作了什么:

/** * 滚动条所在区域鼠标移出时,滚动条要消失 */
function hoverOutSroll (el) {
    const e = el || event;
    const target = e.target || e.srcElement;
    scrollYBar.className = scrollYBar.className.replace(' is-show', ''); // 隐藏滚动条浮标
    scrollYBar.removeEventListener('mousedown', clickStart);
    scrollY.removeEventListener('mouseout', hoverOutSroll);
}
复制代码

其实这个移出事件要作的事情很简单:就是隐藏滚动条浮标,而后解除本来的一些绑定,减小高频监听。

按住拖动滚动条

这是关键的事件监听,主要的功能是,根据鼠标移动的距离,即滚动条浮标移动的距离,按照公式二计算得出对应的内容滚动了的距离,而后加上先前已经滚动的距离,得出最终滚动的距离,而后继续触发scroll事件,天然算出并变更滚动条浮标的移动位置。

其中要注意滚动条的移动极限,即顶部和底部。

/** * 按住滚动条移动 */
function moveScrollBar (el) {
    const e = el || event;
    const delta = e.pageY - startY;
    const scrollAreaValue = scrollContainer.scrollHeight;
    const clientAreaValue = scrollContainer.clientHeight;
    let change = scrollAreaValue * delta / clientAreaValue; // 根据移动的距离,计算出内容应该被移动的距离(scrollTop)
    change += distanceY; // 加上本来已经移动的内容位置,得出确实的scrollTop
    // 若是计算值是负数,证实确定回到滚动最开始的位置了
    if (change < 0) {
        scrollContainer.scrollTop = 0;
        return;
    }
    // 若是大于最大等于移动距离,那么即到达底部
    if (change + clientAreaValue >= scrollAreaValue) {
        scrollContainer.scrollTop = scrollAreaValue - clientAreaValue;
        return;
    }
    scrollContainer.scrollTop = change; // 设置了scrollTop会引发scroll事件的触发
}
复制代码
松开鼠标

这是最后一步了,当松开了鼠标,主要是解绑以前的一些监听。以及把滚动条的移出监听从新加回来,毕竟,以前在按住滚动条时解绑了。

/** * 按住滚动条移动完松开鼠标后 */
function clickEnd () {
    document.removeEventListener('mousemove', moveScrollYBar);
    document.removeEventListener('mouseup', clickEnd);
    scrollY.addEventListener('mouseout', hoverOutSroll);
}
复制代码

小结

以上即为该篇文章介绍如何制做一个自定义滚动条的详细讲解方案,里面的关于交互的脚本设计,都是能够根据你本身的喜爱来变更调整,这是在讲解方案时顺带说起的,只要你掌握本质的自定义功能,后面都能举一反三,触类旁通。

固然该方案有能够留意的兼容性问题

不支持IE9如下,自定义滚动条是个ui美化的工做,既然都是用IE9如下的浏览器了,对这方面的追求,其实也不显得多重要了。因为本方案采用了csstransform属性进行滚动条的移动,IE9如下不支持,若是你想支持的话,请在样式方面替换成绝对定位,用方位属性top, left代替;且绑定事件请用attachEvent。这里不提供该兼容方案的整合。

对比

这小节能够不用看,可是我我的仍是写出来了,请知晓,写这节内容不是为了凸显个人方案有多好。只是为了方便往后本身在查阅资料,再遇到这类资料,能够快速知道其利弊,避免花更多时间从新去解读分析。

有些资料会采用绝对定位内容容器,经过控制方位属性值来模拟拖动滚动条内容发生滚动,的确这个方式挺好的。可是惋惜的是,若是不是像本方案那样用能使用scroll事件来处理滚动时滚动条浮标跟着移动,得采用mousewheelwheel事件来处理了,这是有弊端的:

  1. 兼容性问题
  2. 很差获取滚动距离,如你滚动鼠标滚动,触发了wheel事件,可是你很差获取这个“滚动距离”是多少。
  3. 等你真的作完了上面提到的问题,远不如我这里的方案来的简单。

还有些资料的运算指标是用offsetTopoffsetLeft,这种状况只能在某种特定场景下好用,对于咱们这里的最终目标是生产一个通用的“元素”来应用在任何页面位置上,如把我上述方案封装成一个vue组件或web component,就能用一个自定义标签来表示能展现自定义滚动条的容器了。

微信公众号

==未经容许,请勿私自转载==