uni-app 原生 tabBar 只能做图标 + 文字的平铺样式,而项目需要的是悬浮胶囊 + 滑动指示器 + 中央 + 按钮的毛玻璃效果。本文记录自定义 TabBar 的实现:状态管理、样式与动画、切换时机,以及 H5 与 App 两端的平台差异处理。
一、方案:保留原生 tabBar 配置,渲染交给组件
pages.json 中 tabBar 仍保留完整配置,只是加上 custom: true:
1 | "tabBar": { |
这样做的原因:switchTab、页面路由等能力仍依赖原生 tabBar 的 list 定义,custom: true 只是接管渲染。四个 tab 页各自引入 <glass-tabbar /> 组件,视觉层完全由组件实现。
TAB_LIST(图标、文案、路径)定义在 store/tabbar.js,顺序必须与 pages.json 的 tabBar.list 严格一致——原生层和组件层各持一份清单,靠约定对齐:
1 | // 与 pages.json tabBar.list 顺序保持一致 |
二、状态对齐:onShow 回填,不在点击时改写
指示器位置由 store 中的 activeIndex 驱动,而它的更新时机有一条明确规则:点击时不改写,由目标页 onShow 回填。
1 | // store/tabbar.js —— 各 tab 页 onShow 调用 |
组件侧点击只做一件事——立即切换路由:
1 | const onTab = (tab) => { |
这条规则是闪屏修复的产物。早期实现是点击后先改 activeTab、等 400ms 胶囊滑完再 switchTab,结果制造出”旧页面内容 + 新 Tab 高亮”的中间帧,路由切换瞬间又叠加首帧空窗。改为”立即切换、onShow 回填”后,高亮变化与页面切换落在同一帧附近。外部导航进入(如从详情页返回某个 tab)同样由 sync() 对齐,指示器不会指错。
sync 之外,store 还承担预热期间的就绪回报:各 Tab 页 onReady 时调 markReady(path),App 启动预热用 waitForReady(path, timeout) 等待页面首个 DOM 创建完成——这部分与闪屏修复共用同一状态。
三、毛玻璃样式与滑动指示器
结构分三层:底部胶囊条(glass-tabbar)、绝对定位的指示器(gt-indicator)、悬浮 + 按钮。
毛玻璃胶囊:
1 | .glass-tabbar { |
这里有一条针对 Android 的妥协:基座对 backdrop-filter 支持不稳定,背景色不透明度必须给高——即使模糊完全失效,文字也不会透出底层内容,效果降级为半透明纯色条而不是花屏。
滑动指示器:指示器是绝对定位的一格宽容器,宽 calc((100% - 10rpx) / 4)(4 个 tab 均分,减去胶囊 5rpx 内边距),位移只靠一个 transform:
1 | <view class="gt-indicator" :style="indicatorStyle"> |
1 | const indicatorStyle = computed(() => ({ |
1 | .gt-indicator { |
translateX(100%) 恰好移过一格自身宽度,不需要按像素计算偏移;缓动用 cubic-bezier(0.22, 1, 0.36, 1)(快出缓停),比线性更接近 iOS 的手感。内层白胶囊再叠一层阴影做出悬浮层次,选中态文字变主色、图标 scale(1.08) 淡入。
安全区:悬浮栏不占文档流,底部定位要吃掉 iPhone 刘海区:
1 | .gt-wrap { |
各 tab 页内容区底部同步预留 calc(160rpx + env(safe-area-inset-bottom)),否则最后一张卡片会被悬浮栏遮住。
四、平台差异:H5 端 custom 不生效
tabBar.custom 在 H5 端不生效——原生底栏照样渲染,等于两条 TabBar 叠在一起。处理方式是各 tab 页 onShow 里手动隐藏:
1 | onShow(() => { |
由此引出一个连带问题:早期首页有”弹窗打开时 hide、关闭时 show”的逻辑,弹窗一关 showTabBar 又把原生栏放回来盖住自定义栏。这段逻辑已移除——整个生命周期内都隐藏原生栏,弹窗层级问题自然消失。
App 端 custom: true 正常生效,无需处理;两端共用同一组件与 store,差异只收敛在 hideTabBar 一行。
五、验证清单
- H5:四个 tab 显示自定义栏、原生栏隐藏;点击后胶囊滑动与页面切换同步,无”旧内容 + 新高亮”中间帧;
- App:毛玻璃生效或降级为不透明纯色(不花屏)、安全区不遮挡;
+按钮进入新增物品页,activeIcon/inactiveIcon随选中切换;- 页面内容底部不被悬浮栏遮挡;
- 外部导航进入某 tab 后,指示器位置与当前页一致。
总结
自定义 TabBar 的难点不在样式,而在状态归属:高亮何时改、由谁改(目标页 onShow 的 sync()),以及两份 Tab 清单(pages.json 与 TAB_LIST)如何靠约定保持一致。毛玻璃本身只是几行 backdrop-filter,真正要处理的是 Android 降级、H5 原生栏共存和安全区这三端差异。样式可以随时换皮,状态规则和平台适配层才是稳定下来的部分。
条评论