Picker組件的設計與實現

前言

今天的主題是 NutUI Picker 組件的設計與實現,Picker組件是 NutUI 的一個拾取器組件,它用於顯示一系列的值集合,用戶能夠滾動選擇集合中一項,也能夠支持多個系列的值集合供用戶分別選擇。咱們經過一張效果圖,來看看組件具體實現了什麼功能。css

image

說到 NutUI, 可能有些人還不太瞭解,容咱們先簡單介紹一下。NutUI 是一套京東風格的移動端Vue組件庫,開發和服務於移動 Web 界面的企業級前中後臺產品。經過 NutUI,能夠快速搭建出風格統一的頁面,提高開發效率。目前已有 50+ 個組件,這些組件被普遍使用於京東的各個移動端業務中。html

接下來,咱們會經過如下幾個話題,展開今天的內容:前端

  • 爲何要封裝組件
  • NutUI Picker組件的實現原理
  • 遇到的問題

1、爲何要封裝組件

當業務達到必定規模後,會遇到不少類似功能界面,每次從新開發,會影響開發效律,且這些相近的代碼可能潛伏某些問題,一旦暴露,咱們須要花費不少時間去處理業務裏的相同代碼。若是咱們把這些相同的代碼進行合理化抽離,封裝組件,多處調用,咱們會發現,開發效律獲得質的飛躍。vue

經過一張圖來看一下的封裝組件帶來的好處:node

image

封裝組件,不只可讓協同開發變得高效規範,於此同時,組件化的前端開發方式也能夠爲後續業務擴展帶來更多便利。ios

有人說,這不就是重複造輪子,說到輪子,大部分人會聯想到圈子裏的名句 「不要重複造輪子」。可是不少人殊不知道,這句話的英文原文是 「Stop Trying to Reinvent the Wheel」, 真正的含義是不要重複發明輪子,而不是造輪子。以往的輪子,不必定知足咱們全部的開發需求,但咱們能夠以此爲基礎,加以改進,從而獲得一個更好的輪子,經過這種漸進的過程,達到咱們的需求。git

image

2、 NutUI Picker組件的實現原理

這個組件在平常業務需求中仍是比較常見的。它既能夠承載簡單的選項卡功能,同時也能夠知足較爲繁瑣的日期時間選擇,亦或是級聯地址選擇功能。基於 picker 組件的日期時間組件,咱們也有封裝,有興趣的可訪問 NutUI 組件庫查看。github

從文章前言中,咱們已經大體瞭解 picker 組件實現了什麼功能,它經過相似滾輪的三維旋轉來實現選中選擇集的某一項。npm

先來看看組件源碼的目錄結構:bash

image

咱們主要圍繞最後三個文件來講。
基於就近原則,咱們把相關的文件放在同一個目錄下,基於職責單一原則,咱們把組件顆粒化,以保證組件儘量簡單和通用性比較好。把picker組件分爲父組件 picker.vue 和子組件 picker-slot.vue,子組件只負責滾輪交互處理。父組件負責處理業務類邏輯。

子組件滾輪部分

一、先來看一下dom部分的分工

<div class="nut-picker-list">
    <div class="nut-picker-roller" ref="roller">
        <div class="nut-picker-roller-item" :class="{'nut-picker-roller-item-hidden': isHidden(index + 1)}" v-for="(item,index) in listData" :style="setRollerStyle(index + 1)" :key="item.label" >
            {{item.value}}
        </div>
    </div>
    <div class="nut-picker-content">
        <div class="nut-picker-list-panel" ref="list">
            <div class="nut-picker-item" v-for="(item,index) in listData" :key="item.label " >
                 {{item.value }}
            </div>
        </div>
    </div>
    <div class="nut-picker-indicator"></div>
</div>
複製代碼
  • nut-picker-indicator: 分割線
  • nut-picker-content: 高亮選中區域
  • nut-picker-roller: 滾輪區域

不想看代碼?「小二,上圖!」

image

二、css部分

把nut-picker-indicator設置在最高層級,以避免被遮蓋

.nut-picker-indicator{
    ...
    z-index: 3;
}
複製代碼

nut-picker-roller滾輪區域

.nut-picker-roller{
    z-index: 1;
    transform-style: preserve-3d;
    ...
    .nut-picker-roller-item{
        backface-visibility: hidden;
        position: absolute;
        top: 0;
        ...
    }
}
複製代碼

要實現一些3D效果,transform-style:preserve-3d;是必不可少的,通常而言,該屬性應用在 3D 變換的父元素上,也就是舞臺元素。這樣子元素就具備 3D 屬性效果。在 CSS 的 3D 世界中,默認狀況下,咱們能夠看到背後的元素,爲了切合實際,咱們經常讓後面的元素不可見,因此設置子元素 backface-visibility: hidden; 值得注意的是,設置了 transform-style:preserve-3d 該屬性,就不能防止子級元素溢出,若是設置了overflow:hidden,那麼transform-style:preserve-3d將會無效。

