在web开发中,自定义滚动条是个常见的需求,虽然浏览器原生的滚动条很强大并且在大多数场景下表现的很好,但某些时候我们仍然希望修改他的样式,比如变细一点,或者去掉圆角和轨道,又或者隐藏他们。这些都属于自定义行为,本篇文章将介绍自定义滚动条的几种实现思路,并着重讲解最流行的js方案。
上图是自定义的效果(视频在转换时降速了,其实非常快)
在正文开始前,我们先统一滚动条各个部分的名称。
一、实现思路
实现自定义滚动条的方式不止一种,这里列出三种方式。
1、css修改。
这是最简单的方式,你可以通过::-webkit-scrollbar这个css伪类选择器去修改滚动条样式,包括滚动条轨道、滑块以及上下箭头等,但它只支持webkit内核的浏览器,并且它不是css标准的一部分,这意味着除了浏览器兼容性问题外,将来还可能被浏览器厂商删掉并转而采用新标准。
2、自行实现滚动条部分,但scroll行为交给浏览器原生实现。
这种思路的关键是不能将容器的overflow设为hidden,这样虽然隐藏了滚动条,但也禁止了滚动行为。所以开发者尝试将滚动条遮盖起来,一般通过多个div的嵌套和偏移(偏移量恰好是滚动条的宽度)来实现。遮盖后再将模拟的滚动条固定在容器右侧和底部。之后的关键点就是计算模拟滚动条的宽高与位置,并且监听容器的scroll事件,及时更新滚动条的状态,如果用户拖动滚动条,则此时不能依靠原生滚动行为,需要自己计算实际滚动距离去更新容器的scrollLeft及scrollTop。参考simplebar和react-custom-scrollbars。
该方案有很多优点,首先你可以完全自定义滚动条的样式而不用考虑兼容性问题,其次它的性价比非常高,绝大多数时间,你使用的是浏览器默认的行为(他们性能优秀而且覆盖了边际情况),只有在用户拖动滚动条时,才需要手动计算并更新容器的滚动距离。不过该方案也并非完美无缺,最大的问题是你需要添加多层div才能覆遮盖住原生滚动条,这在一定程度上破坏了开发者预先设想的文档结构。
3、自行实现滚动行为与滚动条样式。
该方案比较复杂,因为滚动行为通常由三个条件触发,分别是鼠标滚轮(或触控板)滑动、键盘导航、鼠标拖动(选择文字时),你得同时监听这三种事件,同时要考虑兼容问题,因为这三种事件在各个浏览器不统一。滚动条部分与方案2相同,这里不再赘述。 虽然这个方案不好搞,但正因为完全自定义,你得以写出更丰富的滚动逻辑,比如整屏滚动或者增加颜色特效。该方案在社区最为流行。
二、js实现思路(pc端)
这里会详细阐述方案3的实现思路。让我们从零开始,现在有一个容器,他的子元素高度超过了容器的高度,需要给他添加一个纵向的滚动条,从交互角度出发,可以分解成以下步骤。
1、监听容器的mousewheel事件。
通过鼠标滚轮或者触控板的滑动,浏览器会生成mousewheel事件,事件中带有滚动偏移量,我们要利用该数值来修改容器的scrollTop以达到滚动效果。这里的问题是mousewheel不是一个标准事件,各个浏览器携带不一样的事件信息,滚动偏移量也不同,所以我们需要抹平他们的差异。一个好的办法是将滚动偏移量统一设为1。
const userAgent = window.navigator.userAgent;
let isSafari = (userAgent.indexOf('Chrome') === -1) && (userAgent.indexOf('Safari') >= 0);
function standardizedWheel(e) {
let wheelEvent = Object.assign({}, e);
// vertical
if (typeof e.wheelDeltaY !== 'undefined') {
// webkit
wheelEvent.deltaY = e.wheelDeltaY / 120;
} else if (typeof e.VERTICAL_AXIS !== 'undefined' && e.axis === e.VERTICAL_AXIS) {
// Firefox < 17
wheelEvent.deltaY = -e.detail / 3;
}
// horizental
if (typeof e.wheelDeltaX !== 'undefined') {
// webkit
if (isSafari) {
wheelEvent.deltaX = - (e.wheelDeltaX / 120);
} else {
wheelEvent.deltaX = e.wheelDeltaX / 120;
}
} else if (typeof e.HORIZONTAL_AXIS !== 'undefined' && e2.axis === e2.HORIZONTAL_AXIS) {
// Firefox < 17
wheelEvent.deltaX = -e.detail / 3;
}
if (wheelEvent.deltaY === 0 && wheelEvent.deltaX === 0 && e.wheelDelta) {
// IE
wheelEvent.deltaY = e.wheelDelta / 120;
}
return wheelEvent;
}
通常来讲,向下滚动偏移量为-1,反之为1。
之后我们设定一个滚动系数(scrollFactor),它可以是字的行高,也可以是任意数值,这取决于你希望在一次滚动中经过的像素是多少。然后用它乘以偏移量,作为最终的滚动偏移量。
let containerDom;
const scrollFactor = 50;
containerDom.addEventListener('mousewheel', (e) => {
let wheelEvent = standardizedWheel(e);
let scrollTop = containerDom.scrollTop - e.deltaY * scrollFactor;
containerDom.scrollTop = scrollTop;
})
2、校准滑块的大小与位置
在纵向的滚动条中,滑块的高度如何计算呢?如果把内容与滚动条分成两个区域,那么他们的可见区域和可滚动区域的比是相等的。
// 根据 visibleHeight / scrollHeight = sliderHeight / scrollbarHeight 得出
sliderHeight = visbileHeight * scrollbarHeight / scrollHeight
接下来解决滑块的位置,在先前的方法中,我们已经知道了scrollTop,只需要让它乘以两个区域的可滚动高度比即可。
// 滑块区域与内容区域的比例
sliderRatio = (scrollHeight - visibleHeight) / (scrollbarHeight - sliderHeight)
sliderTop = scrollTop * sliderRatio
我们将滑块状态的计算方式写成一个函数,随着页面滚动scrollTop始终在变化,需要不停地调用它来更新滑块状态。另外你要处理好边界情况,判断滚动行为是否到了可滚动区域的上限,不要让滚动无休止的下去。
let scrollbar /* 滚动条元素 */
let sliderDom /* 滑块元素 */
function updateSlider(scrollTop) {
sliderHeight = containerDom.clientHeight * scrollbar.clientHeight / containerDom.scrollHeight;
sliderRatio = (scrollbar.clientHeight - sliderDom.clientHeight) / (containerDom.scrollHeight - containerDom.clientHeight);
sliderTop = scrollTop * sliderRatio;
// 更新滑块的高度和位置
sliderDom.style.height = sliderHeight + 'px';
sliderDom.style.top = sliderTop + 'px';
}
3、滑块拖拽
本质上我们可以把滑块的状态作为容器的衍生状态来看待,所以只要有容器的scrollTop,滑块的位置就能确定。现在我们使用兼容性更好的mouse事件。当鼠标点击滑块时,触发mousedown,记录下当时滑块的位置(pageY),随后开始mousemove的监听,在鼠标移动的过程中,我们使用新的pageY减去初始pageY,作为该次滚动的差值moveDelta,得出滑块滚动的位置 sliderTop = lastedSliderTop + moveDelta
。还记得我们之前提到的公式吗,稍微改下就得出scrollTop = sliderTop / sliderRatio
。之后根据scrollTop校准滑块的位置即可。
sliderDom.addEventListener('mousedown', (e) => {
let lastedPageY = e.pageY;
let lastedScrollTop = containerDom.scrollTop * sliderRatio;
let scrollTop;
document.addEventListener('mousemove', (e) => {
let moveDelta = e.pageY - lastedPageY;
let sliderTop = lastedScrollTop + moveDelta;
scrollTop = sliderTop / sliderRatio;
containerDom.scollTop = scrollTop;
updateSlider(scrollTop);
});
})
4、点击滚动轨道的特定位置
我们的算法不变,假设用户在轨道上随机一个位置点击,我们只需得出该位置相对于滚动条的偏移量即可。在现代浏览器中,mousedown事件会直接返回给你offsetY,假如没有就需要简单算下。我们需要使用pageY,注意这个属性是包含文档的滚动距离的。
// 事实上pageY与scrollY在那些陈旧的浏览器也不支持,你可以参考mdn给出的兼容方案
offsetY = e.pageY - scrollbar.getBoundingClientRect().top - window.scrollY;
offsetY减去滑块的高度就是这次滚动的末端位置,不过浏览器通常会定位到滑块的中心点,我们也遵守这个原则,只需要除以2即可。现在我们算出了滑块的位置,将它除以sliderRatio,就得出 scrollTop = (offsetY – sliderHeight / 2) / sliderRatio。
scollbar.addEventListener('mousedown', (e) => {
if (e.target !== sliderDom) {
let offsetY = e.pageY - scrollbar.getBoundingClientRect().top - window.scrollY;
scrollTop = (offsetY - sliderDom.clientHeight / 2) / sliderRatio;
containerDom.scrollTop = scrollTop;
updateSlider(scrollTop);
}
})
5、平滑滚动
如果一次滚动直愣愣的到达终点,是不是很生硬?我们让他看起来更丝滑一些,这也是原生滚动具有的效果。因为scrollTop属性无法用css做动画,所以只能用js实现。我们希望滚动在一开始很快,随着时间推移在快到终点时变慢,所以定义一个缓动函数
// 参数t是时间进度
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
比如滚动开始时间是0,你希望在3s内滚动到终点,那么他在前2s行动的很快,在最后一秒又降速变慢,当然过程是很平缓的。我们的目标是根据当前的时间进度,得出滚动距离。
function scrollToSmooth(from/* 起点 */, to/* 终点 */, duration/* 持续时间 */) {
let startTime = Date.now();
let delta = to - from;
function tick(now) {
// 计算完成度
let completion = (now - startTime) / duration;
if (completion < 1) {
// 小于1表示没有完成滚动
let newScrollTop = from + delta * easeOutCubic(completion);
return {
scrollTop: newScrollTop,
done: false
}
}
return {
scrollTop: to,
done: true
}
}
function performScrolling() {
let update = tick(Date.now());
if (update.done) {
return
}
// 用新的距离更新容器的
containerDom.scrollTop = update.newScrollTop;
requestAnimationFrame(performScrolling());
}
//为了获得性能提升,这里用requestAnimationFrame执行它。
requestAnimationFrame(performScrolling());
}
6、滚动条隐藏与显示
mouseover时,处理好滚动条的display即可。
7、键盘导航
容器要响应上下左右,PageUp,PageDown, Home, End按键,每个按键有不同的滚动offset。难点在键盘事件的兼容性上,参考KeyboardEvent和快捷键
8、内容选中滚动
这种场景在鼠标选中容器子元素的并且越过容器边界的时候发生。需要容器监听mousedown,在mousemove的过程中检查鼠标坐标是否越过容器边界,再根据鼠标停留时间做若干次偏移。
三、还需要做什么
做完了上面这些,你已经搞出一个可用的滚动条了,但这只是为了讲述思路的玩具代码,并不能用在真实环境。在实际开发中,你可能需要使用面向对象设计来组织你的代码,并处理好所有事件的回收,此外,你还要小心处理以下问题。
1、容器的resize。你要重新计算滚动条的所有状态,以保证显示正确。
2、小心iframe。在鼠标事件经过iframe时会产生各种匪夷所思的问题,如果你不幸使用了它,最简单的办法是跨过iframe时销毁监听函数,保证内存不泄露。
3、别忘了横向滚动条。
4、别忘了手机与平板环境下的滚动。
今天的文章自定义滚动条的实现思路与关键算法分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/15772.html