tab欄(標籤切換欄)是app中常見的一種交互方式,它能夠承載更多的內容,同時又兼顧友好體驗的優勢。但在小程序中,官方並無爲我們提供現成的組件。所以咱們程序員展示才藝的時候到了(其實市面上的ui庫也作了這個組件)。今天我們就來實現一個對用戶更加友好的tab欄,讓用戶「一點」就停不下來,起到解壓的功效~~!css
廢話很少說,先上效果圖。html
不瞞您說,這東西我能點一天^^。言歸正傳,因爲tab欄用的地方不少,因此須要封裝成組件,所以沒有開發或者沒用過組件的同志請瞧一瞧官方文檔。我以前也寫過一篇組件開發的教程,有興趣的能夠點一下。git
爲了照顧新手,我會一步步分析整個實現流程。不只僅是分析代碼,思想纔是程序的靈魂,而一個程序員從初級進階的過程也正是從代碼到思想的轉變。程序員
根據上面的效果圖,我們能夠分析出一下幾點預期:github
根據以上預期,能夠分析出實現思路以下:小程序
這些分析是有必要的,它將爲咱們後面的一些工做其指導做用,防止咱們在編碼的過程當中迷失自我。下面先從wxml的編寫開始。api
一下是咱們wxml的基本骨架,最外層用scroll-view組件,內容部分再包一層view,這樣有利於咱們後面佈局。數組
<scroll-view> <view> 內容部分 </view> </scroll-view>
因爲tab欄的項數是不固定的,並且須要組件外傳入。因此咱們使用wx:if指令完成每一項的渲染,並且組件外須要傳入一個數組。編寫後的代碼以下。app
<scroll-view class='component'> <view class='content'> <view data-index="{{ index }}" wx:for="{{ items }}" wx:key="{{ index }}"> <text class='text'>{{ item }}</text> </view> </view> </scroll-view>
相信這一步只要有小程序開發基礎的都能看懂,我順便爲全部的結點加上了類名,後面寫樣式須要用到。注意:組件中不推薦使用標籤及子類選擇器,全部在須要寫樣式的結點上都加上類名,官方推薦使用類選擇器。這一步循環後須要加上 wx:key="{{ index }} 以及 data-index="{{ index }}" 。由於咱們的程序須要明確知道切換的每一項,而且在切換到不一樣項的時候作出相應的操做,不定義一個自定義數據index,後面的工做沒法展開。dom
這樣tab欄的主體wxml就寫完了,不過咱們好像還少了個底部「條塊」的代碼。其實當初我也是以爲底部「條塊」用 border-bottom:1px solid #666 之類的css樣式實現不就能夠了嗎?其實認真觀察就會發現,底部「條塊」是帶動畫效果的,並非一切換就裏馬到文字下方,若是是這樣咱們大可給text或者view設置一個底部邊框,這樣一來咱們的教程就結束了。全部爲了實現動畫效果,咱們須要單獨給個view去做爲這個「條塊」,而且在css中給它添加動畫效果。
這裏打個岔子,由於在編寫組件的過程當中,不少樣式代碼都不能在wxss文件中寫死,這樣組件就毫無擴展性可言,就是去了組件的意義。那麼怎麼把樣式給寫活吶(又不能在wxss中寫邏輯代碼)?實現方式有兩種:1.經過動態改變元素的class;2.經過動態改變元素的style屬性。爲了更精細的控制樣式,咱們這裏採用第二種方式(這樣寫會讓dom渲染時間增長)。
下面是wxml文件的完整形態。
<scroll-view class='component cus' scroll-x="{{ isScroll }}" style='{{ scrollStyle }}'> <view class='content'> <view class='item' data-cus="{{ dataCus[index] }}" data-index="{{ index }}" wx:for="{{ items }}" wx:key="{{ index }}" style='min-width: {{ itemWidth }}rpx; height: {{ height }}rpx' catchtap='onItemTap' > <text class='text' style='color: {{ mSelected == index ? selectColor : textColor }};font-size: {{ textSize }}rpx;'>{{ item }}</text> </view> <view class='bottom-bar {{ theme == "smallBar" ? "small" : "" }}' style='background-color: {{ selectColor }}; left: {{ left }}px; right: {{ right }}px; bottom: {{ bottom }}rpx; transition: {{ transition }}; border-shadow: 0rpx 0rpx 10rpx 0rpx {{ selectColor }};'></view> </view> </scroll-view>
能夠看到裏面動態綁定了不少變量,下面咱們來一個個的介紹各變量的做用。
scroll-x="{{ isScroll }} 用於動態改變scroll-view組件的滾動,由於咱們須要實現當元素小於5個的時候咱們不該該讓tab欄滾動,由於這個時候的元素不多,不滾動纔是最優的用戶體驗。
data-index="{{ index }}" 用於惟一標識每一項,方便後面對每一項進行操做
wx:for="{{ items }}" 用於渲染列表,須要組件外傳入,由於tab組件在被使用前並不知道每一項的具體內容,固然你大可在組件裏定義個數組,這樣的組件就沒有同樣,只能在一種場合下使用。
style='min-width: {{ itemWidth }}rpx; height: {{ height }}rpx' 這裏的兩個變量用於控制每一項最外層view的樣式。其中itemWidth只在組件內部使用,由於對於組件外部來講,咱們更但願這個tab組件能根據咱們傳入的數據自適應的改變寬度。而height須要對外提供接口,由於根據不一樣的使用場景,咱們可能須要不一樣高度的tab組件來知足咱們的需求。
style='color: {{ mSelected == index ? selectColor : textColor }};font-size: {{ textSize }}rpx;' mSelected只在組件內部使用表示選中的某一項,當該項被選中後須要改變顏色,即:當mSelected與當前項的索引index相等時才表示選中。selectColor與textColor都須要外部提供。這樣咱們就實現了選中改變文字顏色的效果。
{{ theme == "smallBar" ? "small" : "" }} 這裏使用到了第一種動態改變樣式的方式,根據主題來改變類名。
style='background-color: {{ selectColor }}; left: {{ left }}px; right: {{ right }}px; bottom: {{ bottom }}rpx; transition: {{ transition }}; border-shadow: 0rpx 0rpx 10rpx 0rpx {{ selectColor }};' 這裏是實現「條塊」動畫的基礎,能夠經過left和right屬性來改變「條塊」的位置以及寬度,是否是很神奇。在js部分咱們就是經過操做left和right變量來實現咱們看到的動畫效果。
因爲咱們大部分樣式都是動態的,因此必須在wxml中寫。所以wxss中的代碼就不多,只須要寫一些靜態的樣式。一下是完整代碼,因爲比較簡單,就不過多的解釋了。
.component { background-color: white; white-space: nowrap; box-sizing: border-box; } .content { position: relative; } .item { display: inline-flex; align-items: center; justify-content: center; padding: 0 30rpx; } .text { transition: color 0.2s } .bottom-bar { position: absolute; height: 2px; border-radius: 2px } .small { height: 4px; border-radius: 2px; }
須要注意的是,底部「條塊」使用了left和right屬性,所以須要使用相對定位。因爲咱們須要實現滾動效果,因此scroll-view的樣式部分咱們還須要加一條 white-space: nowrap; 屬性來防止自動換行(按理來講,既然設置了橫向滾動,scroll-view組件就應該給咱們自動加上這條屬性),反正這應該算是scroll-view組件的一個bug了,有興趣的同窗能夠看下個人這篇博客。
重頭戲來了。首先來看一下完整的js代碼,後面我再一點點講解。
1 const themes = { 2 smallBar: 'smallBar' 3 } 4 5 Component({ 6 /** 7 * 組件的屬性列表 8 */ 9 properties: { 10 items: { 11 type: Array, 12 value: ['item1', 'item2', 'item3', 'item4'], 13 observer: function (newVal) { 14 if (newVal && newVal.length < 5) { 15 this.setData({ 16 itemWidth: (750 / newVal.length) - 60 17 }) 18 } 19 } 20 }, 21 height: { 22 type: String, 23 value: '120' 24 }, 25 textColor: { 26 type: String, 27 value: '#666666' 28 }, 29 textSize: { 30 type: String, 31 value: '28' 32 }, 33 selectColor: { 34 type: String, 35 value: '#FE9036' 36 }, 37 selected: { 38 type: String, 39 value: '0', 40 observer: function (newVal) { 41 this.setData({ 42 mSelected: newVal 43 }) 44 } 45 }, 46 theme: { 47 type: String, 48 value: 'default', 49 observer: function (newVal) { 50 if (this.data.theme == themes.smallBar) { 51 this.setData({ 52 bottom: this.data.height / 2 - this.data.textSize - 8, 53 scrollStyle: '' 54 }) 55 } 56 } 57 }, 58 dataCus: { 59 type: Array, 60 value: '', 61 observer: function (newVal) { 62 this.setData({ 63 mDataCus: newVal 64 }); 65 } 66 } 67 }, 68 69 /** 70 * 組件的初始數據 71 */ 72 data: { 73 itemWidth: 128, 74 isScroll: true, 75 scrollStyle: 'border-bottom: 1px solid #e5e5e5;', 76 left: '0', 77 right: '750', 78 bottom: '0', 79 mSelected: '0', 80 lastIndex: 0, 81 transition: 'left 0.5s, right 0.2s', 82 windowWidth: 375, 83 domData: [], 84 textDomData: [], 85 mDataCus: [] 86 }, 87 88 externalClasses: ['cus'], 89 90 /** 91 * 組件的方法列表 92 */ 93 methods: { 94 barLeft: function(index, dom) { 95 let that = this; 96 this.setData({ 97 left: dom[index].left 98 }) 99 }, 100 barRight: function (index, dom) { 101 let that = this; 102 this.setData({ 103 right: that.data.windowWidth - dom[index].right, 104 }) 105 }, 106 onItemTap: function(e) { 107 const index = e.currentTarget.dataset.index; 108 let str = this.data.lastIndex < index ? 'left 0.5s, right 0.2s' : 'left 0.2s, right 0.5s'; 109 this.setData({ 110 transition: str, 111 lastIndex: index, 112 mSelected: index 113 }) 114 if (this.data.theme == themes.smallBar) { 115 this.barLeft(index, this.data.textDomData); 116 this.barRight(index, this.data.textDomData); 117 } else { 118 this.barLeft(index, this.data.domData); 119 this.barRight(index, this.data.domData); 120 } 121 this.triggerEvent('itemtap', e, { bubbles: true }); 122 } 123 }, 124 125 lifetimes: { 126 ready: function () { 127 let that = this; 128 const sysInfo = wx.getSystemInfoSync(); 129 this.setData({ 130 windowWidth: sysInfo.screenWidth 131 }) 132 const query = this.createSelectorQuery(); 133 query.in(this).selectAll('.item').fields({ 134 dataset: true, 135 rect: true, 136 size: true 137 }, function (res) { 138 that.setData({ 139 domData: res, 140 }) 141 that.barLeft(that.data.mSelected, that.data.domData); 142 that.barRight(that.data.mSelected, that.data.domData); 143 // console.log(res) 144 }).exec() 145 query.in(this).selectAll('.text').fields({ 146 dataset: true, 147 rect: true, 148 size: true 149 }, function (res) { 150 that.setData({ 151 textDomData: res, 152 }) 153 if (that.data.theme == themes.smallBar) { 154 that.barLeft(that.data.mSelected, that.data.textDomData); 155 that.barRight(that.data.mSelected, that.data.textDomData); 156 } 157 console.log(res) 158 }).exec() 159 }, 160 }, 161 })
properties字段中的變量都是對外提供的接口。這個字段裏面咱們着重看一下items字段。
items: { type: Array, value: ['item1', 'item2', 'item3', 'item4'], observer: function (newVal) { if (newVal && newVal.length < 5) { this.setData({ itemWidth: (750 / newVal.length) - 60 }) } } },
咱們把該字段的類型定義爲了數組,所以組件外須要傳入一個數組。在外界沒有傳入任何數值的狀況下咱們也要顯示一個完整的tab欄啊,因此默認值是有必要的,儘管使用的時候必定會覆蓋咱們的默認值。 observer 這個屬性用得可能不是不少,你們可能有些陌生。仔細看過官方文檔的同窗應該知道,該屬性用於當items字段在組件外被賦值或者被改變的狀況下觸發回調函數,其中回調函數能夠接受newVal這樣的新值,也能夠接受oldVal這樣的老值。咱們須要根據傳入的數組動態的設置每一項的寬度,在講解wxml的時候咱們知道 itemWidth 變量是用來控制每一項的寬度的。這裏用if判斷當數組長度小於5時就會設置每一項的寬度,而這個寬度就是經過750除以數組長度來的,最後咱們還要減去每一項的左右padding,由於padding是不計入寬度的。這樣以來,當數組的元素個數低於五個的時候,tab組件就會將屏幕寬度等分,這樣就不會出現滾動效果。當數組的元素個數超過5,那麼咱們就給一個默認值,固然咱們在wxml中設置的是 min-width 屬性,因此不用擔憂設置了寬度就會形成寬度不自適應的狀況。
由於底部「條塊」須要知道當前選項的位置,這樣才能滾動到選中項的下面。因此要實現這個效果,以及當前處於第幾項以及該項的位置。小程序雖然不支持dom操做,但支持獲取dom屬性。
lifetimes: { ready: function () { let that = this; const sysInfo = wx.getSystemInfoSync(); this.setData({ windowWidth: sysInfo.screenWidth }) const query = this.createSelectorQuery(); query.in(this).selectAll('.item').fields({ dataset: true, rect: true, size: true }, function (res) { that.setData({ domData: res, }) that.barLeft(that.data.mSelected, that.data.domData); that.barRight(that.data.mSelected, that.data.domData); // console.log(res) }).exec() query.in(this).selectAll('.text').fields({ dataset: true, rect: true, size: true }, function (res) { that.setData({ textDomData: res, }) if (that.data.theme == themes.smallBar) { that.barLeft(that.data.mSelected, that.data.textDomData); that.barRight(that.data.mSelected, that.data.textDomData); } console.log(res) }).exec() }, },
這段代碼是在ready生命週期中進行的,由於只有組件在ready這個生命週期,咱們才能獲取dom。這個生命週期是在dom渲染完畢後執行的。首先咱們經過 wx.getSystemInfoSync() 獲取系統的信息,裏面包括咱們須要的屏幕寬度。注意整個計算過程都是使用px做爲單位,雖然咱們知道每一個設備的寬度固定爲750rpx,可是px是不固定的。以後咱們經過 this.createSelectorQuery(); 來查詢須要的dom結點(相似與jQuery)。首先查詢類名爲item的全部元素,而且將數據保存到domData變量。因爲在smallBar主題下,咱們是根據文字寬度來定位底部「條塊」的,全部還須要獲取類名爲text的全部結點信息,並將其保存到textDomData變量中。下面咱們來看下獲取的dom數據的結構。
其中left正是該元素在父組件中距離父組件最左邊的距離以px爲單位。對咱們有用的就是left和right兩字段,這意味着咱們知道了每一項的具體定位。至於當前的選項咱們則經過點擊事件來獲取。下面是整個組件的核心代碼。
methods: { barLeft: function(index, dom) { let that = this; this.setData({ left: dom[index].left }) }, barRight: function (index, dom) { let that = this; this.setData({ right: that.data.windowWidth - dom[index].right, }) }, onItemTap: function(e) { const index = e.currentTarget.dataset.index; let str = this.data.lastIndex < index ? 'left 0.5s, right 0.2s' : 'left 0.2s, right 0.5s'; this.setData({ transition: str, lastIndex: index, mSelected: index }) if (this.data.theme == themes.smallBar) { this.barLeft(index, this.data.textDomData); this.barRight(index, this.data.textDomData); } else { this.barLeft(index, this.data.domData); this.barRight(index, this.data.domData); } this.triggerEvent('itemtap', e, { bubbles: true }); } },
這裏定義了三個函數,其中 barLeft 和 barRight 分別完成設置底部「條塊」的left值和right值。須要特別說明一下,只要咱們動態計算並設置了底部「條塊」的left和right屬性,那麼底部「條塊」的位置大小在水平方向上就以及肯定,而垂直方向上的位置大小都是固定寫死在css文件中的。這兩個函數都須要傳入當前選項的索引以及全部選項dom的位置信息。
onItemTap 方法綁定了每一項的點擊事件,能夠查看wxml中的完整代碼。當選項被點擊後,它的索引可經過 e.currentTarget.dataset.index 獲取,由於咱們在wxml中定義了一個自定義屬性。
至此咱們的核心邏輯就實現完畢了,關鍵點在於獲取全部選項的位置信息以及當前選項的索引。有興趣的同窗能夠前往github查看源代碼。
雖然這篇博文是以教程的形式寫的,可是咱們仍是有必要總結一下。
在寫程序的時候思想要走在編碼的前列,不要讓思想被具體代碼牽着鼻子走。要有必定的封裝思想,雖然ctrl+c,ctrl+v大法能夠解決一切問題,可是這樣的代碼是沒法維護和閱讀的。既然封裝,那就得考慮擴展性和閉開原則了。哪裏開放,哪裏閉合內心要有點逼數。可不能夠擴展將影響到後續的修改。當一個極具挑戰的東西須要咱們實現的時候,只須要抓住重點,分步展開,就會發現問題就變得簡單起來了。若是須要的步數太多,那也許是你簡單問題複雜化了。
若是你懶得寫,也能夠嘗試一下使用博主封裝的小程序UI組件庫,裏面包含了開發中經常使用的組件。但願各位老鐵多多提意見,也能夠提交本身的組件。打了這麼多字,你就不心疼一下博主?
掃描小程序碼,可查看效果。