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组件库,里面包含了开发中经常使用的组件。但愿各位老铁多多提意见,也能够提交本身的组件。打了这么多字,你就不心疼一下博主?
扫描小程序码,可查看效果。