微信小程序-tab標籤欄實現教程

1、摘要

  tab欄(標籤切換欄)是app中常見的一種交互方式,它能夠承載更多的內容,同時又兼顧友好體驗的優勢。但在小程序中,官方並無爲我們提供現成的組件。所以咱們程序員展示才藝的時候到了(其實市面上的ui庫也作了這個組件)。今天我們就來實現一個對用戶更加友好的tab欄,讓用戶「一點」就停不下來,起到解壓的功效~~!css

  廢話很少說,先上效果圖。html

  不瞞您說,這東西我能點一天^^。言歸正傳,因爲tab欄用的地方不少,因此須要封裝成組件,所以沒有開發或者沒用過組件的同志請瞧一瞧官方文檔。我以前也寫過一篇組件開發的教程,有興趣的能夠點一下git

2、正文

  爲了照顧新手,我會一步步分析整個實現流程。不只僅是分析代碼,思想纔是程序的靈魂,而一個程序員從初級進階的過程也正是從代碼到思想的轉變。程序員

1.預期與實現思路分析

  根據上面的效果圖,我們能夠分析出一下幾點預期:github

  1. 首先咱得支持滾動效果,不支持滾動那還玩個屁,畢竟手機屏幕並非無限寬的,而咱們須要的tab頁倒是無限多的。
  2. 內容部分必須是自適應的,由於每一項的文字個數並非固定的。
  3. 做爲組件,咱得知足閉開原則,即:須要外部修改的部分對外提供接口,不準外部修改的部分禁止訪問和修改。
  4. 須要支持多種主題,在不一樣的項目中使用不一樣的主題樣式。
  5. 做爲組件,咱得知足最小功能原則,即:一個組件只幹一件具體的事情。

   根據以上預期,能夠分析出實現思路以下:小程序

  1. 因爲須要支持滾動效果,因此wxml中可使用現成的scroll-view組件去實現。
  2. 因爲內部是自適應,因此不能把寬度寫死。並且底部的「條塊」的長度也是自適應的。這是整個實現過程的難點,我先劇透一下,這裏須要使用小程序提供的dom操做相關api。不熟悉的同窗請點這裏
  3. 這一點很簡單,就是要時刻提醒本身,沒必要開放的就不要多此一舉的去寫接口了。
  4. 主題切換無非就是css樣式的變化。因爲小程序不支持動態插入和操做dom(最多讓你獲取一下dom的屬性),因此主題的變化不能設計wxml結構的變化。這裏咱們只能笨重的使用wx:if指令去顯示和隱藏某些元素了,不過本次教程不涉及這個。
  5. 要知足第五點,就只能作tab欄的切換相關東西了,不要把tab欄下面的切換相關的功能也作了。若是你作了,那麼它的壞處顯而易見。首先是組件會變得更復雜(代碼層面),其次使用起來會很是侷限(你怎麼不把一個頁面做爲一個組件吶,我看你怎麼用)。

  這些分析是有必要的,它將爲咱們後面的一些工做其指導做用,防止咱們在編碼的過程當中迷失自我。下面先從wxml的編寫開始。api

2.wxml文件的編寫

  一下是咱們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變量來實現咱們看到的動畫效果。

3.wxss文件的編寫

  因爲咱們大部分樣式都是動態的,因此必須在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了,有興趣的同窗能夠看下個人這篇博客

4.js文件的編寫

  重頭戲來了。首先來看一下完整的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查看源代碼

3、結論

  雖然這篇博文是以教程的形式寫的,可是咱們仍是有必要總結一下。

  在寫程序的時候思想要走在編碼的前列,不要讓思想被具體代碼牽着鼻子走。要有必定的封裝思想,雖然ctrl+c,ctrl+v大法能夠解決一切問題,可是這樣的代碼是沒法維護和閱讀的。既然封裝,那就得考慮擴展性和閉開原則了。哪裏開放,哪裏閉合內心要有點逼數。可不能夠擴展將影響到後續的修改。當一個極具挑戰的東西須要咱們實現的時候,只須要抓住重點,分步展開,就會發現問題就變得簡單起來了。若是須要的步數太多,那也許是你簡單問題複雜化了。

4、寫在最後

  若是你懶得寫,也能夠嘗試一下使用博主封裝的小程序UI組件庫,裏面包含了開發中經常使用的組件。但願各位老鐵多多提意見,也能夠提交本身的組件。打了這麼多字,你就不心疼一下博主?

  GitHub地址>>

  掃描小程序碼,可查看效果。

  

相關文章
相關標籤/搜索