全面讓你瞭解和打造本身的自定義滾動條(提供組件

前言

最近在封裝一個自定義滾動條容器,打算之後用它來取代經常使用的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,就能用一個自定義標籤來表示能展現自定義滾動條的容器了。

微信公衆號

==未經容許,請勿私自轉載==

相關文章
相關標籤/搜索