在微信小程序里,标准tabbar只能配置普通的图标加文字入口,无法把中间区域做成可展开的悬浮操作区。通过开启自定义tabbar,开发者能用组件完全接管底部导航的渲染,从而自由设计中间按钮的弹出菜单或弧形菜单。这类设计常出现在电商发布、工具速记等场景,用来强调主功能并收敛次要操作。

自定义tabbar的基础配置与组件结构
要实现中间按钮的特殊交互,第一步是在 app.json 中声明 "custom": true 并保留 tabBar 的 list 配置用于权限与路径校验。小程序会在运行时优先使用项目根目录下 custom-tab-bar 组件替代原生导航,因此我们必须自行处理选中态、页面切换与图标资源。很多新手容易漏掉 list 中至少两项、最多五项的限制,导致自定义组件虽然渲染了但底层路由校验失败。
custom-tab-bar 组件本质是一个 Component,它可以通过 this.getTabBar() 在页面中被获取,从而同步选中索引。在组件的 wxml 中,我们通常用 flex 布局平分五个占位,中间占位不渲染普通 item 而是放一个圆形按钮。需要注意,自定义tabbar 的层级默认较高,但在某些安卓机型上如果页面使用了 <video> 或 <map> 原生组件,仍可能出现遮挡,此时要配合 cover-view 做兼容。
下面是一段最基础的 custom-tab-bar 组件 js 结构,展示了如何声明中间按钮与切换逻辑:
Component({
data: {
selected: 0,
list: [
{ pagePath: '/index/index', text: '首页' },
{ pagePath: '/topic/topic', text: '话题' },
{ pagePath: '', text: '发布' },
{ pagePath: '/msg/msg', text: '消息' },
{ pagePath: '/mine/mine', text: '我的' }
]
},
methods: {
switchTab(e) {
const idx = e.currentTarget.dataset.index;
const path = this.data.list[idx].pagePath;
if (!path) {
this.triggerEvent('midtap');
return;
}
wx.switchTab({ url: path });
this.setData({ selected: idx });
}
}
});
中间按钮弹出菜单的实现思路与动画控制
弹出菜单指的是点击中间按钮后,从按钮位置向上或向两侧展开多个功能子项,常配合半透明遮罩阻止底层操作。实现时我们可以在组件内维护一个 showMenu 布尔值,通过 wx:if 或 hidden 控制菜单容器显示。动画方面推荐使用 CSS transition 配合 transform,比直接用 wx.createAnimation 更易维护,也能避免频繁调用 api 带来的性能开销。
遮罩层需要绑定 catchtap 来关闭菜单,并且要用 catchtouchmove 阻止滚动穿透。子项按钮建议使用 cover-view 与 cover-image,因为在带原生组件的页面里普通 view 会被盖住。弹出菜单的优点是结构清晰、可承载较多选项;缺点是若不做弧形排布会显得呆板,并且在小屏手机上容易挡住内容。下面的 wxml 片段演示了基础弹出结构:
<view class="tabbar">
<block wx:for="{{list}}" wx:key="index">
<view class="item" data-index="{{index}}" bindtap="switchTab">
<text>{{item.text}}</text>
</view>
</block>
<view class="mid-btn" bindtap="switchTab" data-index="2">+</view>
<view class="mask" wx:if="{{showMenu}}" catchtap="closeMenu" catchtouchmove="stopMove"></view>
<view class="pop-menu" wx:if="{{showMenu}}">
<view class="pop-item">写文章</view>
<view class="pop-item">拍视频</view>
</view>
</view>
在 js 中控制显隐与阻止滚动的方法非常简单,核心是让遮罩捕获事件而不冒泡到页面。实际项目中还应保存菜单打开前的页面状态,防止快速切换 tab 时菜单未关闭造成视觉残留。如果弹出菜单需要更顺滑的弹簧效果,可以在 wxss 中使用 transition: transform 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28); 来模拟物理回弹。
弧形菜单的坐标计算与布局技巧
弧形菜单是弹出菜单的进阶形态,子项沿着以中间按钮为圆心的圆弧排列,既美观又节省横向空间。计算方式是在点击时确定圆心 (cx, cy),然后对每个子项赋予角度 angle = start + i * step,利用 x = cx + r * cos(angle) 与 y = cy - r * sin(angle) 得到绝对定位坐标。由于小程序不支持在 wxml 直接写复杂数学,通常要在 js 里预先算好数组再 setData。
弧形半径 r 建议取屏幕宽度的 0.28 到 0.36 倍,角度区间控制在 180 度上半圆内,避免子项超出可视区。每个子项用 position: absolute 定位,并加上 opacity 与 scale 的进入动画。弧形菜单的劣势是点击热区为圆形,需额外扩大透明 padding 防止误触;另外在横屏或平板设备上要重新计算圆心,否则会偏移。以下代码展示了如何生成弧形坐标:
const radius = wx.getSystemInfoSync().windowWidth * 0.32;
const cx = wx.getSystemInfoSync().windowWidth / 2;
const cy = wx.getSystemInfoSync().windowHeight - 30;
const items = ['拍照', '录音', '扫码', '签到'];
const startAngle = Math.PI * 0.75;
const step = Math.PI * 0.5 / (items.length - 1);
const menu = items.map((txt, i) => {
const a = startAngle + step * i;
return {
text: txt,
x: cx + radius * Math.cos(a),
y: cy - radius * Math.sin(a)
};
});
this.setData({ arcMenu: menu, showArc: true });
在 wxml 中渲染弧形菜单时,要把算出的 x、y 通过 style 内联到子项上,同时用 transform: translate(-50%, -50%) 让子项以自身中心对齐坐标点。为了避免与系统底部安全区冲突,cy 的计算应减去 safeAreaInsets.bottom 等环境值。弧形菜单相比普通弹出更能引导用户视线聚拢到中间按钮,适合核心操作不超过五个的产品,但若子项带文字标签,还要注意圆弧上的文字方向可读性。
层级冲突与多页面状态同步方案
自定义tabbar 是单例组件,所有 tab 页共享同一个实例,这带来状态同步便利也引发脏数据风险。当我们在 A 页面打开了弧形菜单,切换到 B 页面时若未监听 onShow 主动关闭,菜单会悬浮在 B 页面造成困惑。推荐在页面 onShow 中调用 this.getTabBar().setData({ showMenu: false, showArc: false }),保证每次进入页面都是干净状态。
另外,tabbar 组件本身不支持直接在内部使用 wx.navigateTo 跳非 tab 页面来做发布功能,因为会脱离 tab 栈。中间按钮一般改为 wx.navigateTo 到普通页或使用 wx.showActionSheet 作为降级方案。对于 iOS 底部安全区,应在 wxss 使用 padding-bottom: env(safe-area-inset-bottom) 并配合 box-sizing: border-box,否则弧形菜单圆心会整体上移。处理好这些细节,自定义tabbar 的中间按钮才能真正兼顾弹出与弧形两种菜单体验。