本文介紹一個簡單的DrawerLayout(相似Android的DrawerLayout)佈局組件的實現,基於 Vue.js。介紹的內容已經制做成 vue-drawer-layout 組件。
你們有興趣先用手機掃一掃這個二維碼,或者點我css
而後點擊頁面中左上角的頭像打開drawer或者向右向左拖拽,就能夠看到下面gif的效果,打開本身的手機QQ,是否是很像:)html
谷歌官方把這種佈局叫作DrawerLayout(抽屜式導航欄)。那麼咱們要如何實現呢,好了正片開始!vue
頁面結構很簡單,一個抽屜,一個主容器,內容能夠利用slot支持外部自行定製。css3
<div class="drawer-layout"> <!--抽屜--> <div class="drawer-wrap"> <slot name="drawer"></slot> </div> <!--主容器--> <div class="content-wrap"> <!--遮罩--> <div class="drawer-mask"></div> <slot name="content"></slot> </div> </div>
抽屜一開始是隱藏在左側屏幕外的,故設置left:-100%
使其整個都藏在外部git
首先,判斷瀏覽器是否支持touchEvent
github
let isTouch = 'ontouchstart' in window; let mouseEvents = isTouch ? { down: 'touchstart', move: 'touchmove', up: 'touchend', over: 'touchstart', out: 'touchend' } : { down: 'mousedown', move: 'mousemove', up: 'mouseup', over: 'mouseover', out: 'mouseout' };
綁定touchdown
事件web
document.addEventListener(mouseEvents.down, initDrag, false);
先定義一些變量,手指按下的x座標記爲startX
,滑動中手指的位置x座標記爲nowX
,drawer的x座標偏移量記爲startPos
npm
let startX, nowX, startPos;
觸發touchstart
時,記錄起始位置並綁定touchmove
,注意:若是是mouseEvent
,經過e.clientX
來獲取當前的x座標,若是是touchEvent
,要經過e.changedTouches[0].clientX
來獲取x座標瀏覽器
const initDrag = function (e) { startX = e.clientX || e.changedTouches[0].clientX; //記錄手指按下的位置 startPos = this.pos; //記錄drawer的上次位置 document.addEventListener(mouseEvents.move, drag, false); document.addEventListener(mouseEvents.up, removeDrag, false); }.bind(this);
const drag = function (e) { nowX = e.clientX || e.changedTouches[0].clientX; //滑動中手指的位置x座標 let pos = startPos + nowX - startX; pos = Math.min(width, pos); //不能超過滑動最大值 pos = Math.max(0, pos); //不能小於0 this.pos = pos; //設置滾動距離爲拖動的距離 }.bind(this);
那麼,手指滑動的距離就是nowX - startX
,當前drawer的位置爲startPos + nowX - startX
,這樣抽屜已經跟隨手指向右移動了,而且不會超過咱們設置的拖動最大值。函數
接下來你會發現一個問題,當手指垂直滾動主內容時,向右滑動手指也會拖出抽屜,這時應該作一件事:區分垂直滑動和水平滑動
固然,辦法有不少,這裏先介紹一種利用三角函數來斷定的方法
假設,上圖中的每一個箭頭是手指滑動的方向,綠色箭頭表明能夠拖出抽屜,紅色箭頭表明不能夠拖出(注意,紅色箭頭也是有x座標的偏移量的)。即當不能夠拖出抽屜時,應觸發默認事件,好比垂直方向的滾動等等。
當手指按下觸發touchstart
時,記錄初始位置P0;當滑動手指時,觸發的第一次touchmove
時,記錄位置P1,咱們將P0到P1的矢量記爲S(原諒我這個靈魂畫手)
這時候很容易看出,∠θ大於某個值時,好比30度,就多是垂直方向的滾動操做而不是拖動抽屜。因此,能夠根據y/x>tan30°
獲得判斷條件:
if (isVerticle === undefined) isVerticle = Math.abs(nowY - startY) / Math.abs(nowX - startX) > (Math.sqrt(3) / 3);
當isVerticle
爲true
時,不執行drawer的拖動
咱們使用css3的transition
屬性使drawer具備過渡動畫效果,這裏寫一個moving
類
.moving transition transform .3s ease
別忘了加上class綁定,拖動時是不須要過渡動畫的(要跟隨手指),而鬆開手指時才須要過渡動畫。
<div class="drawer-wrap" :class="{'moving':moving,'will-change':willChange}" :style="{width:`${width}px`,left:`-${width)}px`,transform:`translate3d(${pos}px,0,0)`}"> <slot name="drawer"></slot> </div>
因此綁定touchend
事件的方法時要作這些步驟
const removeDrag = function (e) { if (isVerticle !== undefined) { if (!isVerticle) {//當斷定爲抽屜拖動才進入 let pos = this.pos; this.visible = pos > width * 3 / 5 //當前位置若是大於總寬度的3/5就斷定爲所有展開抽屜,不然將抽屜彈回隱藏 if (this.pos > 0 && this.pos < width) this.moving = true;//若是位置已經處於最小值或最大值處,不須要有動畫效果了 } this.pos = this.visible ? width : 0; } if (!this.moving) { this.willChange = false; //留個懸念 } isVerticle = undefined; //取消touchmove和touchend事件綁定 document.removeEventListener(mouseEvents.move, drag, false); document.removeEventListener(mouseEvents.up, removeDrag, false); }.bind(this);
上面你可能發現代碼裏有個this.willChange = false
,它是幹啥的捏?下面咱們請出css的will-change
大法
.will-change will-change transform
CSS 屬性 will-change 爲web開發者提供了一種告知瀏覽器該元素會有哪些變化的方法,這樣瀏覽器能夠在元素屬性真正發生變化以前提早作好對應的優化準備工做。 這種優化能夠將一部分複雜的計算工做提早準備好,使頁面的反應更爲快速靈敏。
實際上是咱們在touchstart
能夠預先告知瀏覽器抽屜可能要發生位移
const initDrag = function (e) { //... this.willChange = true; }.bind(this);
固然最後別忘了在transitionend
事件後把transition
和will-change
去掉,讓瀏覽器歇一下子~
上面說的已經基本上把主要功能實現了,可是這其中還有沒有哪裏能夠優化的?
咦?passive
是什麼鬼?
網站使用被動事件偵聽器以提高滾動性能,在您的觸摸和滾輪事件偵聽器上設置 passive 選項可提高滾動性能 具體看這裏
原來這是現代瀏覽器的一個新特性,咱們須要以新的方式來綁定咱們的touch事件,固然首先先檢測一下是否支持passive
const supportsPassive = (() => { let supportsPassive = false; try { const opts = Object.defineProperty({}, 'passive', { get: function () { supportsPassive = true; } }); window.addEventListener("test", null, opts); } catch (e) { } return supportsPassive; })();
因而咱們的綁定事件代碼變成這樣
document.addEventListener(mouseEvents.move, drag, supportsPassive ? {passive: true} : false);
是否有效果呢?有興趣的朋友能夠點這裏看國外大神的視頻
本文介紹了實現抽屜式導航欄的主要過程,詳細代碼已封裝成vue-drawer-layout組件,支持更豐富的定製和使用方式,具體文檔能夠訪問個人github或者npm官網檢索。歡迎各位多多提issue,不吝賜教,感謝!