抖音的底部导航栏中间有一个造型突出的直播按钮,进入页面时它会带有一圈一圈向外扩散的脉冲光圈,按钮本体还有规律地做缩放呼吸,视觉上非常有辨识度。这种效果在微信小程序里用原生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数组里依然要照常配置各页面的路径和文字,因为微信需要靠这份配置来识别哪些页面属于tab页,只是渲染工作会交给你的自定义组件。
基础配置代码如下:
{
"tabBar": {
"custom": true,
"color": "#999999",
"selectedColor": "#ffffff",
"backgroundColor": "#111111",
"list": [
{ "pagePath": "pages/index/index", "text": "首页" },
{ "pagePath": "pages/discover/discover", "text": "发现" },
{ "pagePath": "pages/live/live", "text": "直播" },
{ "pagePath": "pages/msg/msg", "text": "消息" },
{ "pagePath": "pages/mine/mine", "text": "我的" }
]
}
}接着在custom-tab-bar/index.wxml中编写布局。仿抖音的结构通常是五个导航项,中间第三个做成放大的圆形按钮,其余四个是普通图标加文字。中间按钮要凸出于tabbar顶部,做法是把tabbar整体设为相对定位容器,中间按钮用绝对定位,top值设成负值让它向上突出,同时配合z-index保证它盖在其他元素之上。需要注意的是,tabbar页面中如果使用了原生组件,凸出部分可能被遮挡,所以中间按钮区域建议全部用普通view绘制。
<view class="tab-bar">
<view wx:for="{{list}}" wx:key="pagePath" class="tab-item" data-index="{{index}}" bindtap="switchTab">
<!-- 中间按钮 -->
<block wx:if="{{index === 2}}">
<view class="pulse-wrap">
<view class="pulse-ring pulse-ring-1"></view>
<view class="pulse-ring pulse-ring-2"></view>
<view class="live-btn">
<view class="live-inner">直播</view>
</view>
</view>
</block>
<!-- 普通按钮 --&;
<block wx:else>
<image src="{{selected === index ? item.selectedIcon : item.icon}}" class="tab-icon"/>
<text class="tab-text {{selected === index ? 'active' : ''}}">{{item.text}}</text>
</block>
</view>
</view>js部分的核心是维护一个selected变量记录当前选中的tab,在switchTab方法里调用wx.switchTab切换页面。此外每个tab页面的onShow生命周期里都要调用getTabBar实例并把selected更新为当前页面对应的下标,否则切换页面后高亮状态不会跟着变。
Component({
data: {
selected: 0,
list: [
{ pagePath: "/pages/index/index", icon: "/images/home.png", selectedIcon: "/images/home-active.png", text: "首页" },
{ pagePath: "/pages/discover/discover", icon: "/images/find.png", selectedIcon: "/images/find-active.png", text: "发现" },
{ pagePath: "/pages/live/live", text: "直播" },
{ pagePath: "/pages/msg/msg", icon: "/images/msg.png", selectedIcon: "/images/msg-active.png", text: "消息" },
{ pagePath: "/pages/mine/mine", icon: "/images/mine.png", selectedIcon: "/images/mine-active.png", text: "我的" }
]
},
methods: {
switchTab(e) {
const index = e.currentTarget.dataset.index;
const path = this.data.list[index].pagePath;
wx.switchTab({ url: path });
this.setData({ selected: Number(index) });
}
}
});二、脉冲扩散动画的实现原理与代码
脉冲动画指的是按钮周围有一圈光晕不断向外扩散并淡出,就像水面涟漪一样一圈接一圈。实现思路是放置两个或三个圆形的view作为光环,初始状态与按钮同样大小、完全透明,然后通过CSS的keyframes动画让它们同时做放大和透明度变化。使用两个光环并让第二个延迟一半的周期启动,就能形成连续不断的涟漪效果。
关键点在于光环的定位方式。光环和按钮必须共用同一个中心点,所以外层套一个pulse-wrap容器,设为相对定位并固定宽高,内部所有子元素都用绝对定位铺满容器,再通过border-radius: 50%画成正圆。动画中scale从1放大到1.8左右,opacity从0.6降到0,两个属性写在同一个keyframes里保证同步。
.pulse-wrap {
position: relative;
width: 96rpx;
height: 96rpx;
margin-top: -40rpx; /* 让按钮凸出于tabbar */
}
.pulse-ring {
position: absolute;
left: 0;
top: 0;
width: 96rpx;
height: 96rpx;
border-radius: 50%;
background-color: rgba(252, 55, 82, 0.5);
animation: pulse 2.4s linear infinite;
/* 提升层级避免被后续元素遮挡 */
z-index: 1;
}
.pulse-ring-2 {
animation-delay: 1.2s; /* 第二个光环延迟半个周期,形成交错涟漪 */
}
@keyframes pulse {
0% {
transform: scale(1);
opacity: 0.6;
}
100% {
transform: scale(2);
opacity: 0;
}
}有几点实战经验值得注意。第一,光环的放大倍数不宜过大,scale到2以上时会显得松散无力,一般1.8到2之间最接近抖音的手感。第二,如果想让光环是空心的描边效果而不是实心色块,可以把background-color去掉,改用border: 3rpx solid rgba加透明度,扩散时会有更强的科技感。第三,动画的timing-function用linear比ease更合适,因为涟漪扩散本身是匀速的物理过程,加了缓动反而显得不自然。
三、呼吸缩放动画与按钮本体的绘制
呼吸动画是指按钮本体按固定节奏轻微放大再缩回,营造一种“活着”的感觉。它和脉冲动画互相配合:脉冲负责向外的扩散氛围,呼吸负责按钮自身的节奏感。呼吸动画的实现比脉冲更简单,只需要对按钮主体做一个scale在1到1.06之间往复的动画,周期设置在1.5秒到2秒之间比较舒服,太快会显得躁动,太慢则没有存在感。
.live-btn {
position: absolute;
left: 8rpx;
top: 8rpx;
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: linear-gradient(45deg, #fc3755, #ff7a45);
box-shadow: 0 6rpx 16rpx rgba(252, 55, 82, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 2; /* 按钮本体要盖在光环之上 */
animation: breath 1.8s ease-in-out infinite;
}
.live-inner {
color: #ffffff;
font-size: 26rpx;
font-weight: bold;
}
@keyframes breath {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.08);
}
}这里有个容易踩的坑:呼吸动画的timing-function必须用ease-in-out这类缓动函数,让放大和缩小的过渡都是柔和的。如果用了linear,动画在往返切换的瞬间会有明显的机械感。另外呼吸的幅度要克制,1.08基本是上限,超过1.1之后按钮文字会跟着明显晃动,影响可读性。
按钮的颜色建议使用渐变而不是纯色,linear-gradient从玫红到橙红的过渡最接近抖音的视觉风格,再配合一层低透明度的box-shadow投影,能让按钮看起来悬浮在tabbar之上。中间文字如果是“直播”两个字,字号要控制在26rpx以内,否则小屏幕上会溢出圆形边界。
四、适配细节与常见问题的处理
第一个问题是底部安全区适配。从iPhone X开始,全面屏机型底部都有一条黑色 Home 条,如果tabbar高度写死,内容会被这条横条压住。解决办法是通过wx.getWindowInfo获取safeArea信息,动态计算底部需要额外留出的高度,把结果设置到tabbar容器的padding-bottom上。
Component({
lifetimes: {
attached() {
const winInfo = wx.getWindowInfo();
const safeBottom = winInfo.screenHeight - winInfo.safeArea.bottom;
this.setData({ safeBottom: safeBottom > 0 ? safeBottom : 0 });
}
}
});第二个问题是动画层级。脉冲光环在放大会超出tabbar容器范围,如果tabbar容器设置了overflow: hidden或者背景色不透明,光环会被裁切或者被背景挡住。正确的做法是tabbar背景层和内容层分开写,背景层不设overflow限制,光环的z-index低于按钮但高于背景,这样扩散出去的光圈才能完整显示在页面上。
第三个问题是页面切换时动画是否会中断。CSS的infinite动画在组件持续存在的情况下不会中断,但由于每个tab页共享同一个custom-tab-bar实例,切换页面不会销毁组件,动画可以自然延续。但如果你的tabbar里用了wx:if控制中间按钮的显隐,切换回来时动画会从头开始,建议改用opacity控制显隐,可以让动画衔接更自然。
最后提醒一点,自定义tabbar在不同基础库版本上的表现略有差异,尤其是动画性能方面。低端机型上如果脉冲光环出现掉帧,可以尝试把光环数量从两个减到一个,或者用will-change: transform提前告知渲染层做合成加速,这两种方式都能明显改善流畅度。按照以上步骤实现下来,一个带脉冲光圈和呼吸节奏的仿抖音直播按钮就可以稳定运行在你的小程序里了。