Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

实现一个自定义滚动条 #11

Open
linzx1993 opened this issue Feb 27, 2018 · 5 comments
Open

实现一个自定义滚动条 #11

linzx1993 opened this issue Feb 27, 2018 · 5 comments

Comments

@linzx1993
Copy link
Owner

linzx1993 commented Feb 27, 2018

因为最近在写项目的UI库,遇到自定义滚动条这一个槛还是卡了我挺久的,主要卡在了如何自动监听内容变化并更新滚动条高度。市面上基本所有的滚动条插件都没有实现这一点,最后面扒了element的源码才最终解决。本文主要讲的也是这个。

首先,我们先把需要实现的功能先确定下来。

  • 鼠标左键点击可以拖动
  • 鼠标滑轮滚动
  • 内容发生变化,自动更新滚动条长度
  • 提供开发者一个滚动回调的接口

前面两点依靠原生滚动条其实比较简单,但是在第三点上实在是卡了我好久,想了好久都没有想出来。最后还是看了element源码才实现成功。

接下去我会以垂直滚动条为例(水平滚动条基本同理),实现一个自定义的滚动条出来。我争取把其中原理细节讲清楚。

1、搭建好基本的样式框架

开始我们先把HTML和样式写好

<div class="scrollbar">
	<div class="scrollbar-content">
		<ul class="box">
			<li>11111</li><li>11111</li><li>11111</li><li>11111</li>
			<li>11111</li><li>11111</li><li>11111</li><li>11111</li>
			<li>11111</li><li>11111</li><li>11111</li><li>11111</li>
			<li>11111</li><li>11111</li><li>11111</li><li>11111</li>
		</ul>
	</div>
	<div class='scrollbar-bar'>
		<div ref="thumb" class="scrollbar-thumb"></div>
	</div>
</div>

第一步的HTML和CSS

滚动条的框架如上面所示,接下午我会以简称wrapbar,thumb进行简称

  • wrap :内容区域包裹框
  • bar : 包裹区域中自定义滚动条的滚动框
  • thumb :自定义滚动条

开始之前要大家可以先记住一点,我们并不是不用原生滚动条,实际上我们所有的操作都需要依靠原生滚动条才能实现。只不过它隐藏在了暗处,而让UI更好看的自定义滚动条出现在明处。

1.1计算出滚动条的宽度。

第一步我们先将原生的滚动条隐藏掉。但是这里涉及到第一个问题,那就是不同浏览器的下滚动条宽度是不一样的。我们需要准确的知道,如果wrap产生了滚动条,那它的宽度是多少。

先写一个获取到区域内滚动条的宽度(scrollWidth)的回调函数getScrollWidth,获取到滚动条高度之后,

function getScrollWidth(){
    const outer = document.createElement("div");
    outer.className = "el-scrollbar__wrap";
    outer.style.width = '100px';
    outer.style.visibility = "hidden";
    outer.style.position = "absolute";
    outer.style.top = "-9999px";
    document.body.appendChild(outer);

    const widthNoScroll = outer.offsetWidth;
    outer.style.overflow = "scroll";

    const inner = document.createElement("div");
    inner.style.width = "100%";
    outer.appendChild(inner);

    const widthWithScroll = inner.offsetWidth;
    outer.parentNode.removeChild(outer);
    scrollBarWidth = widthNoScroll - widthWithScroll;

    return scrollBarWidth;
}

获取到滚动条的宽度scrollBarWidth之后,通过再来设置wrap的css样式,通过marginRight将滚动条移动到视线之外

wrap.style.overflow = scroll;
wrap.style.marginRight = -scrollWidth + "px";
1.2计算出滚动条的高度。

第二步我们需要计算出滚动条的高度。计算方法也很简单,元素高度scrollHieght/内容高度clientHeight,得出来的就是滚动条所占的百分比。

因为内容高度经常变更,我们可以写一个更新滚动条高度的回调函数updateThumb,方便后期s随时调用。

function updateThumb(){
    let heightPercentage = (wrap.clientHeight * 100 / wrap.scrollHeight);
    thumb.style.height = heightPercentage + "%";   
}

