您好,登錄后才能下訂單哦!
前言
記得以前偶然有一次瀏覽過(guò)一個(gè)開(kāi)源的cms項(xiàng)目,發(fā)現(xiàn)這個(gè)項(xiàng)目的左邊的菜單已經(jīng)超出了windows的寬度,我就好奇為什么沒(méi)出滾動(dòng)條呢?然后我仔細(xì)一看,發(fā)現(xiàn)它左側(cè)有一個(gè)小的div,然后我嘗試著拖動(dòng)它,發(fā)現(xiàn)竟能和原生的滾動(dòng)條一樣!可以通過(guò)查看它的源碼,發(fā)現(xiàn)了這款滾動(dòng)條的叫做slimScroll,然后我去它的github倉(cāng)庫(kù) 看了下,研究了一下源碼,給我的感覺(jué)是我也能做出來(lái)一樣的滾動(dòng)條!通過(guò)vue實(shí)現(xiàn)!
設(shè)計(jì)
好, 現(xiàn)在開(kāi)始我們的設(shè)計(jì)滾動(dòng)條的步驟:
設(shè)計(jì)滾動(dòng)條dom
首先要思考的是: 如果要使你需要滾動(dòng)的內(nèi)容滾動(dòng)的話,首先一點(diǎn)是它的父dom必須為固定長(zhǎng)寬,即超出部分要隱藏掉,即加了個(gè)樣式: overflow: hidden , 所以,我們給所要滾動(dòng)的內(nèi)容加個(gè)包裝,使它的長(zhǎng)寬和父dom相等,然后有一個(gè)樣式叫: overflow: hidden ,這個(gè)包裝的元素就叫 scrollPanel
其次:我們知道,我們要做到與原生滾動(dòng)條一樣強(qiáng)大!就必須設(shè)計(jì)水平滾動(dòng)條和垂直滾動(dòng)條,滾動(dòng)條和scrollPanel屬于兄弟節(jié)點(diǎn)之間的關(guān)系,因?yàn)闈L動(dòng)條的存在不能使原本的樣式排版錯(cuò)誤,并且支持top、left來(lái)控制其位置,所以滾動(dòng)條的position必須是absolute,好了,我們叫水平滾動(dòng)條為:hBar,垂直滾動(dòng)條為:vBar
最后:我們?cè)O(shè)計(jì)了scrollPanel、vBar、hBar, 我們需要一個(gè)父div來(lái)把他們包裝起來(lái),然后加個(gè)樣式:position: relative
實(shí)踐
設(shè)計(jì)組件結(jié)構(gòu)
首先,我們的插件一共是4個(gè)組件,其中3個(gè)是子組件,1個(gè)是父組件,分別是: vueScroll (父組件)、 scrollPanel (包裹需要滾動(dòng)內(nèi)容的子組件)、 vBar (垂直滾動(dòng)條)、 hBar (水平滾動(dòng)條)
其次,讓我們?cè)O(shè)計(jì)一下各組件所分管的功能。這里的組件分為控制層組件和展示組件(熟悉react的同學(xué)應(yīng)該有所了解),展示層組件只完成展示的功能: vBar 、 hBar 、 scrollPanel ,控制層組件有點(diǎn)類似于cpu,可以控制子組件的各個(gè)狀態(tài),比如寬、高、顏色、透明度、位置等等??刂茖咏M件就是: vueScroll 。
具體實(shí)現(xiàn)
hBar/vBar
hBar/vBar 這兩個(gè)分別為水平滾動(dòng)條和垂直滾動(dòng)條,所實(shí)現(xiàn)的功能大體是一樣的,所以舊放在一起說(shuō)了,這里以 vBar 為例。
props 接收父組件傳過(guò)來(lái)的屬性,具體為:
{ height: vm.state.height + 'px', //滾動(dòng)條的高度 width: vm.ops.width, // 滾動(dòng)條的寬度 position: 'absolute', background: vm.ops.background, // 滾動(dòng)條背景色 top: vm.state.top + 'px', // 滾動(dòng)條的高度 transition: 'opacity .5s', // 消失/顯示 所用的時(shí)間 cursor: 'pointer', // opacity: vm.state.opacity, // 透明度 userSelect: 'none' }
2 事件,主要是當(dāng)鼠標(biāo)移動(dòng)的時(shí)候,顯示滾動(dòng)條。
... render(_c){ return _c( // ... { mouseenter: function(e) { vm.$emit('showVBar'); // 觸發(fā)父組件事件,顯示滾動(dòng)條 } } // ... ) }
其中 state 表示狀態(tài),是在運(yùn)行時(shí)可發(fā)生改變的,而 ops 則是配置參數(shù),是用戶傳過(guò)來(lái)的。
scrollPanel
包裹滾動(dòng)內(nèi)容的組件,樣式需設(shè)置為: overflow: hidden 。
1、樣式
var style = vm.scrollContentStyle; style.overflow = 'hidden'; // ... { style: style } // ...
2、事件
// ... render(_c) { // ... on: { mouseenter: function() { vm.$emit('showBar'); }, mouseleave: function() { vm.$emit('hideBar'); } } // ... } // ...
vuescroll
控制組件。控制子組件顯示的狀態(tài),添加各種監(jiān)聽(tīng)事件等。
1、取得子組件的dom元素,用來(lái)取得dom的實(shí)時(shí)信息。
// ... initEl() { this.scrollPanel.el = this.$refs['vueScrollPanel'] && this.$refs['vueScrollPanel'].$el; this.vScrollBar.el = this.$refs['vScrollBar'] && this.$refs['vScrollBar'].$el; this.hScrollBar.el = this.$refs['hScrollBar'] && this.$refs['hScrollBar'].$el; } // ...
2、顯示滾動(dòng)條
顯示滾動(dòng)條,包括顯示水平滾動(dòng)條和顯示垂直滾動(dòng)條,這里以顯示垂直滾動(dòng)條為例:
// ... var temp; var deltaY = { deltaY: this.vScrollBar.ops.deltaY // 獲取用戶配置的deltaY }; if(!this.isMouseLeavePanel || this.vScrollBar.ops.keepShow){ if ((this.vScrollBar.state.height = temp = this.getVBarHeight(deltaY))) { // 判斷條件 // 重新設(shè)置滾動(dòng)條的狀態(tài) this.vScrollBar.state.top = this.resizeVBarTop(temp); this.vScrollBar.state.height = temp.height; this.vScrollBar.state.opacity = this.vScrollBar.ops.opacity; } } // ...
3、獲取滾動(dòng)條的高度
因?yàn)閐om元素的高度不是固定的,所以你要實(shí)時(shí)地獲取dom真實(shí)的高度,滾動(dòng)條的高度計(jì)算公式如下:
var height = Math.max( scrollPanelHeight / (scrollPanelScrollHeight / scrollPanelHeight), this.vScrollBar.minBarHeight );
即: 滾動(dòng)條的高度:scrollPanel的高度 == scrollPanel的高度:dom元素高度
4、resizeVBarTop ,為了防止誤差,并且可以求出滾動(dòng)條距離父元素的高度。
resizeVBarTop({height, scrollPanelHeight, scrollPanelScrollHeight, deltaY}) { // cacl the last height first var lastHeight = scrollPanelScrollHeight - scrollPanelHeight - this.scrollPanel.el.scrollTop; if(lastHeight < this.accuracy) { lastHeight = 0; } var time = Math.abs(Math.ceil(lastHeight / deltaY)); var top = scrollPanelHeight - (height + (time * this.vScrollBar.innerDeltaY)); return top; }
5、監(jiān)聽(tīng)滾輪滾動(dòng)的事件。
// ... on: { wheel: vm.wheel } // ... wheel(e) { var vm = this; vm.showVBar(); vm.scrollVBar(e.deltaY > 0 ? 1 : -1, 1); e.stopPropagation(); } // ...
6、監(jiān)聽(tīng)滾動(dòng)條拖拽事件
listenVBarDrag: function() { var vm = this; var y; var _y; function move(e) { _y = e.pageY; var _delta = _y - y; vm.scrollVBar(_delta > 0 ? 1 : -1, Math.abs(_delta / vm.vScrollBar.innerDeltaY)); y = _y; } function t(e) { var deltaY = { deltaY: vm.vScrollBar.ops.deltaY }; if(!vm.getVBarHeight(deltaY)) { return; } vm.mousedown = true; y = e.pageY; // 記錄初始的Y的位置 vm.showVBar(); document.addEventListener('mousemove', move); document.addEventListener('mouseup', function(e) { vm.mousedown = false; vm.hideVBar(); document.removeEventListener('mousemove', move); }); } this.listeners.push({ dom: vm.vScrollBar.el, event: t, type: "mousedown" }); vm.vScrollBar.el.addEventListener('mousedown', t); // 把事件放到數(shù)組里面,等銷毀之前移除掉注冊(cè)的時(shí)間。 }
7、適配移動(dòng)端,監(jiān)聽(tīng) touch 事件。原理跟拖拽事件差不多,無(wú)非就是多了個(gè)判斷,來(lái)判斷當(dāng)前方向是x還是y。
listenPanelTouch: function() { var vm = this; var pannel = this.scrollPanel.el; var x, y; var _x, _y; function move(e) { if(e.touches.length) { var touch = e.touches[0]; _x = touch.pageX; _y = touch.pageY; var _delta = void 0; var _deltaX = _x - x; var _deltaY = _y - y; if(Math.abs(_deltaX) > Math.abs(_deltaY)) { _delta = _deltaX; vm.scrollHBar(_delta > 0 ? -1 : 1, Math.abs(_delta / vm.hScrollBar.innerDeltaX)); } else if(Math.abs(_deltaX) < Math.abs(_deltaY)){ _delta = _deltaY; vm.scrollVBar(_delta > 0 ? -1 : 1, Math.abs(_delta / vm.vScrollBar.innerDeltaY)); } x = _x; y = _y; } } function t(e) { var deltaY = { deltaY: vm.vScrollBar.ops.deltaY }; var deltaX = { deltaX: vm.hScrollBar.ops.deltaX }; if(!vm.getHBarWidth(deltaX) && !vm.getVBarHeight(deltaY)) { return; } if(e.touches.length) { e.stopPropagation(); var touch = e.touches[0]; vm.mousedown = true; x = touch.pageX; y = touch.pageY; vm.showBar(); pannel.addEventListener('touchmove', move); pannel.addEventListener('touchend', function(e) { vm.mousedown = false; vm.hideBar(); pannel.removeEventListener('touchmove', move); }); } } pannel.addEventListener('touchstart', t); this.listeners.push({ dom: pannel, event: t, type: "touchstart" }); }
8、滾動(dòng)內(nèi)容
滾動(dòng)內(nèi)容的原理無(wú)非就是改變 scrollPanel 的 scrollTop/scrollLeft 來(lái)達(dá)到控制內(nèi)容上下左右移動(dòng)的目的。
scrollVBar: function(pos, time) { // >0 scroll to down <0 scroll to up var top = this.vScrollBar.state.top; var scrollPanelHeight = getComputed(this.scrollPanel.el, 'height').replace('px', ""); var scrollPanelScrollHeight = this.scrollPanel.el.scrollHeight; var scrollPanelScrollTop = this.scrollPanel.el.scrollTop; var height = this.vScrollBar.state.height; var innerdeltaY = this.vScrollBar.innerDeltaY; var deltaY = this.vScrollBar.ops.deltaY; if (!((pos < 0 && top <= 0) || (scrollPanelHeight <= top + height && pos > 0) || (Math.abs(scrollPanelScrollHeight - scrollPanelHeight) < this.accuracy))) { var Top = top + pos * innerdeltaY * time; var ScrollTop = scrollPanelScrollTop + pos * deltaY * time; if (pos < 0) { // scroll ip this.vScrollBar.state.top = Math.max(0, Top); this.scrollPanel.el.scrollTop = Math.max(0, ScrollTop); } else if (pos > 0) { // scroll down this.vScrollBar.state.top = Math.min(scrollPanelHeight - height, Top); this.scrollPanel.el.scrollTop = Math.min(scrollPanelScrollHeight - scrollPanelHeight, ScrollTop); } } // 這些是傳遞給父組件的監(jiān)聽(tīng)滾動(dòng)的函數(shù)的。 var content = {}; var bar = {}; var process = ""; content.residual = (scrollPanelScrollHeight - scrollPanelScrollTop - scrollPanelHeight); content.scrolled = scrollPanelScrollTop; bar.scrolled = this.vScrollBar.state.top; bar.residual = (scrollPanelHeight - this.vScrollBar.state.top - this.vScrollBar.state.height); bar.height = this.vScrollBar.state.height; process = bar.scrolled/(scrollPanelHeight - bar.height); bar.name = "vBar"; content.name = "content"; this.$emit('vscroll', bar, content, process); },
9、銷毀注冊(cè)的事件。
剛才我們已經(jīng)把注冊(cè)事件放到listeners數(shù)組里面了,我們可以在beforedestroy鉤子里將他們進(jìn)行銷毀。
// remove the registryed event. this.listeners.forEach(function(item) { item.dom.removeEventListener(item.event, item.type); });
運(yùn)行截圖
PC端運(yùn)行截圖如下圖所示:
注冊(cè)監(jiān)聽(tīng)事件以后如下圖所示:
在手機(jī)上運(yùn)行截圖:
可以看出,跟原生滾動(dòng)條表現(xiàn)效果一致。
結(jié)語(yǔ)&感悟
以上就基本把我設(shè)計(jì)的滾動(dòng)條設(shè)計(jì)完了,首先很感激掘金給了我這么一個(gè)分享平臺(tái),然后感謝slimScroll的作者給了我這么一個(gè)思路。做完這個(gè)插件, 我對(duì)dom元素的scrollWidth、scrollHeigh、scrollTop、scrollLeft的了解更多了,最后,附上github項(xiàng)目地址
以上部分就是這個(gè)組件的核心源碼了。希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。