【只發精品】匠心打造Vue側滑選單元件

AlexanderHuang發表於2017-12-18

本文介紹一個簡單的DrawerLayout(類似Android的DrawerLayout)佈局元件的實現,基於Vue.js。介紹的內容已經制作成 vue-drawer-layout 元件。

前言

大家有興趣先用手機掃一掃這個二維碼,或者點我

試一試
然後點選頁面中左上角的頭像開啟drawer或者向右向左拖拽,就可以看到下面gif的效果,開啟自己的手機QQ,是不是很像:)
演示
谷歌官方把這種佈局叫做DrawerLayout(抽屜式導航欄)。那麼我們要如何實現呢,好了正片開始!

HTML結構

頁面結構很簡單,一個抽屜,一個主容器,內容可以利用slot支援外部自行定製。

<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%使其整個都藏在外部

使用Touch

首先,判斷瀏覽器是否支援touchEvent

    let isTouch = 'ontouchstart' in window;
    let mouseEvents = isTouch ?
        {
            down: 'touchstart',
            move: 'touchmove',
            up: 'touchend'
        } :
        {
            down: 'mousedown',
            move: 'mousemove',
            up: 'mouseup'
        };
複製程式碼

繫結touchdown事件

    document.addEventListener(mouseEvents.down, initDrag, false);
複製程式碼

先定義一些變數,手指按下的x座標記為startX,滑動中手指的位置x座標記為nowX,drawer的x座標偏移量記為startPos

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,這樣抽屜已經跟隨手指向右移動了,並且不會超過我們設定的拖動最大值。

區分垂直滑動和水平滑動

接下來你會發現一個問題,當手指垂直滾動主內容時,向右滑動手指也會拖出抽屜,這時應該做一件事:區分垂直滑動和水平滑動

當然,辦法有很多,這裡先介紹一種利用三角函式來判定的方法

【只發精品】匠心打造Vue側滑選單元件
假設,上圖中的每個箭頭是手指滑動的方向,綠色箭頭代表可以拖出抽屜,紅色箭頭代表不可以拖出(注意,紅色箭頭也是有x座標的偏移量的)。即當不可以拖出抽屜時,應觸發預設事件,比如垂直方向的滾動等等。

當手指按下觸發touchstart時,記錄初始位置P0;當滑動手指時,觸發的第一次touchmove時,記錄位置P1,我們將P0到P1的向量記為S(原諒我這個靈魂畫手)

【只發精品】匠心打造Vue側滑選單元件
這時候很容易看出,∠θ大於某個值時,比如30度,就可能是垂直方向的滾動操作而不是拖動抽屜。所以,可以根據y/x>tan30°得到判斷條件:

if (isVerticle === undefined) isVerticle = Math.abs(nowY - startY) / Math.abs(nowX - startX) > (Math.sqrt(3) / 3);
複製程式碼

isVerticletrue時,不執行drawer的拖動

讓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事件後把transitionwill-change去掉,讓瀏覽器歇一會兒~

還有什麼可以優化的?

上面說的已經基本上把主要功能實現了,但是這其中還有沒有哪裡可以優化的?

【只發精品】匠心打造Vue側滑選單元件
咦?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,不吝賜教,感謝!

相關文章