到了这一步,基本上一个滚动条的基本样式已经出来了。接下去我们要实现它的使用功能。

查看第一步的成果

2、添加滚动条滑动功能

到这里我们已经可以看到成型的滚动条的UI界面了,但是仍然缺少滚动和拖动的功能。关键点是在于如何去监听滚动条的变化。

2.1滚轮滑动

还记得文章开头说过,我们所有功能的实现都依赖隐藏起来的原生滚动条。如果大家理解了我上面说的话,那么问题就简单了。当我们开始滑动滚轮的时候,隐藏在暗处的原生滚动条也会同时滚动,此时便会触发原生滚动条的scroll事件

这里可以再详细说明下。只要元素的scrollTop发生变化,就必然会触发scroll事件。所以我们操作滚轮,其实本质上是改变元素的scrollTop。

所以我们只需要写一个相应的回调函数handleScroll,在每次触发回调的时候,实时修改我们自定义滚动条的样式就行了。

function handleScroll(){
    this.moveY = (wrap.scrollTop *100 / wrap.clientHeight);
    //通过计算出来的百分比,然后对滚动条执行translate移动
    thumb.style.transform = "translateY" + moveY;
},

wrap.addEventListener('scroll',handleScroll);

查看滚轮滑动效果

2.2点击滚动框,滚动条及内容移动到相应位置

接下去我们实现第二个功能。当我们点击滚动框的一个位置时,滚动条也会跳到这个位置,同时内容位置也会发生改变。

第一步先获得点击的y坐标,然后计算出和滚动框bar顶部的距离,再算出占滚动框的百分比,这个百分比就是滚动条的高度

function clickTrackHandle(e){
    //获得点击位置与滚动框顶部之间的距离
    const offset = Math.abs(e.target.getBoundingClientRect().top - e.clientY)
    //让点击位置处于滚动条的中间
    const thumbHalf = thumb.offsetHeight / 2;
    //计算出滚动条在滚动框的百分比位置
    const thumbPositionPercentage = (offset - thumbHalf) * 100 / wrap.offsetHeight;
    //通过改变scrollTop来操作。所有操作滚动条的最后一步都是通过handleScroll来实现
    wrap.scrollTop = (thumbPositionPercentage * wrap.scrollHeight / 100);
}

bar.addEventListener("click",clickTrackHandle);

只要scrollTop值发生变化就会触发我们上一步写的回调。

查看点击滚动框的效果

2.3拖动滚动条,移动内容

接下来我们再去实现手动拖拽滚动条去实现移动内容,这个知识点就是拖拽的知识点,不过在看源码的时候发现element的习惯很好,他是在当你点击滚动条的时候绑定拖拽,然后松开的时候取消绑定。

function mouseMoveDocumentHandler(){};   //实时记录滚动条位置的拖拽函数

//当点击滚动条时
document.addEventListener("mousedown",mouseMoveDocumentHandler);
document.onselectstart = false; //同时阻止选中
//当松开滚动条时
document.removeEventListener("mousedown",mouseMoveDocumentHandler);
document.onselectstart = null; //同时阻止选中

因为这一块代码比较多,就不贴文章里,大家可以直接链接里看就是了。
查看拖动滚动条的效果

3、实现滚动条随内容实时更新

第二章讲的主要都是实现滚动条功能,这一章讲的是纠结😖我很久的功能。

因为滚动条的高度并不是我们一开始能够确定的,它需要在dom内容渲染出来之后才能确定。而且有时候随着内容的变化,还需要实时改变滚动条的高度。再看了市面上的滚动条之后,发现基本都没有满足这一功能。

事实上缺少了这一点,使用起来是缺少视觉交互的。举个例子,加入一个原来有滚动条的元素因为内容减少导致了滚动条小时,但是自定义滚动条因为没有检测到变化仍然存在,那就会给用户造成困扰。

我不希望每次更新内容都要通过加一步回调函数来更新一下滚动条,而是希望它自己实时更新。在网上没有找到答案之后,最终去翻了element源码,研究了好久,总算找到了想要的答案。

关键点就在于我能前面之前说的那一句话——如果我们改变元素的scrollTop,是会触发scroll事件。

大家想象一个情景,如果滚动条永远出现在最底部,比如下图
image