咱們經過模擬滾輪旋轉來實現組件的交互效果,用一張側面圖來更直觀的看一下。

image

接下來咱們來看一下如何實現。

首先,須要模擬一個球體,設置選擇集的每一項(如下簡稱「滾輪項」)爲 position:absolute,共用同一個中心點即球心,而後依次堆疊於此。

image

咱們先溫習一些基礎知識,translate3d() 函數可使一個元素在三維空間移動。這種變形的特色是,使用三維向量的座標定義元素在每一個方向移動多少。當z軸值越大時,元素也離觀看者更近,咱們經過設置z軸讓滾輪項的兩端到達球體表面,z軸的大小,至關於球體的半徑,由於咱們設定可視區域的高度爲260,因此設置半徑爲104,若是半徑太小,咱們須要戴着高倍放大鏡來尋找滾輪項,若是半徑過大,那麼滾輪項就跑到咱們腦後去了...,不能讓眼睛長在後腦勺這麼可怕的事情發生!所謂距離產生美,因此保持適當的距離(80%)是最美的。

setRollerStyle(index) {
    return `translate3d(0px, 0px, 104px)`;
}
複製代碼

image

這時候,咱們發現,全部滾輪項從集體堆疊球心變爲堆疊到球體某兩個點上,咱們須要把它們按照周長平鋪開來。這時,咱們要用到rotate3d()屬性,咱們滾輪是圍繞 X 軸旋轉,因此設定 X 軸 rotate3d(1, 0, 0, a) 便可, a 是一個角度值,用來指定元素在 3D 空間旋轉的角度,值爲正值,元素順時針旋轉,反之元素逆時針旋轉。那這個角度如何來設定呢,能夠經過一個圓心角公式來推斷,圓心角的度數等於它所對的弧的度數,咱們的半徑是104,弧長是36(咱們預先設定的顯示區),從而四捨五入計算 a 角度爲20。是否是有一種被說蒙圈的感受,咱們經過一張圖,更直觀的理解一下。

image

利用上面的分析,咱們來動態設置滾輪項的最終位置。

setRollerStyle(index) {
    return `transform: rotate3d(1, 0, 0, ${-this.rotation * index}deg) translate3d(0px, 0px, 104px)`;
}
複製代碼

須要注意的是滾輪項的個數可能會不少,超過一圈的可能性是大大存在的,但咱們既不能一刀切只給用戶展現指定的個數,也不能所有展現形成重疊問題出現。這時候,咱們須要把超出的部分隱藏掉,咱們知道角度值 a 是20度,圓的一週是360度,因此最多能夠顯示18個,咱們以當前中心爲基礎點,前面展現8個,後面展現9個。

isHidden(index) {
    return (index >= this.currIndex + 9 || index <= this.currIndex - 8) ? true : false;
}
複製代碼

三、添加事件

最後,咱們來添加滑動事件,先獲取 Vue 實例關聯的 DOM 元素,設置touchstarttouchmovetouchend事件,須要注意的是,咱們要記得在beforeDestroy事件中銷燬這些事件。

touchstart事件用來記錄開始點,touchmovetouchend事件用來記錄滾動結束點,計算差值,動態設置滾輪最外層元素的滾動距離和滾動角度。在滾動時候須要對滾動距離進行修正,保證滾動的最後距離爲 lineSpacing (滾輪項的高度36)的倍數值。

咱們還增長了增長彈性效果,容許touchmove滾動超出滾動範圍,而後在touchend事件中修正位置爲首項、尾項。

來看一下具體實現。

setMove(move, type, time) {
    let updateMove = move + this.transformY;
    if (type === 'end') { // touchend 滾動處理
    
        // 超出限定滾動距離修正
        if (updateMove > 0) {
            updateMove = 0;
        }
        if (updateMove < -(this.listData.length - 1) * this.lineSpacing) {
            updateMove = -(this.listData.length - 1) * this.lineSpacing;
        }

        // 設置滾動距離爲lineSpacing的倍數值
        let endMove = Math.round(updateMove / this.lineSpacing) * this.lineSpacing;
        let deg = `${(Math.abs(Math.round(endMove / this.lineSpacing)) + 1) * this.rotation}deg`;
        this.setTransform(endMove, type, time, deg);
        this.timer = setTimeout(() => {
            this.setChooseValue(endMove);
        }, time / 2); 

        this.currIndex = (Math.abs(Math.round(endMove/ this.lineSpacing)) + 1);
    } else { // touchmove 滾動處理
        let deg = '0deg';
        if (updateMove < 0) {
            deg = `${(Math.abs(updateMove / this.lineSpacing) + 1) * this.rotation}deg`;
        } else {
            deg = `${((-updateMove / this.lineSpacing) + 1) * this.rotation}deg`;
        }
        this.setTransform(updateMove, null, null, deg);
        this.currIndex = (Math.abs(Math.round(updateMove/ this.lineSpacing)) + 1);
    }
},
複製代碼

