微信小程序原生tabbar只能支持简单的图标加文字切换,一旦产品需求里出现"中间凸起的圆形发布按钮"、"点击时图标旋转"或者"凸起按钮周围弹出子菜单"这类设计稿,原生tabbar就无能为力了。好在小程序官方提供了自定义tabbar机制,允许开发者在custom-tab-bar目录下完全接管底部导航的渲染和交互。本文就带大家完整实现一个带中间凸起按钮和点击旋转动画的自定义tabbar,并附上可直接运行的代码。
一、开启自定义tabbar的配置基础
首先要明确,自定义tabbar不是简单地在每个页面上写一个组件,而是使用小程序官方预留的特殊目录。在小程序项目根目录下新建一个名为custom-tab-bar的文件夹,注意目录名必须完全一致,否则小程序无法识别。在这个目录下创建index.js、index.json、index.wxml、index.wxss四个文件,结构上和一个普通自定义组件完全相同。
接着修改app.json,在tabBar配置项中增加"custom": true,同时保留list数组的声明。这一点非常关键:即使你完全接管了渲染,list中的页面路径仍然是小程序识别tab页的依据,pagePath、text等字段不能删除,否则switchTab接口会失效。示例配置如下:
{
"tabBar": {
"custom": true,
"color": "#999999",
"selectedColor": "#1989fa",
"backgroundColor": "#ffffff",
"list": [
{ "pagePath": "pages/index/index", "text": "首页" },
{ "pagePath": "pages/publish/publish", "text": "发布" },
{ "pagePath": "pages/mine/mine", "text": "我的" }
]
}
}还需要注意,index.json中必须声明"component": true,表示这是一个组件。另外,如果项目使用了分包,custom-tab-bar目录必须放在主包根目录下,放到分包里是找不到的。配置完成后编译,原生tabbar就会消失,取而代之的是你自己的组件,只是此时还是空白,需要继续往下写界面。
二、中间凸起按钮的布局实现
凸起按钮的核心是让一个圆形按钮在视觉上"超出"tabbar顶部边界。小程序中overflow默认不会裁剪子元素,所以只要让按钮使用绝对定位,负的top值把它顶出去即可。tabbar整体高度一般设为100rpx左右,再加上底部安全区高度(适配iPhone X以后的机型),凸起按钮直径约110rpx,通过bottom: 30rpx; margin: auto; left: 0; right: 0;实现水平居中悬浮。
wxml结构可以这样组织,三个tab项横向排列,中间的发布按钮单独用绝对定位:
<view class="tab-bar">
<view class="tab-item {{selected === 0 ? 'active' : ''}}" bindtap="switchTab" data-index="0" data-path="/pages/index/index">
<image src="/images/home{{selected === 0 ? '-active' : ''}}.png"/>
<text>首页</text>
</view>
<view class="center-btn {{rotating ? 'rotate' : ''}}" bindtap="onCenterTap">
<image class="center-icon" src="/images/publish.png"/>
</view>
<view class="tab-item {{selected === 2 ? 'active' : ''}}" bindtap="switchTab" data-index="2" data-path="/pages/mine/mine">
<image src="/images/mine{{selected === 2 ? '-active' : ''}}.png"/>
<text>我的</text>
</view>
</view>对应的wxss关键样式如下,重点看center-btn的绝对定位和圆角阴影:
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 100rpx;
padding-bottom: env(safe-area-inset-bottom);
background: #ffffff;
display: flex;
align-items: center;
justify-content: space-around;
border-top: 1rpx solid #eeeeee;
}
.center-btn {
position: absolute;
left: 50%;
transform: translateX(-50%);
top: -55rpx;
width: 110rpx;
height: 110rpx;
border-radius: 50%;
background: linear-gradient(135deg, #ff9a56, #ff5a3c);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 6rpx 20rpx rgba(255, 90, 60, 0.4);
z-index: 10;
}
.center-icon {
width: 56rpx;
height: 56rpx;
}这里有个细节:transform的translateX(-50%)会和后面的旋转动画共用transform属性,后面写旋转时要记得把它拼进去,否则旋转开始时按钮会突然偏移。解决方式有两种:一是外层view负责居中定位,内层view专门做旋转动画;二是旋转时写成transform: translateX(-50%) rotate(45deg)。推荐第一种,结构更清晰,互不干扰。
三、旋转动画的两种实现方式
1. CSS transition方案
最简单的旋转做法是给图标加一个class切换,配合transition实现平滑过渡。点击时图标旋转135度变成一个"加号变叉"的效果,再次点击旋转回来。这种方案纯CSS驱动,性能好,代码量少,适合状态简单切换的场景:
.center-icon {
transition: transform 0.4s ease;
}
.rotate {
transform: rotate(135deg);
}
// custom-tab-bar/index.js
Component({
data: {
selected: 0,
rotating: false
},
methods: {
onCenterTap() {
const next = !this.data.rotating;
this.setData({ rotating: next });
if (next) {
// 打开子菜单或跳转发布页
wx.navigateTo({ url: '/pages/publish/publish' });
}
}
}
});2. JS控制角度的连续旋转方案
如果需求是每点击一次累计旋转90度,而不是来回切换,纯CSS class切换就不方便了,因为每次移除class角度会归零。这时用data维护一个角度值,每次点击加90,动画依然交给transition完成:
Component({
data: {
selected: 0,
angle: 0
},
methods: {
onCenterTap() {
const angle = this.data.angle + 90;
this.setData({ angle });
setTimeout(() => {
wx.switchTab({ url: '/pages/publish/publish' });
}, 400); // 等动画播完再跳转
}
}
});wxml里用内联样式绑定角度:style="transform: rotate({{angle}}deg); transition: transform 0.4s;"。这种写法角度只增不减,动画始终朝一个方向旋转,视觉上更流畅。还可以进一步扩展成弹出子菜单:点击凸起按钮后,周围按弧形分布弹出三个小按钮,每个小按钮的位置用三角函数计算,配合scale和opacity的transition就能做出不错的扩散效果。
四、selected状态同步与常见坑
自定义tabbar最大的坑是每个tab页都会创建独立的tabbar组件实例,切换页面后组件会重新初始化,selected状态丢失。解决办法是在每个tab页的onShow里调用组件的setData方法同步索引:
// pages/index/index.js
onShow() {
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({ selected: 0 });
}
}每个tab页都要写一遍这段代码,只是selected的值不同。如果觉得繁琐,可以把这段逻辑抽成behaviors或者公共函数复用。
另外几个容易踩的点:第一,跳转tab页必须用wx.switchTab,不能用navigateTo,否则会出现页面叠加;第二,凸起按钮超出了tabbar区域,如果tabbar背景色和页面背景色不一致,注意按钮下缘的阴影在低端安卓机上可能被截断,可以适当加高tabbar容器;第三,封面组件的问题在自定义tabbar中已不存在,自定义tabbar本身就是原生组件层级,直接用view和image即可,不需要cover-view,但在video、map等原生组件上方仍需真机验证显示层级是否正常。
最后,如果产品要求"凸起按钮点击后原地旋转并弹出扇形菜单",可以把第二节的布局和第三节的连续旋转结合起来,用wx:if控制子菜单的渲染,配合CSS的transform-origin属性把旋转中心设置到凸起按钮圆心,就能实现菜单围绕按钮弧线展开的动效。整体方案在小程序基础库2.5.0以上版本均可运行,建议开发时用真机调试动画流畅度,模拟器上的动画表现和真机存在差异。