前面已经讲了微信小程序底部导航栏的使用,也涉及了简单的多用户登录问题,不过底部标签页最多五个的限制真是太头疼了,这就不得不再使用其他的办法自定义底部导航栏了,大致有两种思路。
自定义底部导航栏的两种思路
- 将导航作为组件
这里的意思大概就是在每个界面都放一个底部导航栏,不同页面选中自己的标签,当点击其他标签的时候,通过跳转前往其他页面。因为每个页面都有底部导航栏,看起来底部导航栏好像是没变一样,但是通过跳转实现肯定是有一定卡顿的,特别是还会有页面闪烁。
所以这里不推荐使用这种方法,一是有页面闪烁,二是太笨不够优雅。
将页面作为组件
微信官方是支持使用 component 构建页面的,作为一个安卓开发者,完全可以将 component 当成 fragment 来使用,所以这个方法用起来还是很熟悉的。
https://developers.weixin.qq.com/miniprogram/dev/reference/api/Component.html
这里需要一个主界面用来放底部导航栏以及页面容器,底部导航栏用来切换容器内页面,页面容器里用来存放和底部导航栏标签对应的页面。虽然小程序没有安卓那样的 FramLayout,但是使用 wx:if 以及 display:none 还是可以凑成差不多的效果。
废话不多说,下面就详细介绍第二种思路的具体实现。
先看显示效果
这里将页面分成了两部分,底部导航栏部分以及中间容器部分
创建主界面
这里我直接使用 index 页面作为主页面,主页面功能就是放底部导航栏和中间页面的,需要在 app.json 中注册,但是不需要放在 pages 里面的第一个。
主界面代码
- WXML
<!--pages/tabbar/index.wxml-->
<view class="page-container" style="height:{{containerHeight}}rpx">
<!-- 使用wx:if 不会渲染,每次点击会重走组件生命周期 -->
<!-- 使用display:none隐藏,仍然会渲染组件,只走一遍生命周期 -->
<!-- 使用hidden隐藏,和display:none一样,但是受限于块级元素,无法使用 -->
<!-- 用户 -->
<block wx:if="{{roleType=='user'}}">
<home id="id0" indexpage="{{true}}" style="display: {{currentPageIndex==0?'block':'none'}};" />
<user id="id1" show='{{currentPageIndex==1}}' style="display: {{currentPageIndex==1?'block':'none'}};" />
</block>
<!-- 管理员 -->
<block wx:elif="{{roleType=='manager'}}">
<manager id="id0" style="display: {{currentPageIndex==0?'block':'none'}};" />
<log id="id1" style="display: {{currentPageIndex==1?'block':'none'}};" />
</block>
</view>
<!-- 底部切换菜单 -->
<view class="row-center tab-container" style="height:{{104 + bottomHeight}}rpx;padding-bottom:{{bottomHeight}}rpx">
<view wx:for="{{tabBar}}" wx:key="index" class="flex1 center-both col"
style="color:{{index==currentPageIndex?'#0B7BFBFF':'#616161FF'}}" data-index="{{index}}" bindtap="onSelectTap">
<view class="{{item.iconClass}} icon"></view>
<view class="text">{{item.text}}</view>
</view>
</view>
这里和我们看到的页面样式一致,就是两部分内容。
页面容器里面,通过 wx:if 来渲染不同角色页面,只有本角色的页面才会被渲染,其他角色页面不会被渲染;而通过 display: none 来隐藏页面,不会触发页面的重新生成,保证页面的唯一性。通过两者的搭配,还是很合理的。
这里的 indexpage 和 show 是组件的两个属性,后续可能会用到,一个是标识页面在主界面被使用,一个可以使组件感知到页面的切换,即当该界面显示的时候 show 的值会被设置为 true,组件页面通过监听 show 值,可以在显示的时候做一些工作。
底部导航栏没什么说的,就是一个列表横向渲染,选中的标签会被设置成不同颜色。这里用到了 iconfont,实际上把 icon 当成文字来使用,能把文字和 icon 统一设置样式,还是很不错的。
至于 containerHeight 和 bottomHeight 这两个值是用来适配用的,后面详细讲解。主要就是确定了容器高度和底部导航栏的高度,这里底部还有一个安全高度,空出来就好。
- WXSS
/* pages/tabbar/index.wxss */
.row-center{
display:flex;
flex-direction:row;
align-items: center;
}
.center-both{
display:flex;
justify-content: center;
align-items: center;
}
.page-container{
position: absolute;
width: 100%;
box-sizing: border-box;
overflow: scroll;
}
.tab-container{
height: 104rpx;
width: 100%;
position: fixed;
bottom: 0rpx;
border-top: 2rpx solid #E5E5E5FF;
box-sizing: border-box;
background: #FFFFFFFF;
overflow: hidden;
}
.active{
color: #0B7BFBFF;
}
.unactive{
color: #616161FF;
}
.icon{
font-size: 40rpx;
}
.text{
font-size: 22rpx;
font-family: PingFangSC-Regular, PingFang SC;
font-weight: 400;
line-height: 32rpx;
}
这里稍微注意下 104 这个值是我们底部导航栏的高度,而且是 rpx 值。
- JSON
"usingComponents": {
"home": "../home/home",
"user": "../user/user",
"manager": "../manager/manager",
"log": "../log/log",
},
这里引入中间容器的页面,页面作为组件引入,页面的路径写成了相对路径。
- Page.js
// pages/index/index.js
var app = getApp()
Page({
data: {
//底部高度
bottomHeight: app.globalData.bottomHeight,
//中间内容容器高度 = 屏幕高度 - 底部安全高度 - 底部导航栏高度
containerHeight: app.globalData.screenHeight - app.globalData.bottomHeight - 104,
currentPageIndex: 0,
roleType: "",
tabBar: [],
userTabBar: [{
"iconClass": "iconfont home",
"text": "首页",
},
{
"iconClass": "iconfont user",
"text": "个人中心",
},
],
managerTabBar: [{
"iconClass": "iconfont manager",
"text": "管理",
},
{
"iconClass": "iconfont log",
"text": "日志",
},
]
},
onLoad: function (options) {
let tabBar = this.data.tabBar
if (options.roleType == 'user') {
tabBar = this.data.userTabBar
} else if (options.roleType == 'manager') {
tabBar = this.data.managerTabBar
}
// 在此加载数据
this.setData({
tabBar: tabBar,
roleType: options.roleType,
})
},
// 下拉加载
onPullDownRefresh() {
this.selectComponent("#id" + this.data.currentPageIndex).onPullDownRefresh()
},
onSelectTap(event) {
let index = event.currentTarget.dataset.index;
//选中页面更新
this.setData({
currentPageIndex: index,
})
//标题更新
wx.setNavigationBarTitle({
title: this.data.tabBar[index].text,
})
},
})
这里和前面简单的多用户登录类似,只是规划了一个专门的主界面来控制导航页面,在页面加载的时候通过传参判断角色类型,加载不同的底部导航数据,并刷新页面,这时候就会把各个标签页面加载进来。切换也比较简单,主要就是更新 currentPageIndex 的值,刷新这个值会使页面切换,同时还要修改的就是标题的内容。
高度适配值
上面用到的 containerHeight 和 bottomHeight 这两个值是在 app 内配置的全局变量,也具体看看如何得到的吧,这里也把其他一些值写出来,后面博客可能会用到。
globalData: {
//系统信息
systemInfo: {},
//1px像素值 对应 rpx
pixelRatio1: 2,
//胶囊信息
menuInfo: {},
//屏幕高度
screenHeight: 2000,
//顶部高度 = 状态栏高度 + 导航栏高度
topHeight: 0,
//状态栏高度
statusHeight: 0,
//导航栏高度
naviHeight: 0,
//底部安全高度
bottomHeight: 0,
},
onLaunch: function () {
var that = this;
//获取设备信息
let systemInfo = wx.getSystemInfoSync()
that.globalData.systemInfo = systemInfo
//1rpx 像素值
let pixelRatio1 = 750 / systemInfo.windowWidth;
that.globalData.pixelRatio1 = pixelRatio1
//胶囊信息
let menu = wx.getMenuButtonBoundingClientRect()
that.globalData.menuInfo = menu
//状态栏高度
let statusHeight = systemInfo.statusBarHeight
that.globalData.statusHeight = statusHeight * pixelRatio1
//导航栏高度
let naviHeight = (menu.top - statusHeight) * 2 + menu.height
that.globalData.naviHeight = naviHeight * pixelRatio1
//顶部高度 = 状态栏高度 + 导航栏高度
that.globalData.topHeight = (statusHeight + naviHeight) * pixelRatio1
//屏幕高度
let screenHeight = systemInfo.screenHeight
that.globalData.screenHeight = screenHeight * pixelRatio1
//底部高度 = 屏幕高度 - 安全区域bottom
let bottom = systemInfo.safeArea.bottom
that.globalData.bottomHeight = (screenHeight - bottom) * pixelRatio1
}
这里导航栏高度要说明一下,具体看下图,用胶囊顶部绝对高度减去状态栏高度得到一个小差值,这个差值的两倍加上胶囊的高度就是导航栏的高度了,应该不难,自定义导航栏的时候还是可以用到的。
主界面到这就讲的差不多了,下面看看具体的中间组件界面。
- WXML
自己随便写,和 page 一样,没什么要求。
- WXSS
这里有问题了,一个是样式隔离问题,一个是选择器问题,选择器问题就是要遵守组件样式要求,具体看下面的官方文档(组件样式部分)。
https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/wxml-wxss.html
样式隔离问题,下面另开一节。
@import '/assets/fonts/iconfont.wxss';
...
这里记得重新引入 iconfont,不然无法使用,很奇怪啊,后面设置了可以使用全局 css 啊。。。
- JSON
{
"usingComponents": {},
"component": true
}
这里声明页面是组件,好像不写也可以,还是写上吧,不然得去 app.json 注册页面。
- JS
Component({
//解决样式隔离
options: {
styleIsolation: 'apply-shared'
},
// 属性值,可以和 data 一样在页面绑定
properties: {
// 在首页
indexpage: {
type: Boolean,
value: false,
},
show: {
type: Boolean,
value: false,
observer: function (newVal, oldVal) {
// 监听页面被显示
if(newVal) {
...
}
}
}
},
// 私有数据
data: {
},
lifetimes: {
attached: function () {
// 在这初始化数据
},
},
// 方法要写在里面
methods: {
},
})
这里其实可以不写成 Component 的,直接用Page,大家可以试试,问题就是写成 Page 的话初始方法到底写在哪呢?写在 onLoad 里面是不会生效的,写在 attached 里面好像 Page 不是 Component 吧,试过了也不会生效,所以除非是静态页面,还是乖乖用 Component 吧。
组件样式隔离
写在页面容器里面的页面,样式会互相冲突,所以写 WXSS 的时候尽量使用不同的命名,不然样式不对了,找起来真的头疼。在小程序组件中,自定义组件的样式默认只受到自定义组件 WXSS 的影响,不会受到全局的影响,下面几种方法可以突破这种限制:
- 在 app.wxss 或页面的 wxss 中使用标签名选择器直接指定样式
//app.wxss
text{
color:red;
}
- 指定特殊的样式隔离选项
styleIsolation
Component({
options: {
styleIsolation: 'isolated'
}
})
- 启用全局样式选项 addGlobalClass,等价于设置
styleIsolation: apply-shared
/* 组件 custom-component.js */
Component({
options: {
addGlobalClass: true,
}
})
styleIsolation
选项从基础库版本 2.6.5 开始支持。它支持以下取值:
- isolated 表示启用样式隔离,在自定义组件内外,使用 class 指定的样式将不会相互影响(一般情况下的默认值);
- apply-shared 表示页面 wxss 样式将影响到自定义组件,但自定义组件 wxss 中指定的样式不会影响页面;
- shared 表示页面 wxss 样式将影响到自定义组件,自定义组件 wxss 中指定的样式也会影响页面和其他设置了 apply-shared 或 shared 的自定义组件。(这个选项在插件中不可用。)
使用后两者时,请务必注意组件间样式的相互影响。
如果这个 Component 构造器用于构造页面 ,则默认值为 shared
,且还有以下几个额外的样式隔离选项可用:
- page-isolated 表示在这个页面禁用 app.wxss ,同时,页面的 wxss 不会影响到其他自定义组件;
- page-apply-shared 表示在这个页面禁用 app.wxss ,同时,页面 wxss 样式不会影响到其他自定义组件,但设为 shared 的自定义组件会影响到页面;
- page-shared 表示在这个页面禁用 app.wxss ,同时,页面 wxss 样式会影响到其他设为 apply-shared 或 shared 的自定义组件,也会受到设为 shared 的自定义组件的影响。
自动加载数据
在这里有个很傻的问题,就是在首页的自动加载的问题。我们这里将底部导航栏抽象在一起,把功能解耦开来了,如果中间的页面需要传值进去,那怎么办?如果只是在首页使用,那直接在 attached 中用默认值加载就行,但是如果我们的中间容器的页面如果在多个地方使用到的话,比如我在组件页面外包裹一层 page,使传递的参数能被组件页面使用,这时候自动加载就会触发一次无效的加载。
说的很复杂,看看具体代码吧。
在首页使用:
lifetimes: {
attached: function () {
//在首页要加载
if (this.properties.indexpage) {
// 请求初始数据
xx.request(-1)
...
}
},
},
在非首页通过传值加载数据:
<!-- 包裹page层wxml -->
<user config="config"></user>
// 包裹page层
Page({
onLoad: function (options) {
this.setData({
config: options.config
})
},
})
页面属性值监听
properties: {
config: {
type: Number,
value: -1,
observer: function (newVal, oldVal) {
// 切换传入值触发
if(newVal != -1) {
// 请求初始数据
xx.request(newVal)
...
}
}
}
},
如果不对是否在首页进行判断,那么不在首页时两个请求数据会同时触发,收到结果的顺序会有问题,造成错误,不太优雅。
下拉刷新
可以在 app.json 中设置全局下拉加载,也可以在 index.json 中加入下拉选项使页面能够下拉加载,这里最好把 backgroundTextStyle 设置成黑色,不然下拉的三个点看不到。
"window": {
...
"backgroundTextStyle": "dark",
"enablePullDownRefresh": true
},
在 index.js 中重写 onPullDownRefresh,选择对应的组件页面调用它的 onPullDownRefresh 方法
onPullDownRefresh() {
this.selectComponent("#id" + this.data.currentPageIndex).onPullDownRefresh()
},
在组件页面中实际是没有 onPullDownRefresh 方法的,我们可以按需增加这么一个方法,使之生效
onPullDownRefresh() {
this.setData({
currentPage: 1,
isLoadEnd: false,
isLoadComplete: false,
})
xx.request(...)
},
// 请求成功失败都应该关闭下拉
requestFinish() {
wx.stopPullDownRefresh();
}
结语
这样我们这个自定义底部导航栏的功能就挺完善了,下一节我们在这基础上再增加自定义导航栏,并加上沉浸状态栏的效果。
end
完美撒花