一、摘要

  tab栏(标签切换栏)是app中常见的一种交互方式,它可以承载更多的内容,同时又兼顾友好体验的优点。但在小程序中,官方并没有为咱们提供现成的组件。因此我们程序员展现才艺的时候到了(其实市面上的ui库也做了这个组件)。今天咱们就来实现一个对用户更加友好的tab栏,让用户“一点”就停不下来,起到解压的功效~~!

  废话不多说,先上效果图。

  不瞒您说,这东西我能点一天^^。言归正传,由于tab栏用的地方很多,所以需要封装成组件,因此没有开发或者没用过组件的同志请瞧一瞧官方文档。我之前也写过一篇组件开发的教程,有兴趣的可以点一下

二、正文

  为了照顾新手,我会一步步分析整个实现流程。不仅仅是分析代码,思想才是程序的灵魂,而一个程序员从初级进阶的过程也正是从代码到思想的转变。

1.预期与实现思路分析

  根据上面的效果图,咱们可以分析出一下几点预期:

  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的编写开始。

2.wxml文件的编写

  一下是我们wxml的基本骨架,最外层用scroll-view组件,内容部分再包一层view,这样有利于我们后面布局。

  1. <scroll-view>
  2. <view>
  3. 内容部分
  4. </view>
  5. </scroll-view>

  由于tab栏的项数是不固定的,而且需要组件外传入。所以我们使用wx:if指令完成每一项的渲染,而且组件外需要传入一个数组。编写后的代码如下。

  1. <scroll-view class='component'>
  2. <view class='content'>
  3. <view data-index="{{ index }}" wx:for="{{ items }}" wx:key="{{ index }}">
  4. <text class='text'>{{ item }}</text>
  5. </view>
  6. </view>
  7. </scroll-view>

  相信这一步只要有小程序开发基础的都能看懂,我顺便为所有的结点加上了类名,后面写样式需要用到。注意:组件中不推荐使用标签及子类选择器,所有在需要写样式的结点上都加上类名,官方推荐使用类选择器。这一步循环后需要加上 wx:key="{{ index }} 以及 data-index="{{ index }}" 。因为我们的程序需要明确知道切换的每一项,并且在切换到不同项的时候做出相应的操作,不定义一个自定义数据index,后面的工作无法展开。

  这样tab栏的主体wxml就写完了,不过我们好像还少了个底部“条块”的代码。其实当初我也是觉得底部“条块”用 border-bottom:1px solid #666 之类的css样式实现不就可以了吗?其实认真观察就会发现,底部“条块”是带动画效果的,并不是一切换就里马到文字下方,如果是这样我们大可给text或者view设置一个底部边框,这样一来我们的教程就结束了。所有为了实现动画效果,我们需要单独给个view去作为这个“条块”,并且在css中给它添加动画效果。

  这里打个岔子,因为在编写组件的过程中,很多样式代码都不能在wxss文件中写死,这样组件就毫无扩展性可言,就是去了组件的意义。那么怎么把样式给写活呐(又不能在wxss中写逻辑代码)?实现方式有两种:1.通过动态改变元素的class;2.通过动态改变元素的style属性。为了更精细的控制样式,我们这里采用第二种方式(这样写会让dom渲染时间增加)。

  下面是wxml文件的完整形态。

  1. <scroll-view class='component cus' scroll-x="{{ isScroll }}" style='{{ scrollStyle }}'>
  2. <view class='content'>
  3. <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' >
  4. <text class='text' style='color: {{ mSelected == index ? selectColor : textColor }};font-size: {{ textSize }}rpx;'>{{ item }}</text>
  5. </view>
  6. <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>
  7. </view>
  8. </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中的代码就很少,只需要写一些静态的样式。一下是完整代码,由于比较简单,就不过多的解释了。

  1. .component {
  2. background-color: white;
  3. white-space: nowrap;
  4. box-sizing: border-box;
  5. }
  6. .content {
  7. position: relative;
  8. }
  9. .item {
  10. display: inline-flex;
  11. align-items: center;
  12. justify-content: center;
  13. padding: 0 30rpx;
  14. }
  15. .text {
  16. transition: color 0.2s
  17. }
  18. .bottom-bar {
  19. position: absolute;
  20. height: 2px;
  21. border-radius: 2px
  22. }
  23. .small {
  24. height: 4px;
  25. border-radius: 2px;
  26. }

  需要注意的是,底部“条块”使用了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字段。

  1. items: {
  2. type: Array,
  3. value: ['item1', 'item2', 'item3', 'item4'],
  4. observer: function (newVal) {
  5. if (newVal && newVal.length < 5) {
  6. this.setData({
  7. itemWidth: (750 / newVal.length) - 60
  8. })
  9. }
  10. }
  11. },

  我们把该字段的类型定义为了数组,因此组件外需要传入一个数组。在外界没有传入任何数值的情况下我们也要显示一个完整的tab栏啊,所以默认值是有必要的,尽管使用的时候一定会覆盖我们的默认值。 observer 这个属性用得可能不是很多,大家可能有些陌生。仔细看过官方文档的同学应该知道,该属性用于当items字段在组件外被赋值或者被改变的情况下触发回调函数,其中回调函数可以接受newVal这样的新值,也可以接受oldVal这样的老值。我们需要根据传入的数组动态的设置每一项的宽度,在讲解wxml的时候我们知道 itemWidth 变量是用来控制每一项的宽度的。这里用if判断当数组长度小于5时就会设置每一项的宽度,而这个宽度就是通过750除以数组长度来的,最后我们还要减去每一项的左右padding,因为padding是不计入宽度的。这样以来,当数组的元素个数低于五个的时候,tab组件就会将屏幕宽度等分,这样就不会出现滚动效果。当数组的元素个数超过5,那么我们就给一个默认值,当然我们在wxml中设置的是 min-width 属性,所以不用担心设置了宽度就会造成宽度不自适应的情况。

  因为底部“条块”需要知道当前选项的位置,这样才能滚动到选中项的下面。所以要实现这个效果,以及当前处于第几项以及该项的位置。小程序虽然不支持dom操作,但支持获取dom属性。

  1. lifetimes: {
  2. ready: function () {
  3. let that = this;
  4. const sysInfo = wx.getSystemInfoSync();
  5. this.setData({
  6. windowWidth: sysInfo.screenWidth
  7. })
  8. const query = this.createSelectorQuery();
  9. query.in(this).selectAll('.item').fields({
  10. dataset: true,
  11. rect: true,
  12. size: true
  13. }, function (res) {
  14. that.setData({
  15. domData: res,
  16. })
  17. that.barLeft(that.data.mSelected, that.data.domData);
  18. that.barRight(that.data.mSelected, that.data.domData);
  19. // console.log(res)
  20. }).exec()
  21. query.in(this).selectAll('.text').fields({
  22. dataset: true,
  23. rect: true,
  24. size: true
  25. }, function (res) {
  26. that.setData({
  27. textDomData: res,
  28. })
  29. if (that.data.theme == themes.smallBar) {
  30. that.barLeft(that.data.mSelected, that.data.textDomData);
  31. that.barRight(that.data.mSelected, that.data.textDomData);
  32. }
  33. console.log(res)
  34. }).exec()
  35. },
  36. },

  这段代码是在ready生命周期中进行的,因为只有组件在ready这个生命周期,我们才能获取dom。这个生命周期是在dom渲染完毕后执行的。首先我们通过 wx.getSystemInfoSync() 获取系统的信息,里面包括我们需要的屏幕宽度。注意整个计算过程都是使用px作为单位,虽然我们知道每个设备的宽度固定为750rpx,但是px是不固定的。之后我们通过 this.createSelectorQuery(); 来查询需要的dom结点(类似与jQuery)。首先查询类名为item的所有元素,并且将数据保存到domData变量。由于在smallBar主题下,我们是根据文字宽度来定位底部“条块”的,所有还需要获取类名为text的所有结点信息,并将其保存到textDomData变量中。下面我们来看下获取的dom数据的结构。

  其中left正是该元素在父组件中距离父组件最左边的距离以px为单位。对我们有用的就是left和right两字段,这意味着我们知道了每一项的具体定位。至于当前的选项我们则通过点击事件来获取。下面是整个组件的核心代码。

  1. methods: {
  2. barLeft: function(index, dom) {
  3. let that = this;
  4. this.setData({
  5. left: dom[index].left
  6. })
  7. },
  8. barRight: function (index, dom) {
  9. let that = this;
  10. this.setData({
  11. right: that.data.windowWidth - dom[index].right,
  12. })
  13. },
  14. onItemTap: function(e) {
  15. const index = e.currentTarget.dataset.index;
  16. let str = this.data.lastIndex < index ? 'left 0.5s, right 0.2s' : 'left 0.2s, right 0.5s';
  17. this.setData({
  18. transition: str,
  19. lastIndex: index,
  20. mSelected: index
  21. })
  22. if (this.data.theme == themes.smallBar) {
  23. this.barLeft(index, this.data.textDomData);
  24. this.barRight(index, this.data.textDomData);
  25. } else {
  26. this.barLeft(index, this.data.domData);
  27. this.barRight(index, this.data.domData);
  28. }
  29. this.triggerEvent('itemtap', e, { bubbles: true });
  30. }
  31. },

  这里定义了三个函数,其中 barLeft 和 barRight 分别完成设置底部“条块”的left值和right值。需要特别说明一下,只要我们动态计算并设置了底部“条块”的left和right属性,那么底部“条块”的位置大小在水平方向上就以及确定,而垂直方向上的位置大小都是固定写死在css文件中的。这两个函数都需要传入当前选项的索引以及所有选项dom的位置信息。

   onItemTap 方法绑定了每一项的点击事件,可以查看wxml中的完整代码。当选项被点击后,它的索引可通过 e.currentTarget.dataset.index 获取,因为我们在wxml中定义了一个自定义属性。

  至此我们的核心逻辑就实现完毕了,关键点在于获取所有选项的位置信息以及当前选项的索引。有兴趣的同学可以前往github查看源代码

