今天的主題是 NutUI Picker 組件的設計與實現,Picker組件是 NutUI 的一個拾取器組件,它用於顯示一系列的值集合,用戶能夠滾動選擇集合中一項,也能夠支持多個系列的值集合供用戶分別選擇。咱們經過一張效果圖,來看看組件具體實現了什麼功能。css
說到 NutUI, 可能有些人還不太瞭解,容咱們先簡單介紹一下。NutUI 是一套京東風格的移動端Vue組件庫,開發和服務於移動 Web 界面的企業級前中後臺產品。經過 NutUI,能夠快速搭建出風格統一的頁面,提高開發效率。目前已有 50+ 個組件,這些組件被普遍使用於京東的各個移動端業務中。html
接下來,咱們會經過如下幾個話題,展開今天的內容:前端
當業務達到必定規模後,會遇到不少類似功能界面,每次從新開發,會影響開發效律,且這些相近的代碼可能潛伏某些問題,一旦暴露,咱們須要花費不少時間去處理業務裏的相同代碼。若是咱們把這些相同的代碼進行合理化抽離,封裝組件,多處調用,咱們會發現,開發效律獲得質的飛躍。vue
經過一張圖來看一下的封裝組件帶來的好處:node
封裝組件,不只可讓協同開發變得高效規範,於此同時,組件化的前端開發方式也能夠爲後續業務擴展帶來更多便利。ios
這個組件在平常業務需求中仍是比較常見的。它既能夠承載簡單的選項卡功能,同時也能夠知足較爲繁瑣的日期時間選擇,亦或是級聯地址選擇功能。基於 picker 組件的日期時間組件,咱們也有封裝,有興趣的可訪問 NutUI 組件庫查看。 git
從文章前言中,咱們已經大體瞭解 picker 組件實現了什麼功能,它經過相似滾輪的三維旋轉來實現選中選擇集的某一項。github
先來看看組件源碼的目錄結構:npm
咱們主要圍繞最後三個文件來講。
基於就近原則,咱們把相關的文件放在同一個目錄下,基於職責單一原則,咱們把組件顆粒化,以保證組件儘量簡單和通用性比較好。把picker組件分爲父組件 picker.vue 和子組件 picker-slot.vue,子組件只負責滾輪交互處理。父組件負責處理業務類邏輯。bash
一、先來看一下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>
不想看代碼?「小二,上圖!」
二、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
將會無效。
咱們經過模擬滾輪旋轉來實現組件的交互效果,用一張側面圖來更直觀的看一下。
接下來咱們來看一下如何實現。
首先,須要模擬一個球體,設置選擇集的每一項(如下簡稱「滾輪項」)爲 position:absolute
,共用同一個中心點即球心,而後依次堆疊於此。
咱們先溫習一些基礎知識,translate3d()
函數可使一個元素在三維空間移動。這種變形的特色是,使用三維向量的座標定義元素在每一個方向移動多少。當z軸值越大時,元素也離觀看者更近,咱們經過設置z軸讓滾輪項的兩端到達球體表面,z軸的大小,至關於球體的半徑,由於咱們設定可視區域的高度爲260,因此設置半徑爲104,若是半徑太小,咱們須要戴着高倍放大鏡來尋找滾輪項,若是半徑過大,那麼滾輪項就跑到咱們腦後去了...,不能讓眼睛長在後腦勺這麼可怕的事情發生!所謂距離產生美,因此保持適當的距離(80%)是最美的。
setRollerStyle(index) { return `translate3d(0px, 0px, 104px)`; }
這時候,咱們發現,全部滾輪項從集體堆疊球心變爲堆疊到球體某兩個點上,咱們須要把它們按照周長平鋪開來。這時,咱們要用到rotate3d()
屬性,咱們滾輪是圍繞 X 軸旋轉,因此設定 X 軸 rotate3d(1, 0, 0, a)
便可, a 是一個角度值,用來指定元素在 3D 空間旋轉的角度,值爲正值,元素順時針旋轉,反之元素逆時針旋轉。那這個角度如何來設定呢,能夠經過一個圓心角公式來推斷,圓心角的度數等於它所對的弧的度數,咱們的半徑是104,弧長是36(咱們預先設定的顯示區),從而四捨五入計算 a 角度爲20。是否是有一種被說蒙圈的感受,咱們經過一張圖,更直觀的理解一下。
利用上面的分析,咱們來動態設置滾輪項的最終位置。
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 元素,設置touchstart
、touchmove
、touchend
事件,須要注意的是,咱們要記得在beforeDestroy
事件中銷燬這些事件。
touchstart
事件用來記錄開始點,touchmove
和touchend
事件用來記錄滾動結束點,計算差值,動態設置滾輪最外層元素的滾動距離和滾動角度。在滾動時候須要對滾動距離進行修正,保證滾動的最後距離爲 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; }
這裏把背景設置成黃色,便於咱們看效果。
感受還能夠,這樣就搞定了嗎?
咱們在pc端模擬一切正常,在真機上卻出現了詭異的畫面,上滑彈出的時候,蒙層會延遲展現,影響體驗效果。只有禁止上滑過渡效果,才能夠正常展現。去除上滑效果是不可能的,咱們只能考慮一下其餘辦法。
第三種,是否能夠設置一個附屬滾動,也就是上面說的高亮顯示區,將其蓋在滾輪上面,裏面每一個元素高度等於可視區高度,當滾輪滑動的時候,高亮顯示區內部列表元素跟隨一塊兒滑動。
實踐證實,這種方法能夠避免上述兩個方法的弊端,完美解決咱們的需求。來看一下具體實現方法。
.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事件,傳遞給外層。
咱們的組件是基於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組件庫,更多組件等你發現。