实现自定义滚动条 ScrollBar
2.5k9WEB自定义滚动条2019-08-15

写在前面

原生的滚动条很强大,但是各个浏览器对于滚动条开放的 api 不同,google 是相当有好的,样式几乎都可以修改,But IE 就只能修改颜色,然而只改一个颜色不够啊,还是那么粗那么生硬
为了保证样式的统一,就得自定义一个滚动条,如果你也想写或者你正在写但是卡在了某个地方,那就看看我踩过的坑吧

效果

效果

实现思路

首先需要实现这几个功能

*搭建好基本 DOM 框架
*计算滑块高度和位置
*当鼠标滚轮滑动,页面同步滚动
*鼠标左键点击可以实现拖动
*当 DOM 发生变化,更新滚动条位置和长度

搭建好基本 DOM 框架

首先你得要有一个横向和一个纵向的滚动条出现在目标元素的右边和底部
这是生成的基本的框架,ScrollBar 是目标元素,containerY 和 containerX 分别是纵向和横向的滚动条,wrapper 主要是为了滚动条定位也为了不去污染原本的 html 结构

1
2
3
4
5
6
7
8
9
<div class="wrapper">
<div class="ScrollBar"></div>
<div class="containerY">
<div></div>
</div>
<div class="containerX">
<div></div>
</div>
</div>

计算滑块高度和位置

以纵向滚动条为例:

滑块的高度

  • element.offsetHeight:元素可见的高度
  • element.scrollHeight:元素实际内容的高度

滚动条的高度 = 元素可见的高度
*滑块在滚动条的占比 = 元素可见的高度 / 元素实际内容的高度 * 100*

  • 滑块是滚动条的子元素,滑块可以用 ‘absolute’ 相对于滚动条也就是父元素定位,100% 即是滚动条的高度
  • 这个地方为了方便后面的计算滑块的的位置所以就不需要去计算滑块的准确高度,只需要知道他在滚动条中的占比即可,然后用 height 展示这个百分比

滑块的位置

滑块默认位置当然 top 为 0

但是当滚动行为发生或者其他行为导致了可见区域展示的内容发生了变化,这个时候我们也需要去更新滑块在滚动条中的位置

那么这个时候滑块的位置 = element.scrollTop / element.scrollHeight * 100

  • element.scrollTop:元素实际内容的顶部与可见区域的顶部之间的距离

另外在鼠标滚动或者鼠标拖动事件中需要注意处理边界的情况,避免滑块超出滚动条

最好将更新滑块位置的方式写成函数,因为这个需要频繁调用

鼠标滚轮滑动,页面同步滚动

  • 这个主要就是对 mousewheel 事件进行监听,在回调函数中对滑块的位置和页面进行同步更新
  • 需要注意判断鼠标滚轮是向上还是向下
1
this.wrapper.addEventListener('mousewheel', function (e) { _this.wheel(e) })
  • 这里面也可以写一些其他的动效,可以让用户体验更好些,比如开始滚动效果和结束滚动什么的

鼠标左键点击实现拖动

  • 这个主要需要监听 mousedown, mousemove, mouseup 三个事件
  • 在 mousedown 中记录用户拖动的起点,mousemove 中计算用户拖动的距离然后对滑块的位置和页面进行同步更新,然后再 mouseup 中结束这个事件
  • 这个有个需要注意的点是滑块的高度,用元素 css 的 Top 属性控制
  • 用户点击滚动条某个位置就将滑块移至某个位置,这个就只需要监听 mousedown 事件

注意:需要判断是向下还是向上,如果向上只需要修改 top 的值,但如果是向下那么就需要在计算中加入滑块本身的高度,避免滑块底部超出滚动条

监听数据变化更新滚动条

监听 DOM 变化我了解到了三个方法,当然如果你还有其他的方法,欢迎留言告诉我

1.MutationObserver()
2.Resize Observer
3.requestAnimationFrame

MutationObserver

构造函数:MutationObserver(),监视和记录 DOM 对象上发生的子节点删除、属性修改、文本内容修改等等,变化结束后触发回调
在 Vue.nextTick() 中便是由 MutationObserver 监听到DOM更新然后调用回调

1
let observer = new MutationObserver(callback)

函数有三个方法:

  • observe():mutationObserver.observe(target, options) MDN参数
     ** target:受监视的 DOM 元素**
     ** options 参数介绍:**
       *attributeFilter: array, 要监视的特定属性名称的数组
       *attributeOldValue: boolean, 是否记录任何有改动的属性的上一个值
       *attributes: boolean, 是否观察属性值变更
       *characterData: boolean, 是否观察指定目标节点或子节点树中节点所包含的字符数据的变化
       *characterDataOldValue: boolean, 是否记录受监视节点上发生更改时节点文本的上一个值
       *childList: boolean, 是否观察目标子节点添加或者删除(如果subtree为true,则观察包含子孙节点)
       *subtree: boolean, 是否观察后代节点,包括整个子树的所有节点, 默认值为false
  • disconnect():阻止 MutationObserver 实例继续接收的通知,直到再次调用其observe方法,该观察者对象包含的回调函数都不会再被调用
  • takeRecords():返回已检测到但尚未由观察者的回调函数处理的所有匹配DOM更改的列表,使变更队列保持为空,此方法最常见的使用场景是在断开观察者之前立即获取所有未处理的更改记录,以便在停止观察者时可以处理任何未处理的更改。