三、结论

  虽然这篇博文是以教程的形式写的,但是我们还是有必要总结一下。

  在写程序的时候思想要走在编码的前列,不要让思想被具体代码牵着鼻子走。要有一定的封装思想,虽然ctrl+c,ctrl+v大法可以解决一切问题,但是这样的代码是无法维护和阅读的。既然封装,那就得考虑扩展性和闭开原则了。哪里开放,哪里闭合心里要有点逼数。可不可以扩展将影响到后续的修改。当一个极具挑战的东西需要我们实现的时候,只需要抓住重点,分步展开,就会发现问题就变得简单起来了。如果需要的步数太多,那也许是你简单问题复杂化了。

四、写在最后

  如果你懒得写,也可以尝试一下使用博主封装的小程序UI组件库,里面包含了开发中常用的组件。希望各位老铁多多提意见,也可以提交自己的组件。打了这么多字,你就不心疼一下博主?

  GitHub地址>>

  扫描小程序码,可查看效果。

  

微信小程序-tab标签栏实现教程的更多相关文章

  1. 微信小程序Tab选项卡切换大集合

    代码地址如下:http://www.demodashi.com/demo/14028.html 一.前期准备工作 软件环境:微信开发者工具 官方下载地址:https://mp.weixin.qq.co ...

  2. 微信小程序开发语音识别文字教程

    微信小程序开发语音识别文字教程 现在后台 添加插件 微信同声传译 然后app.json 加入插件 "plugins": { "WechatSI": { &quo ...

  3. 关于微信小程序前端Canvas组件教程

    关于微信小程序前端Canvas组件教程 微信小程序Canvas接口函数 ​ 上述为微信小程序Canvas的内部接口,通过熟练使用Canvas,即可画出较为美观的前端页面.下面是使用微信小程序画图的一些 ...

  4. 如何找回微信小程序源码?2020年微信小程序反编译最新教程 小宇子李

    前言:在网上看了找回微信小程序源码很多教程,都没法正常使用.微信版本升级后,会遇到各种报错, 以及无法获取到wxss的问题.查阅各种资料,最终解决,于是贴上完整的微信小程序反编译方案与教程. 本文章仅 ...

  5. 完整微信小程序授权登录页面教程

    完整微信小程序授权登录页面教程 1.前言 微信官方对getUserInfo接口做了修改,授权窗口无法直接弹出,而取而代之是需要创建一个button,将其open-type属性绑定getUseInfo方 ...

  6. 微信小程序-云开发实战教程

    微信小程序-云开发实战教程 云函数,云存储,云数据库,云调用 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/basis/gettin ...

  7. 微信小程序(应用号)开发教程

    本文档将带你一步步创建完成一个微信小程序,并可以在手机上体验该小程序的实际效果.这个小程序的首页将会显示欢迎语以及当前用户的微信头像,点击头像,可以在新开的页面中查看当前小程序的启动日志.下载源码 1 ...

  8. hello-weapp 微信小程序最简示例教程

    打开微信小程序官方开发文档,最好全篇看一遍,基本上就会了. 点击文档中 工具 选项卡中 下载工具页面 下载对应系统版本的微信开发者工具 注意:脱离微信开发者工具是不能调试的 好了,安装下工具即可打开, ...

  9. 微信小程序tab切换,可滑动切换,导航栏跟随滚动实现

    简介 看到今日头条小程序页面可以滑动切换,而且tab导航条也会跟着滚动,点击tab导航,页面滑动,切导航栏也会跟着滚动,就想着要怎么实现这个功能 像商城类商品类目如果做成左右滑动切换类目用户体验应该会 ...

随机推荐

  1. Delphi IOS开发环境安装

    RAD Delphi XE/10 Seattle 安装IOS.OSX环境安装,IOS模拟器,MAC X 真机可以调试 http://community.embarcadero.com/blogs/en ...

  2. .net 委托的简化语法

    1. 不需要构造委托对象 ThreadPool.QueueUserWorkItem:通过线程池 public static void WorkItem() { ThreadPool.QueueUser ...

  3. MVC4 Model ControllerDescriptor

    1. ControllerDescriptor 的描述 Controller  的Action 方法有以下一些特性: 1.1 ActionNameAttribute特性  他继承自 System.We ...

  4. [LINK]List of .NET Dependency Injection Containers (IOC)

    http://www.hanselman.com/blog/ListOfNETDependencyInjectionContainersIOC.aspx I'm trying to expand my ...

  5. NLP常用开源/免费工具

    一些常见的NLP任务的开源/免费工具, *Computational Linguistics ToolboxCLT http://complingone.georgetown.edu/~linguis ...

  6. Win RT Webview获取cookie

    方法1: HttpBaseProtocolFilter filter = new HttpBaseProtocolFilter(); var cookis = filter.CookieManager ...

  7. Spreadsheet 常用属性

    标题栏是否可见 Spreadsheet1.TitleBar.Visible=true 标题栏背景颜色 Spreadsheet1.TitleBar.Interior.Color="Green& ...

  8. MongoDB基础知识记录

    MongoDB基础知识记录 一.概念: 讲mongdb就必须提一下nosql,因为mongdb是nosql的代表作: NoSQL(Not Only SQL ),意即“不仅仅是SQL” ,指的是非关系型 ...

  9. 514. Freedom Trail

    In the video game Fallout 4, the quest "Road to Freedom" requires players to reach a metal ...

  10. JQuery实现全选、反选和取消功能

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...