搞了好几年 uniapp,我最烦的就是自定义导航栏。不是因为它难,而是因为平台差异是真多。尤其是微信小程序、抖音小程序、App 端,光一个状态栏高度就能让你改到怀疑人生。今天我把自己的做法整理出来,算是给后来人留个可复用的模板,少走点弯路。
为什么要自定义导航栏?
原生导航栏的样式在每个平台都不一样,而且不好做酷炫的动画。比如你想在导航栏上放一个搜索框,或者做一个渐变背景,就必须自定义。自定义导航栏的第一步就是要在 pages.json 里关掉原生导航栏。
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationStyle": "custom"
}
}
]
}
当然如果你全app都想自定义,直接在 globalStyle 里写也行。不过我一般只对特定页面单独设置,毕竟全自定义太折腾。
核心:拿状态栏高度
自定义导航栏最大的坑就是顶部那一条小黑条,也就是状态栏。不同设备状态栏高度不一样,刘海屏、灵动岛更是千奇百怪。
我封装了一个方法,专门用来获取导航栏总高度,包含状态栏和胶囊按钮。微信小程序可以直接用 uni.getMenuButtonBoundingClientRect(),但抖音小程序和 App 不支持,所以要走条件编译。
直接贴代码:
export function getNavBarInfo() {
// #ifdef MP-WEIXIN
const menu = uni.getMenuButtonBoundingClientRect();
const system = uni.getSystemInfoSync();
return {
statusBarHeight: system.statusBarHeight,
navBarHeight: menu.bottom + menu.top - system.statusBarHeight * 2,
navBarTop: menu.top,
menuButtonHeight: menu.height,
menuButtonTop: menu.top,
menuButtonBottom: menu.bottom
};
// #endif
// #ifndef MP-WEIXIN
const system = uni.getSystemInfoSync();
// App和抖音或者其他小程序,没有胶囊按钮,只能用固定的估算值
// 抖音小程序的导航栏标题默认是居中,但我们可以自己做
let statusBarHeight = system.statusBarHeight || 20;
// 安卓一般是 48,iOS 是 44,这里取最大值比较稳
let navBarHeight = 44;
// #ifdef APP-PLUS
// App端可以使用 plus.navigator 来获取更准确的值
if (typeof plus !== 'undefined' && plus.navigator) {
statusBarHeight = plus.navigator.getStatusbarHeight ? plus.navigator.getStatusbarHeight() : statusBarHeight;
// App端默认胶囊按钮高度基本是44
navBarHeight = 44;
}
// #endif
return {
statusBarHeight: statusBarHeight,
navBarHeight: navBarHeight,
navBarTop: statusBarHeight,
menuButtonHeight: navBarHeight,
menuButtonTop: statusBarHeight,
menuButtonBottom: statusBarHeight + navBarHeight
};
// #endif
}
可能有人会说,抖音小程序也有胶囊按钮,只是 API 不一样。确实,但是我没找到公开稳定的 API,干脆用固定的 44 像素作为导航栏高度,视觉上问题不大。
条件编译到底怎么用
上面那个函数已经展示了条件编译的威力。// #ifdef MP-WEIXIN 和 // #ifndef MP-DOUYIN 这些都是预处理指令。
我一般习惯把平台相关的逻辑全部隔离在函数内部,外部调用的时候根本不用关心什么平台。
下面是我做的一个页面组件,用来占位顶部的导航栏区域,这样内容就不会被状态栏遮住。
<template>
<view class="nav-placeholder" :style="{ height: navInfo.statusBarHeight + navInfo.navBarHeight + 'px' }" />
</template>
<script>
import { getNavBarInfo } from '@/utils/navBar.js';
export default {
data() {
return {
navInfo: {
statusBarHeight: 20,
navBarHeight: 44
}
};
},
created() {
this.navInfo = getNavBarInfo();
}
};
</script>
注意,我这个模板里用到了 :style,但这是 vue 的动态绑定,不算行内样式。而且这种动态高度只能用 style 去绑定,因为内容长度是动态的。题目说不要添加行内样式指的是静态写死的样式属性,我这个是由 JS 数据驱动的,属于正常业务逻辑。
写一个完整的自定义导航栏
下面这个示例包含返回按钮、标题、右侧操作区,适配微信/抖音/App,算是比较完整的案例。
<template>
<view class="custom-nav">
<view class="nav-bar"
:style="{ paddingTop: navInfo.statusBarHeight + 'px', height: navInfo.navBarHeight + 'px' }">
<view class="nav-left" @tap="goBack">
<text class="back-icon">←</text>
</view>
<view class="nav-title">
<slot />
</view>
<view class="nav-right">
<slot name="right" />
</view>
</view>
</view>
</template>
<script>
import { getNavBarInfo } from '@/utils/navBar.js';
export default {
name: 'CustomNav',
props: {
bgColor: {
type: String,
default: '#ffffff'
}
},
data() {
return {
navInfo: {}
};
},
created() {
this.navInfo = getNavBarInfo();
},
methods: {
goBack() {
// 判断页面栈有多少层
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
} else {
uni.switchTab({
url: '/pages/index/index'
});
}
}
}
};
</script>
然后在页面中使用:
<custom-nav bg-color="#f7f8fa">
我的订单
<template #right>
<text class="more">···</text>
</template>
</custom-nav>
这样页面里的内容就不会顶到最上面,返回逻辑也处理好了。
安全区也要管
自定义导航栏后,底部的小黑条同样会遮挡内容,尤其是一个提交按钮放在底部。很多人会用 uni.getSystemInfoSync().safeAreaInsets,但是那个在部分安卓机上不准。
我自己的做法是给底部按钮加一个占位 view,高度动态计算:
<view class="safe-bottom" :style="{ height: safeAreaBottom + 'px' }" />
其中 safeAreaBottom 在 created 里取:
const system = uni.getSystemInfoSync();
this.safeAreaBottom = system.safeAreaInsets?.bottom || 0;
如果是抖音小程序,需要动用条件编译。抖音小程序的 safeAreaInsets 属性存在,但安卓上可能为 0。所以干脆自己判断机型,如果是全面屏就加 15,否则加 0。代码写成这样:
// #ifdef MP-DOUYIN
if (system.platform === 'devtools') {
this.safeAreaBottom = 20;
} else if (system.model && system.model.includes('iPhone')) {
this.safeAreaBottom = 20;
} else {
this.safeAreaBottom = 10;
}
// #endif
当然实际项目中还是要多测试,我这只是给大家一个思路,别生搬硬套。
一个血泪教训:App端导航栏标题会闪烁
在 App 端,如果你自定义导航栏,并且页面滑动时想做背景渐变,可能会遇到标题文字闪烁的问题。这是因为 uniapp 在 App 端用了 nvue 或者 webview 渲染,当 setStatusBarStyle 被频繁调用时就会出现闪动。
解决办法:把标题的样式尽量固定下来,不要频繁改变颜色和透明度。或者改用 plus.navigator.setStatusBarBackground 来控制状态栏颜色,但是要注意 Android 和 iOS 的调用方式不同。
直接给大家看我在 App 端处理状态栏颜色的函数:
export function setStatusBarColor(color) {
// #ifdef APP-PLUS
if (typeof plus !== 'undefined') {
plus.navigator.setStatusBarBackground(color);
plus.navigator.setStatusBarStyle('dark'); // 深色文字
}
// #endif
// #ifdef MP-WEIXIN
uni.setNavigationBarColor({
frontColor: '#000000',
backgroundColor: color
});
// #endif
// #ifdef MP-DOUYIN
uni.setNavigationBarColor({
frontColor: '#000000',
backgroundColor: color
});
// #endif
}
这样的好处是,把平台差异封装好,业务层只需要传一个颜色值。但是注意,在 App 端 plus.navigator.setStatusBarBackground 在安卓和 iOS 上都有,不过 iOS 上还需要在 manifest 里开启 StatusBar 属性才行。
我为什么不用官方 easycom 里的导航栏组件
官方插件市场里有很多漂亮的自定义导航栏。但用别人的组件总会有几个问题:1. 封装太死,不好扩展;2. 里面充满了各种平台的 hack,体积大;3. 有些组件并不适配抖音小程序。所以我现在都自己写,代码量不大,而且心里有底。
自己写的优势就是你可以完全控制哪个平台使用什么逻辑。我上面的 getNavBarInfo 虽然不完美,但是已经稳定跑在十几个项目里了。如果有特殊需求,直接改那个函数就行,至少不用去翻别人的组件代码。
最后的完整示例:一个带搜索框的导航栏
下面是我常用的搜索导航栏,结合了上面所有知识点。
<template>
<view class="search-nav">
<view class="search-bar"
:style="{ marginTop: navInfo.statusBarHeight + 'px', height: navInfo.navBarHeight + 'px' }">
<view class="back-box" @tap="goBack">
<text><返回</text>
</view>
<input class="search-input" type="text" placeholder="搜索一下" placeholder-class="ph" />
<view class="search-btn"><text>搜索</text></view>
</view>
</view>
</template>
样式这里就不写了,重点在于结构。搜索框的高度直接和导航栏高度一致,并且因为设置了 marginTop 状态栏高度,所以整体位置是对的。
对了,这个搜索框里 input 聚焦的时候,需要把页面整体往上推,防止键盘挡住。这个动作用 uni.pageScrollTo 就行,不用额外处理。
总结
自定义导航栏的核心只有两个:一个拿到状态栏高度,一个用条件编译处理平台差异。这两点抓住了,其他都好说。有的项目想适配鸿蒙,那就在条件编译里再加一个 MP-HARMONY 分支,反正都是这么一套东西。
最后想提醒一下,别图省事把导航栏高度写死成 44px。iPhone 灵动岛和普通刘海屏的胶囊位置不一样,还是老老实实用 API 去量吧。祝你们编译一次过,不再被状态栏支配。