touchend中,爲滾輪父元素增長了過渡的「緩動函數」, 模擬慣性滾動效果。

setTransform(translateY = 0, type, time = 1000, deg) {
    this.$refs.roller.style.transition =  type === 'end' ? `transform ${time}ms cubic-bezier(0.19, 1, 0.22, 1)` : '';
    this.$refs.roller.style.transform = `rotate3d(1, 0, 0, ${deg})`;
}
複製代碼

經過以上的內容,咱們的滾輪效果已經基本成型。可是咱們還想要相似 ios 上時間選擇器高亮當前區域的效果,該如何實現呢?

咱們嘗試了以下三種方法。

第一種,考慮當滾輪項停留在高亮選中區域的時候,字體進行變化,但實踐發現,只能在滾動結束的時候讓字體變化,沒法在滾動過程當中設置,體驗並不友好。

第二種,是否能夠巧用 CSS,利用背景漸變和 background-size 配合完成漸變,利用蒙層來實現呢!

.nut-picker-mask{
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-image: linear-gradient(180deg,hsla(0,0%,100%,.9),hsla(0,0%,100%,.6)),linear-gradient(0deg,hsla(0,0%,100%,.9),hsla(0,0%,100%,.6));
    background-position: top, bottom;
    background-size: 100% 108px;
    background-repeat: no-repeat;
    z-index: 3;
}
複製代碼

image

這裏把背景設置成黃色,便於咱們看效果。

感受還能夠,這樣就搞定了嗎?

咱們在pc端模擬一切正常,在真機上卻出現了詭異的畫面,上滑彈出的時候,蒙層會延遲展現,影響體驗效果。只有禁止上滑過渡效果,才能夠正常展現。去除上滑效果是不可能的,咱們只能考慮一下其餘辦法。

第三種,是否能夠設置一個附屬滾動,也就是上面說的高亮顯示區,將其蓋在滾輪上面,裏面每一個元素高度等於可視區高度,當滾輪滑動的時候,高亮顯示區內部列表元素跟隨一塊兒滑動。

image

實踐證實,這種方法能夠避免上述兩個方法的弊端,完美解決咱們的需求。來看一下具體實現方法。

.nut-picker-content {
    position: absolute;
    height: 36px;
    ...
    .nut-picker-roller-item{
        height: 36px;
        ...
    }
}
複製代碼

而後在上面的 setTransform 函數中,增長高亮展現區滾動效果。

setTransform(translateY = 0, type, time = 1000, deg) {
    ...
    this.$refs.list.style.transition =  type === 'end' ? `transform ${time}ms cubic-bezier(0.19, 1, 0.22, 1)` : '';
    this.$refs.list.style.transform = `translate3d(0, ${translateY}px, 0)`;
}
複製代碼

父組件部分

除了滾動效果,咱們還有一些灰色蒙層、上滑彈出、工做欄等業務內容,咱們交由父組件去處理。咱們業務中也會涉及到多列狀況,因此父組件能夠把 props 數據拆分傳給子組件,讓每一個子組件相互獨立,監聽子組件event事件,傳遞給外層。

3、遇到的問題

咱們的組件是基於px來實現的,在 issues 中,收集到部分用戶遇到一些問題,這裏提供瞭解決方案。

一、使用px2rem,滾輪旋轉出現誤差

由於 px 轉 rem 有時候轉出來的值會有誤差,而且出現多個小數位,致使滾動的高度和實際轉化的高度出現誤差,咱們可體經過如下配置解決

第一種:在.postcssrc.js配置文件中,把nutui開頭的過濾掉

module.exports = ({ file }) => {
  return {
    plugins: [
        ...
        pxtorem({
            rootValue: rootValue,
            propList: ['*'],
            minPixelValue: 2,
            selectorBlackList: ['.nut'] // 設置
        })
   }
}
複製代碼

第二種: postcss-px2rem-exclude代替postcss-px2rem

npm uninstall postcss-px2rem
npm i postcss-px2rem-exclude -D
複製代碼
// 在.postcssrc.js配置
module.exports = ({ file }) => {
    return {
        plugins: [
            ...
            pxtorem({
                remUnit: rootValue,
                exclude: '/node_modules/@nutui/nutui/packages/picker'
            })
        ]
   }
}
複製代碼

二、使用lib-flexible,組件被縮小問題

咱們的 css 是基於 data-dpr 爲1的時候編寫的,若是使用了 lib-flexible, 頁面要設置

<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
複製代碼

後續咱們也會考慮從代碼層面上去解決上述問題。

總結

以上就是本文的所有內容,主要介紹了 Picker 組件的一些設計思想與實現原理,若是您對這個組件感興趣,不妨查看和試用一下,使用上有任何問題,可在 issues 上進行提問,咱們會盡快解答和修復,後續咱們也會對組件進行持續優化迭代,訪問 NutUI組件庫,更多組件等你發現。

相關文章
相關標籤/搜索