uniapp自定义导航栏适配全攻略:条件编译与安全区一步到位

2026-08-11 0 578

搞了好几年 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 去量吧。祝你们编译一次过,不再被状态栏支配。

uniapp自定义导航栏适配全攻略:条件编译与安全区一步到位
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 uniapp uniapp自定义导航栏适配全攻略:条件编译与安全区一步到位 https://www.taomawang.com/web/uniapp/2526.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务