1
2
3
4
5
6
7
8
9
var targetNode = document.querySelector("#someElement")
var observerOptions = {
childList: true,
attributes: true,
subtree: true
}

var observer = new MutationObserver(callback)
observer.observe(targetNode, observerOptions)

Resize Observer

Resize Observer, 视窗大小的变化,而不仅仅是当一个元素的大小发生变化
当元素被动态地添加或删除时,会影响父元素的大小
观察DOM元素的内容矩形大小(宽度、高度)的变化

1
2
3
4
5
window.addEventListener('resize', callback)

function callback() {
console.log(resize)
}
1
2
3
4
5
6
7
const element = document.('.box')
const myObserver = new ResizeObserver(entries => {
console.log(entries)
});

myObserver.observe(element)

requestAnimationFrame 刷新

这个方法我是在 jQuery custom content scroller 源码中看到的,它是使用 window.requestAnimationFrame 和 timeout 更新滚动条

1
window.requestAnimationFrame

你可能会碰到的问题

  • mousewheel 事件执行多次

当你的 DOM 结构是这样的

1
2
3
4
<div>
<div></div>
<div></div>
</div>

上面三个div都用了滚动条,也就是嵌套的情况下,当你想滚动里面 div 元素的时候你会发现外层的 div 元素也在滚动

打印一下 mousewheel 里面的 event, 第一个执行的是里面的 div,然后再是外层的 div

这是因为 addEventListener 默认是按照事件冒泡的顺序执行,所以最里面一层 div 会先执行, 使用 event.stopPropagation() 方法阻止事件传播

  • scrollTop 赋值无效
    元素的 scrollTop 赋值始终是 0,原来是给最外层元素的 wrapper 设置了 overflow:hidden,应该给目标元素设置,因为最外层的元素没有设高

参考文章

其他

Google 修改滚动条

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/*控制整个滚动条*/
::-webkit-scrollbar {
background-color: lightgray;
width: 10px;
height: 10px;
background-clip: padding-box;
}

/*滚动条两端方向按钮*/
::-webkit-scrollbar-button {
background-color: pink;
}

/*滚动条中间滑动部分*/
::-webkit-scrollbar-thumb {
background-color: blue;
border-radius: 5px;
}

/*滚动条右下角区域*/
::-webkit-scrollbar-corner {
background-color: red;
}


/* 更详细的伪类 */

:horizontal /*水平方向的滚动条*/

:vertical /*垂直方向的滚动条*/

:decrement /*应用于按钮和内层轨道(track piece)。它用来指示按钮或者内层轨道是否会减小视窗的位置(比如,垂直滚动条的上面,水平滚动条的左边。)*/

:increment /*decrement类似,用来指示按钮或内层轨道是否会增大视窗的位置(比如,垂直滚动条的下面和水平滚动条的右边。)*/

:start /*伪类也应用于按钮和滑块。它用来定义对象是否放到滑块的前面。*/

:end /*类似于start伪类,标识对象是否放到滑块的后面*/

:double-button /*该伪类以用于按钮和内层轨道。用于判断一个按钮是不是放在滚动条同一端的一对按钮中的一个。对于内层轨道来说,它表示内层轨道是否紧靠一对按钮。*/

:single-button /*类似于double-button伪类。对按钮来说,它用于判断一个按钮是否自己独立的在滚动条的一段。对内层轨道来说,它表示内层轨道是否紧靠一个single-button。*/

:no-button /*用于内层轨道,表示内层轨道是否要滚动到滚动条的终端,比如,滚动条两端没有按钮的时候*/

:corner-present /*用于所有滚动条轨道,指示滚动条圆角是否显示*/

:window-inactive /*用于所有的滚动条轨道,指示应用滚动条的某个页面容器(元素)是否当前被激活*/

IE 修改滚动条

1
2
3
4
5
6
7
8
9
10
div {
scrollbar-arrow-color: #f4ae21; /*三角箭头的颜色*/
scrollbar-face-color: #333; /*立体滚动条的颜色*/
scrollbar-3dlight-color: #666; /*立体滚动条亮边的颜色*/
scrollbar-highlight-color: #666; /*滚动条空白部分的颜色*/
scrollbar-shadow-color: #999; /*立体滚动条阴影的颜色*/
scrollbar-darkshadow-color: #666; /*立体滚动条强阴影的颜色*/
scrollbar-track-color: #666; /*立体滚动条背景颜色*/
scrollbar-base-color:#f8f8f8; /*滚动条的基本颜色*/
}