前言
最近在封装一个自定义滚动条容器,打算以后用它来取代常用的 div标签,因为在Window上的浏览器的确比较丑,为了跟mac里的滚动条尽量保持一致,自己动手封一个。
写该篇文章目的有俩
方便以后自己再做类似的工作好来个回顾,避免频繁查阅各种资料
在动手时发现现有网络资源的一些不足之处,在这里加以补充和描述,希望后来之人在查阅资料时能看到这篇文章就能满足所需。
简单说下目前一些网络资料待加强的地方,我这里会针对这些问题弥补一下
只有容器内滚动(如鼠标滚轮滚动引起),自定义滚动条按比例移动的实现方案
以上两个方案没有合并一起提供完整方案
各种计算标准,眼花缭乱,最好提供一更好理解的一些标准计算
有些方案是针对特定的场景,没有考虑全面,例如仅针对bod页面垂直滚动条,我们这里要考虑的是把所有滚动条都变成自定义
没有针对内容变化,继而改变滚动条高度
没有做一些兼容处理
组件
开始文章之前,我想介绍一下我用 vue封装的一个自定义容器组件。方便可能有些人可能只想找这么一个现成的解决方案组件,而不想细看其中的来龙去脉。
而且,这个组件比下面讲解的解放方案会有更多优化工作,毕竟为了方便大家理解,过多的拓展优化我就不在这篇文章里一一介绍。
特点
针对滚动条区域不占用内容本身空间,影响尺寸的浏览器滚动条,采用原生滚动条,组件最终也只会渲染成一个div标签。
如mac系统上的绝大部分浏览器(暂时没遇到不是的),它的原生滚动条本身交互效果还是挺好且好看的,不需要自定义滚动条
除上述MAC的情况外,由于方案的实现问题,对这类浏览器的滚动条不做自定义处理,如window系统上的浏览器,这种情况比较少见(暂时没遇到)。所以不为这种少数的情况做处理,增加方案复杂度。
自定义滚动条会渲染成几个嵌套结构,增加DOM,所以能不用就不用了
针对非上述两种情况下的浏览器,一般为window系统的浏览器,如果是webkit内核的浏览器,组件就会利用-webkit-scrollbar等css方式自定义原生滚动条样式,最终渲染成一个div标签。——这个选择是用户可选的,可以不用这个效果。
除了上述情况,都会采用自定义滚动条方式,这样分情况来渲染不同的结果,可以最大程度上采用最简单的方式,来满足好看的滚动条样式。
组件是包含横向和垂直滚动条
简而言之,组件会采取“最优”的方案,在满足滚动条样式可观的情况下,采用渲染结构最简单,组件性能最好的方案。
下面文章只讲自定义滚动条的部分,不展开讲述兼容判断。
核心思想
首先要明确实现的目标:弃用浏览器提供的默认滚动条,我们自行用DOM元素来模拟滚动条行为
我们从一个正常的浏览器滚动条现象来模型化。我们以垂直方向的滚动条为例子说明
如图这是一个带有滚动条的容器的情况
图一:
蓝色部分为内容实际高度
图二:
我们把上述的现象抽象成以下图
图三:
实际内容区域:即现象图里的蓝色部分
内容的可视区域:即图一灰色框区域
滚动条所能游动区域:即滚动条容器区域,不是仅仅浮标高度,实际上是等于内容的可视区域
滚动条浮标的高度:即你拖动滚动条上下移动的那块,即滚动条容器里的深灰色部分高度
好了。我们理解完相关“区域”,接下来来文字化滚动条的交互行为。
滚动条交互行为
以下的描述并不是真正的行为本质,但是从现象上我们可以按照下述的效果来理解。
容器发生滚动时,实际内容区域向上/下移动;而滚动条浮标也会跟着向下/上移动。
这里提出一个问题:内容滚动的距离,跟滚动条浮标移动的距离,有什么关系?即内容滚了多少,滚动条浮标应该对应移动多少?
为什么有这个问题呢,因为当内容滚动到底,浮标也要到达底部,即内容所能滚动的距离,跟浮标所能移动距离,是有按照一定比例来协调的。
比例关系
我们看着图三来理解,容器发生滚动时,实际内容区域向上/下移动,就好比内容可视区域向下/上移动;而滚动条浮标也会跟着向下/上移动。
有没有发现,容器可视区域的移动行为和滚动条浮标的移动行为是很相似的。
我们把“实际内容区域”看作“滚动条所能游动的区域”,“内容的可视区域”看作“滚动条浮标的高度”,如果这样来看待滚动行为的话,他们的比例关系就一目了然了。
实际内容区域 / 内容的可视区域 = 滚动条浮标的高度 / 滚动条所能游动的区域
此外还有其他比例关系的公式,但都遵循一个原则,就是在内容区域行为上表现一致的,跟在滚动条区域表现一致的是构成一个比例的。如
实际内容区域移动距离 / 内容的可视区域 = 滚动条浮标移动距离 / 滚动条所能游动的区域
由于浏览器默认提供的滚动条,它的浮标高度已经是计算好了,我们平常也没多大关心。但是现在我们要写自定义的滚动条,所以要计算出这个浮标的高度,我们根据上述第一个比例公式就可以算出浮标的高度了。
我们先把文字公式,转化成代码公式,
根据第一个比例关系公式:
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-left和 margin-right都是 -17px(window下的浏览器的滚动条一般为17px,这里先这么写着,后续要有方法计算出每个浏览器各自的滚动条宽度),这样设置之后就会超出父元素的宽高,但是随着父元素 .scroll-div设置 overflow:hidden,就能把子元素 .scroll-div-view超出的内容给隐藏了,即把超出的滚动条区域给 hidden掉了。
而滚动条所在区域 .scroll-div-y是相对父元素 .scroll-div做绝对定位,定位在右边,高度和父元素一样。
然后我们设置滚动条浮标 .scroll-div-y-bar的样式,可以看到,我设置了 opacity,这里是用透明度来控制滚动条浮标的隐藏和显示,而不是用 display或 visibility,有以下原因:
我想让滚动条消失是渐变的,即有动画效果的,用display控制隐藏消失不能应用动画效果transition
用visibility会引起重绘,而用opacity则不会。
js脚本控制滚动条
这里主要是实现:
拖动滚动条浮标,引起滚动
进行滚动操作(如滚动鼠标滚轮),滚动条浮标随着移动
要实现的滚动条交互效果,是参考 mac系统浏览器上的交互情况,第一,我觉得 mac系统的交互效果还蛮好的,第二,为了尽量让用户感受统一,即在 mac和 window系统上,交互效果能尽量统一,好让用户习惯。因此,在这里的脚本,除了实现上述两个主要目的外,还会附带一些实现这些交互效果的功能脚本。
初始化时
初始化时,获取各个html元素对象,且根据容器的实际宽高情况动态计算滚动条浮标的高度;最后为内容容器进行滚动监听,为滚动条区域添加鼠标移入监听(即悬浮效果);各自绑定事件函数以及这里一开头定义的一些变量后面会具体讲其用途。
const scrollTop = 0; // 记录最新一次滚动的scrollTop,用于判断滚动方向
const timer = null; // 滚动条消失定时器
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系统的交互效果:
默认不展示滚动条,滚动时才会出现滚动条
滚动之后限定时间内不继续滚动,滚动条会消失
所以其余代码主要是为了实现这两个效果,其中还有一行代码是计算滚动条浮标高度的。这么做的目的是,当你内容发生变化时,如请求一些数据之后,内容变多变少了,滚动条高度是需要重新计算的,不然根据公式计算就不准了。
我个人觉得这种交互挺好的,在每次滚动时就重新展示滚动并计算最新的高度。但是当你不想用这种交互时,你想当内容尺寸大于可视区域,一直出现滚动条时,如现在的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;
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以下的浏览器了,对这方面的追求,其实也不显得多重要了。由于本方案采用了 css的 transform属性进行滚动条的移动,IE9以下不支持,如果你想支持的话,请在样式方面替换成绝对定位,用方位属性 top,left代替;且绑定事件请用 attachEvent。这里不提供该兼容方案的整合。
对比
这小节可以不用看,但是我个人还是写出来了,请知晓,写这节内容不是为了凸显我的方案有多好。只是为了方便日后自己在查阅资料,再遇到这类资料,可以快速知道其利弊,避免花更多时间重新去解读分析。
有些资料会采用绝对定位内容容器,通过控制方位属性值来模拟拖动滚动条内容发生滚动,的确这个方式挺好的。但是可惜的是,如果不是像本方案那样用能使用 scroll事件来处理滚动时滚动条浮标跟着移动,得采用 mousewheel或 wheel事件来处理了,这是有弊端的:
兼容性问题
不好获取滚动距离,如你滚动鼠标滚动,触发了wheel事件,但是你不好获取这个“滚动距离”是多少。
等你真的做完了上面提到的问题,远不如我这里的方案来的简单。
还有些资料的运算指标是用 offsetTop或 offsetLeft,这种情况只能在某种特定场景下好用,对于我们这里的最终目标是生产一个通用的“元素”来应用在任何页面位置上,如把我上述方案封装成一个 vue组件或 web component,就能用一个自定义标签来表示能展示自定义滚动条的容器了。