那么只要我内容发生了一点变化,滚动条必然会变长或者变短。那么在滚动条长度变化时,scrollTop自然发生了改变(滚动条消失则scrollTop变为0),那么就会触发scroll的回调函数,那么我们就自动监测到了啊😊。

在明白了这一点后,却又冒出来一个问题。正常情况下,滚动条不可能出现在最底部啊,那怎么办呢?

element选择了自己造一个置于底部的滚动条来满足自己需求。

做了个demo,查看效果点这里

<script>
    const ul = document.getElementById("ul");
    const resizeTrigger = document.createElement("div");
        resizeTrigger.className = "resize-triggers";
        resizeTrigger.innerHTML = '<div class="expand-trigger"><div><div></div></div></div>';
        ul.appendChild(resizeTrigger);
    	
    const resetTrigger = function (element) {
        const trigger = element.__resizeTrigger__;
        const expand = trigger.firstElementChild;
        const expandChild = expand.firstElementChild;
        expandChild.style.height = expand.offsetHeight + 1 + 'px';
        expand.scrollTop = expand.scrollHeight;
    };
    
    ul.addEventListener("scroll",function(){
        resetTrigger(this);
    },true)
</script>

ul是我们包裹内容的DOM元素。

配合着css来看,第一段JS我们创建出了resizeTrigger这个div,并且我们将他的height:100%。这样子如果内容发生变化,resizeTrigger永远和父元素ul同时改变高度。这里设置成高度100%非常重要,这样子才能主动同步到内容的变化

注意到resizeTrigger里面还有有一个父子元素expandexpandChild。在第二段JS的resetTrigger函数中。然后设置expandChild的高度超过父元素expand的高度,促使expand产生滚动条。然后我们再将滚动条的scrollTop设置为最大,这样子滚动条就会出现在滚动区域resizeTrigger的最底部了。

现在我们做到了将滚动条设置在了最底部,所以只要内容发生了变化,那么滚动条的scrollTop必然也会发生变化

最后一段代码就是scroll的监听。当监听到scrollTop值发生变化时,触发相应的回调函数。

所以这块代码最后的逻辑其实是这样的。内容改变 --> ul高度改变--> resizeTrigger高度改变 --> expand滚动条的scrollTop发生变化 --> 触发scroll的回调函数,在函数里面调整再次调整滚动条的高度,保证滚动条高度正确。

通过这三段代码,我们也基本实现了自动监听内容变化来更新滚动条。

简单画了个配图来帮助理解逻辑
image

image

通过两个小蓝框产生的滚动条来帮助监听内容变化

4、实现组件化,方便开发者使用

经过以上3大步基本上是可以实现一个自定义的滚动条的。上面的代码是面向原生js的。在我们的项目里面,实现第4点是通过封装成一个scrollbar的的组件,在项目里面进行使用。

这一条要求因为不同框架实现方式都不一样,所以就不详细贴代码了,不过最终原理肯定还是一致的。因为自己项目用的是一个Vue框架,所以是个Vue组件,有需求可以自己去看。

没有写过写Vue组件的可以看看这一篇,少踩些坑

查看scrollbar组件


好了文章就到此结束了,在看人家源码的过程中也学到了许多。比如使用JSX来编写组件;scroll监听其实就是判断scrollTop;比如通过自己造滚动条的方法监听scrollTop来实现自动更新。最后通过写文章,对一些新的知识点理解还是加深了许多。

@cobish
Copy link

cobish commented Apr 10, 2018

“查看拖动滚动条的效果”,该链接跳错了~

tim 20180410094246

@linzx1993
Copy link
Owner Author

@cobish 呀,sorry,改正了

@basala
Copy link

basala commented Sep 13, 2018

最后 查看scrollbar组件的链接失效了~

@AnneDong
Copy link

你好,
查看scrollbar组件的链接失效了~
有完整版的vue组件源码吗

@wodeMAX
Copy link

wodeMAX commented Aug 4, 2021

你好,
查看scrollbar组件的链接失效了~
有完整版的vue组件源码吗

https://github.com/linzx1993/aps-ui/tree/master/src/components/scrollbar

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants