十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

微信小程序自定义导航栏:高度计算、组件封装与跨机型适配

微信小程序自定义导航栏:高度计算、组件封装与跨机型适配 简介这份资源是一套微信小程序自定义顶部导航栏的完整实例面向需要兼容适配所有机型、希望替换原生导航栏效果的小程序开发者或前端学习者。核心思路是先在全局配置中隐藏系统导航栏再通过自定义组件实现胶囊按钮对齐、状态栏高度适配等细节同时涵盖组件用法、参数传递及导航栏相关配置要点可直接套用到真实项目。压缩包为 rar 格式共十六个文件体积仅约九 KB非常轻量。其中以 json、js、wxss、wxml 文件为主分别承担页面配置、逻辑处理、样式定义和结构渲染另含两张示例图片便于对照效果。整体目录结构清晰包含全局配置、页面、组件及工具模块适合按模块阅读。目前已有三千三百七十人学习下载该实例虽小但针对性强能帮助开发者快速理清自定义导航栏的实现路径避开机型适配中的常见问题是一份即拿即用的参考代码。1. 自定义顶部导航栏navigationBar 不够用时的第一选择收到过一个需求小程序顶栏要改成品牌色标题左边还要放一个小图标。默认的 navigationBar 只允许改背景色、文字颜色图标、插槽、滚动变色、渐变遮罩统统不支持。于是只能把navigationStyle设为custom关掉原生导航栏用 view 自己画一条。标题里讲的这件事本质就是拿到状态栏高度、用胶囊按钮反推导航栏高度、把这段逻辑封装成组件并处理灵动岛、刘海屏、状态栏高度返回 0 这类边界。适合所有需要定制顶部导航栏的小程序开发者新手可以直接抄组件熟手可以拿走一套连边界问题都覆盖的测量方案。2. 用 getMenuButtonBoundingClientRect statusBarHeight 推导导航栏高度2.1 微信把胶囊放在哪导航栏就有多高微信小程序的顶部由两段组成状态栏和导航栏本体。状态栏是系统绘制的那一条显示时间、信号、电量导航栏本体是微信绘制的那一条胶囊按钮右上角那三个点就嵌在其中。默认情况下胶囊按钮在导航栏内是垂直居中的这给了我们一个反推公式的机会。// 取胶囊按钮在屏幕中的位置和尺寸 const menuRect wx.getMenuButtonBoundingClientRect() // 取状态栏高度注意兼容新老 API const info wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync() const statusBarHeight info.statusBarHeight // 导航栏本体高度 (胶囊顶部到状态栏底部的距离) * 2 胶囊高度 const navBarHeight (menuRect.top - statusBarHeight) * 2 menuRect.height这个公式的前提是胶囊垂直居中于导航栏。上间距等于下间距所以导航栏本体高度就是“上间距 胶囊高度 下间距”。menuRect.top - statusBarHeight是胶囊上边缘到状态栏底部的距离乘以 2 就是上下两段的等距空间再加上胶囊自身高度正好是导航栏本体的完整高度。这套推导并不是为了算得精确到像素而是为了让整个导航栏在视觉上和胶囊按钮保持“左右平衡”。如果你写死了 44 或 48在部分 Android 机上胶囊偏上标题就会显得偏下用公式推导标题永远和胶囊对齐。常见答疑里“微信小程序顶部导航栏高度是多少”就没法给固定答案iOS 一般是 44Android 一般是 48但刘海屏、灵动岛、字体缩放都会改变这两个数字公式才是跨机型通用的解法。2.2 第一次取不到胶囊坐标先同步后延迟wx.getMenuButtonBoundingClientRect()在基础库 2.1.0 之后可用但“可用”不代表“第一次调用就一定拿得到准确值”。在部分 Android 机型上页面刚渲染时胶囊的 rect 可能返回全 0 或 undefined尤其是从分享卡片、扫码、外部跳转冷启动进入页面的时候。我的做法是同步取一次取不到就用兜底常量然后在页面onShow里延迟 100~200ms 再取一次把准确的 rect 值覆盖进去。const DEFAULT_MENU_RECT { top: 26, height: 32, left: 278, right: 365, width: 87 } function getMenuRect() { try { const rect wx.getMenuButtonBoundingClientRect() if (rect rect.height 0 rect.top 0) { return rect } } catch (e) { // 低版本基础库或某些 WebView 环境可能直接报错 } return DEFAULT_MENU_RECT }兜底常量选top: 26, height: 32能覆盖大部分竖屏机型但不要依赖它作为最终值。延迟重取的意义在于首次渲染时状态栏高度和胶囊位置可能还没被系统层回传等 100ms 后微信绘制完成拿到的值才是用户屏幕上的真相。获取时机可靠性说明Page.onLoad 同步取大多数情况可靠冷启动偶发取到 0Component.attached 同步取同上组件实例化早于页面布局Page.onShow 延迟 200ms 取最可靠微信绘制完成后的最终值App.onLaunch 同步取不可靠此时导航栏尚未初始化延迟重取只做一次不要做成定时器否则页面反复切换时会闪一下。新值和旧值差异小于 2 像素时直接忽略避免无意义的 setData。2.3 导航栏高度的 rpx / px 换算边界很多人在这一步会踩坑把导航栏高度按 rpx 写进 wxml。导航栏的高度、胶囊的位置、状态栏高度getMenuButtonBoundingClientRect()和getWindowInfo()返回的全部是物理像素 px不是 rpx。!-- 正确px 直接用于 style -- view styleheight: {{statusBarHeight}}px; background: #fff;/view view styleheight: {{navBarHeight}}px; background: #fff;/view页面根节点拿到这两个值后padding-top用statusBarHeight navBarHeight撑开内容区域。不要把 px 转换成 rpx 再传进 style微信的 style 绑定直接接受 px转成 rpx 反而会导致不同屏幕宽度下高度失真。3. 把高度计算封装成自定义导航栏组件3.1 组件结构与 wxml 布局第 2 章的公式和多时空值逻辑直接写进页面会散落得到处都是。我一般会把导航栏封装成custom-nav-bar组件页面 json 注册wxml 里一行引入组件的data里维护statusBarHeight和navBarHeight。组件目录结构如下components/custom-nav-bar/ ├── index.js ├── index.json ├── index.wxml └── index.wxss先看 index.json{ component: true, options: { multipleSlots: true } }multipleSlots: true是为了支持右侧插槽后面放分享按钮或菜单按钮时要用到。组件 index.js 如下const DEFAULT_MENU_RECT { top: 26, height: 32, width: 87, left: 278, right: 365 } Component({ options: { multipleSlots: true }, properties: { bgColor: { type: String, value: #ffffff }, title: { type: String, value: }, titleColor: { type: String, value: #1a1a1a }, showBack: { type: Boolean, value: true } }, data: { statusBarHeight: 20, navBarHeight: 44, capsuleWidth: 87, capsuleHeight: 32 }, lifetimes: { attached() { this.initMenuRect() } }, methods: { initMenuRect() { const info wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync() const rect this.getMenuRect() const statusBarHeight info.statusBarHeight || 20 const navBarHeight (rect.top - statusBarHeight) * 2 rect.height this.setData({ statusBarHeight, navBarHeight, capsuleWidth: rect.width, capsuleHeight: rect.height }) }, getMenuRect() { try { const rect wx.getMenuButtonBoundingClientRect() if (rect rect.height 0 rect.top 0) { return rect } } catch (e) {} return DEFAULT_MENU_RECT }, handleBack() { const pages getCurrentPages() if (pages.length 1) { wx.navigateBack({ delta: 1 }) } else { // 冷启动时没有上一页跳回首页 wx.switchTab({ url: /pages/index/index }) } } } })initMenuRect()里先取窗口信息再取胶囊 rect最后算出导航栏高度。注意getWindowInfo()如果版本过低会不存在所以写法是wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync()。getMenuRect()单独抽出来是为了后面在onShow里延迟重取时复用。3.2 组件 wxml 的三种节点占位层、固定层、内容层组件模板的关键是把“占位”和“固定”分离。占位层在文档流里撑开高度固定层用position: fixed悬浮在页面顶部这样页面滚动时导航栏一直可见占位层保证页面内容不会钻进导航栏下方。!-- 占位把页面内容顶到导航栏以下 -- view classnav-placeholder styleheight: {{statusBarHeight navBarHeight}}px; background: {{bgColor}};/view !-- 固定真正显示的导航栏 -- view classnav-bar styleheight: {{statusBarHeight navBarHeight}}px; background: {{bgColor}}; view classstatus-bar styleheight: {{statusBarHeight}}px;/view view classnav-content styleheight: {{navBarHeight}}px; view classnav-side stylewidth: {{capsuleWidth}}px; view wx:if{{showBack}} classback-btn bindtaphandleBack view classback-icon/view /view /view view classnav-title stylecolor: {{titleColor}};{{title}}/view view classnav-side slot nameright/slot /view /view /view.nav-content用 flex 布局左右各一个和胶囊等宽的.nav-side中间.nav-title横向居中。这样标题的中心点落在左右两侧按钮区域的正中不会被右侧胶囊按钮的重量带偏。/* index.wxss */ .nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 999; } .nav-content { display: flex; align-items: center; padding: 0 8px; } .nav-side { display: flex; align-items: center; flex-shrink: 0; } .nav-title { flex: 1; text-align: center; font-size: 17px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .back-btn { width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; } .back-icon { width: 12px; height: 12px; border-left: 2px solid #1a1a1a; border-bottom: 2px solid #1a1a1a; transform: rotate(45deg); }这里.nav-side左侧和右侧宽度相同右侧槽位即便没有内容也占据和胶囊等宽的空白保证标题视觉居中。返回箭头没有用图片而是用 CSS 画了一个‹形状避免引入额外资源颜色跟随titleColor则把 border 颜色也换成 data 绑定。3.3 在页面里使用三行接入页面 json 注册组件{ navigationStyle: custom, usingComponents: { custom-nav-bar: /components/custom-nav-bar/index } }页面 wxml 引入custom-nav-bar title商品详情 bg-color#f5f5f5 title-color#333333 show-back{{true}} /页面无需再写任何高度计算逻辑组件内部负责测量。唯一要注意的是navigationStyle: custom要写进页面 json如果写在 app.json 的 window 下所有页面都会失去原生导航栏到时候每个页面都要引组件排查起来很麻烦。4. 跨机型适配清单从灵动岛到状态栏高度为 0 的坑4.1 状态栏高度返回 0 或 20 的兜底策略statusBarHeight在真机上通常不会为 0但开发工具模拟器、部分 Android WebView 环境、以及极个别定制 ROM 上确实会出现 0。另一种情况是返回固定 20这是微信对无法识别状态栏高度的降级值并非真实高度。兜底方案要分两层取值时兜底布局时兜底。const info wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync() let statusBarHeight info.statusBarHeight || 20 // 额外处理iPhone 刘海屏机型最低 44灵动岛最低 59 if (statusBarHeight 20) { statusBarHeight 20 }顺序是先用新 API 取取到后判断是否小于 20小于就按 20 处理。20 是 iPhone 非刘海屏和大部分 Android 的基础状态栏高度这个兜底值能保证布局不会塌掉但不能保证视觉完美。不正常的高度意味着胶囊 rect 大概率也不正常所以兜底之后还要做一次延迟重取校准。4.2 灵动岛、横屏和胶囊位置的特殊情况iPhone 灵动岛机型的状态栏高度在竖屏下是 59 或 62胶囊按钮整体下移用公式反推出来的navBarHeight会自动变大不需要针对灵动岛单独写 if 判断。容易出问题的是横屏场景横屏下状态栏高度变为 0胶囊按钮会移动到屏幕左侧或右侧边缘。小程序如果要支持横屏自定义导航栏建议直接不做或单独做一套横屏布局。判断横屏的代码const info wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync() if (info.windowWidth info.windowHeight) { // 横屏降级为原生导航栏或抬高导航栏底部 this.setData({ navBarHeight: 32 }) }横屏的复杂点在于不同机型的胶囊位置差异大有的在左有的在右公式推导出来的高度在横屏下没有参考价值。常规做法是横屏下强制使用默认导航栏在页面 json 里动态切换navigationStyle不现实所以绝大多数小程序直接放弃横屏自定义导航栏只有游戏类小程序会用。4.3 页面栈深度与返回键的显示逻辑自定义导航栏的返回键不能一直显示。从首页进入详情页页面栈长度是 2返回键应该出现从分享卡片冷启动进入页面页面栈长度是 1没有上一页可返回返回键点了也没有反应。组件里已经处理过这个逻辑handleBack() { const pages getCurrentPages() if (pages.length 1) { wx.navigateBack({ delta: 1 }) } else { wx.switchTab({ url: /pages/index/index }) } }这里有个细节冷启动进入的页面如果本身不是 tabBar 页面switchTab会失败需要加fail回调或者判断当前页面是否属于 tabBar 页面再做跳转。常见做法是给组件加一个homeUrl属性由使用方传入冷启动时的回跳地址。4.4 字体缩放和 onShow 时的二次校准微信「通用-字体大小」设置改变后重新打开小程序胶囊按钮的位置和状态栏高度可能发生变化。这不是每次都会发生但一旦发生导航栏标题和胶囊的垂直对齐就会偏差几个像素。解决方式是在页面onShow里延迟重取一次导航栏信息。onShow() { setTimeout(() { const navBar this.selectComponent(#customNavBar) if (navBar) { navBar.initMenuRect() } }, 200) }重取时比较新旧数据差异差距小于 2px 直接 return不给用户看到闪烁。这行代码不用每个页面都写我一般封装一个useCustomNavBar的混入或组件方法页面只需要在 onShow 调一次。机型/场景现象处理方案iPhone 灵动岛状态栏高 59胶囊下移公式自动适配无需判断Android 挖孔屏状态栏高度 44~48公式自动适配开发工具模拟器状态栏固定 20/24真机预览为准冷启动拿不到 rect导航栏高度错误onShow 延迟 200ms 重取字体缩放后胶囊与标题不对齐重取后 diff 阈值更新横屏状态栏为 0胶囊移动放弃自定义或单独布局5. 自定义导航栏的滚动渐变、状态栏前景色与机型走查5.1 滚动渐变从透明到纯白的过程自定义导航栏常见需求是页面顶部是头图导航栏透明显示白色文字往下滚动后导航栏变成白色背景、黑色文字。这个效果不需要组件内部监听滚动而是由页面通过onPageScroll把状态传给组件。// 页面 js onPageScroll(e) { const { scrollTop } e this.setData({ navBarScrolled: scrollTop 50 }) }组件增加一个scrolled属性wxml 里动态切换样式view classnav-bar {{scrolled ? nav-bar--scrolled : }} stylebackground: {{scrolled ? bgColor : transparent}};配合.nav-bar的transition: background-color 0.2s ease;就能得到一个平滑的渐变效果。注意状态栏前景色时间、电量、信号的颜色也要跟着切换页面 json 里navigationBarTextStyle支持black和white在自定义导航栏模式下依然生效。滚动后调用wx.setNavigationBarColor切换前后景色if (scrollTop 50) { wx.setNavigationBarColor({ frontColor: #000000 }) } else { wx.setNavigationBarColor({ frontColor: #ffffff }) }frontColor只支持#000000和#ffffff别传品牌色否则会报错。切换时会有瞬间跳变微信没有提供渐变动画所以只在滚动跨过阈值时调用一次不要在每个 scroll 事件里都调。5.2 下拉刷新区域与导航栏的边界处理用了自定义导航栏后页面下拉刷新的动画会出现在导航栏下方而不是原生导航栏下方。如果你页面里开了enablePullDownRefresh: true胶囊按钮区域会露出默认的白色背景非常难看。处理办法有两个。一是把enablePullDownRefresh关闭用 scroll-view 自定义下拉刷新二是把页面根节点page的背景色设置成导航栏背景色相同视觉上融为一体。推荐第一种scroll-view 自带refresher-enabled刷新动画的位置可以自由控制不会和导航栏抢空间。5.3 发布前的机型走查清单每次发布涉及导航栏改动我都会过一遍这个清单比看任何文档都有用开发工具 iPhone X 模拟器 真机 iOS 刘海屏各跑一遍确认标题上下间距一致返回按钮不浮在状态栏里。Android 真机选一台挖孔屏手机现在主流就是这类检查状态栏高度和胶囊 rect 是否正常重点看返回按钮和胶囊按钮的底部是否对齐。把微信字体大小调到最大杀掉小程序重新进入确认二次校准生效标题没有下移。从分享卡片、小程序码、公众号菜单三种入口冷启动确认页面栈长度判断正确返回键不会出现在首页。开启深色模式切到深色背景页面确认导航栏背景和状态栏前景色都有对应配置。横竖屏切换一次如果被设计上禁止确认pageOrientation为portrait避免横屏下导航栏塌掉。最后补一个细节getSystemInfoSync()在小程序基础库 2.20.1 之后标注即将废弃不建议继续使用。新项目直接wx.getWindowInfo()老项目给兼容分支不要图省事只用老 API等基础库升级后你会在控制台看到各种 deprecation warning。本文还有配套的精品资源点击获取
返回列表