ARTICLE DETAIL

资讯详情

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

微信小程序自定义顶部导航栏:从原理到全机型适配实践

微信小程序自定义顶部导航栏:从原理到全机型适配实践 简介微信小程序原生顶部导航栏在样式定制和不同机型适配上有不少限制这份完整实例正面向有自定义导航栏需求的小程序开发者演示如何先在 app.json 中关闭原生导航栏再通过自定义 navigationBar 组件构建统一头部并处理好组件属性定义、参数传递、状态栏高度与胶囊按钮位置适配等关键环节让导航栏在刘海屏、水滴屏及普通机型上都能保持稳定布局完整代码可直接参考或二次改造。压缩包共16个文件以 json、js、wxss、wxml 为主json 用于页面和组件配置js 负责导航栏逻辑与数据交互wxss 控制视觉样式wxml 搭建组件结构另附2张png效果图便于预览整包仅9KB轻量且没有冗余资源适合快速学习或嵌入现有项目。目前已有3370人学习下载内容得到过不少小程序开发者的关注。通过这份实例开发者能梳理出一套从原生导航栏隐藏、自定义组件封装、参数传入到多机型适配的完整思路同时获得一个命名清晰、目录结构一目了然的 navigationBar 组件雏形可直接用于后续项目复用。1. 自定义顶部导航栏为什么每个微信小程序项目都要重新处理刚开始做微信小程序时系统自带的navigationBar看起来够用能改标题、背景色、前后按钮。但一旦页面顶部需要放自定义组件、做沉浸式效果或者想统一安卓和iOS的视觉差异默认导航栏立刻变得僵硬。最头痛的是不同机型的顶部安全区、胶囊按钮坐标和状态栏高度都不一样写死固定px值在iPhone 14 Pro Max上刚好换到华为Mate 60可能就会出现标题顶到状态栏、按钮错位。自定义顶部导航栏(navigationBar)就是把整个顶部区域接管过来自己计算状态栏和胶囊按钮位置从而兼容适配所有机型。这篇文章从微信官方提供的接口原理讲起落到一个能直接复制使用的完整实例适合正在做小程序项目或准备封装统一导航组件的人。2. 自定义导航栏原理状态栏、导航栏与胶囊按钮的高度如何计算2.1 三个关键数据safeArea、menuButtonRect与screenHeight自定义顶部导航栏不能凭感觉写px微信提供了一套官方接口用来获取设备和胶囊按钮的坐标。核心是wx.getWindowInfo()和wx.getMenuButtonBoundingClientRect()。前者的返回值里有safeArea、statusBarHeight、screenWidth、screenHeight后者返回胶囊按钮的准确位置包括top、bottom、left、right和宽高。这两个接口组合起来就能描述任何机型下导航栏的工作区域。实际计算导航栏高度时常用公式是导航栏高度 (menuButtonRect.top - statusBarHeight) * 2 menuButtonRect.height这个公式的原理是微信在布局时会让胶囊按钮的垂直中心对齐导航栏区域的中心。因此从状态栏底边到胶囊按钮顶部的距离乘以2再加上胶囊本身高度刚好得到完整的导航栏高度。这样计算出来的值在iOS和Android上都能自动跟随胶囊位置而不是依赖一张固定的机型对照表。2.2 为何不直接使用默认navigationBar默认navigationBar在全局配置app.json里通过navigationStyle: custom就能关掉但很多人不知道关掉后页面顶层会直接顶到屏幕最上方。此时状态栏文字仍然是原来的黑白色如果页面背景也是白色状态栏文字就会看不清。同时胶囊按钮仍然保留它不是页面的wxml而是微信原生绘制的不能通过组件隐藏。所以自定义顶部导航栏的本质是确定状态栏高度、导航栏自身高度、胶囊按钮的位置然后把自己的view放在胶囊按钮同一行中间留出合适间距。这里给出一个常用的数据获取代码片段function getNavInfo() { const windowInfo wx.getWindowInfo(); const menuRect wx.getMenuButtonBoundingClientRect(); const { statusBarHeight, screenWidth } windowInfo; const navHeight (menuRect.top - statusBarHeight) * 2 menuRect.height; return { statusBarHeight, navHeight, menuRect, screenWidth, navBarHeight: navHeight statusBarHeight // 顶部总占用高度 }; }这段代码中navHeight是从状态栏底部到导航栏底部的高度通常范围在32到48px之间navBarHeight则是整个顶部区域从屏幕顶部到导航栏底部的高度页面内容设置padding-top时使用这个值。注意wx.getWindowInfo()从基础库2.20.1开始稳定如果项目还在用老版本可以加一个兼容判断用wx.getSystemInfoSync()兜底。2.3 状态栏文字颜色与背景穿透自定义导航栏后状态栏由页面窗口接管需要通过wx.setNavigationBarColor来设置状态栏文字颜色但注意当navigationStyle: custom时这个接口只对状态栏前景色有效背景色需要自己画。还有一种方式是直接使用page-meta组件中的page-style字段例如page-meta page-style--status-bar-color: #ffffff; /不过更通用的是在页面json里配置navigationStyle: custom然后在布局中用一个占位view设置高度为statusBarHeight背景色和导航栏一致这样视觉上就形成了完整的自定义顶部导航栏。在安卓手机上部分机型状态栏背景色会默认是黑色或半透明需要把占位view背景色设置成不透明否则会出现状态栏和页面背景不一致的尴尬情况。3. 手写一个兼容所有机型的自定义顶部导航栏组件3.1 在页面json中启用custom导航所有实现的第一步是在对应页面的.json文件中配置{ navigationStyle: custom, navigationBarTextStyle: white }navigationBarTextStyle在这里不影响布局但可以提前把状态栏文字指定为白色因为自定义导航栏通常是深色背景。如果导航栏是浅色应该设置成black。注意这个设置是页面级的页面切换时状态栏文字颜色变化可能会晚一帧如果出现闪烁可以在onLoad里主动调用一次wx.setNavigationBarColor({ frontColor: #ffffff })。3.2 组件结构设计状态栏占位、导航栏容器、菜单栏按钮我通常把自定义导航栏封装成组件custom-nav。组件的内部结构分三层第一层是状态栏占位高度等于statusBarHeight第二层是导航栏容器高度等于navHeight文字和返回按钮垂直居中第三层是内容占位高度为0避免后续内容被导航栏遮挡。胶囊按钮的位置由微信控制组件需要通过计算预留出右侧空间避免自己的按钮或者文字和胶囊重叠。下面是一个最小可用的组件模板view classcustom-nav stylepadding-top: {{statusBarHeight}}px; view classnav-bar styleheight: {{navHeight}}px; view classnav-bar__content view classnav-bar__left bindtaphandleBack image wx:if{{showBack}} src/images/back.png modeaspectFit / /view view classnav-bar__title{{title}}/view view classnav-bar__right/view /view /view /view对应的样式关键点是让nav-bar__content使用flex布局左中右三块均匀分布并且中间标题不能被左右元素挤歪。如果左侧有返回按钮右侧可能需要根据胶囊宽度设置等宽的占位否则标题不会居中。在iPhone上胶囊按钮宽度大约87px安卓机型则一般在80到95px之间所以动态获取menuRect后将right宽度设置为menuRect.width即可。3.3 组件属性与数据绑定组件的JS部分需要接收外部传入的标题、是否显示返回箭头等并在attached生命周期中计算导航信息。下面是一个完整示例Component({ properties: { title: { type: String, value: }, showBack: { type: Boolean, value: true }, bgColor: { type: String, value: #ffffff }, frontColor: { type: String, value: #000000 } }, data: { statusBarHeight: 20, navHeight: 44, menuRect: {} }, lifetimes: { attached() { const windowInfo wx.getWindowInfo(); const menuRect wx.getMenuButtonBoundingClientRect(); const statusBarHeight windowInfo.statusBarHeight; const navHeight (menuRect.top - statusBarHeight) * 2 menuRect.height; this.setData({ statusBarHeight, navHeight, menuRect }); if (this.properties.frontColor) { wx.setNavigationBarColor({ frontColor: this.properties.frontColor, animation: { duration: 0, timingFunc: easeIn } }); } } }, methods: { handleBack() { const pages getCurrentPages(); if (pages.length 1) { wx.navigateBack(); } else { wx.switchTab({ url: /pages/index/index }); } } } });这里的attached生命周期会在组件初始化时执行拿到的是当时窗口的尺寸。需要注意如果页面在onLoad中通过wx.setNavigationBarColor修改状态栏颜色可能会和组件的设置冲突。我一般会在组件中统一处理不再在页面里重复调用。属性bgColor用来设置导航栏容器的背景色但在组件中使用时需要动态绑定到style上例如styleheight: {{navHeight}}px; background-color: {{bgColor}};。提示组件样式建议设置styleIsolation: isolated否则页面全局样式可能会穿透进来导致高度或间距被意外覆盖。3.4 在页面中使用组件并验证最小效果在页面的json中声明组件{ usingComponents: { custom-nav: /components/custom-nav/index } }然后在页面的wxml中直接放置custom-nav title个人中心 showBack{{false}} bgColor#f5f5f5 / view classpage-content stylepadding-top: {{navBarHeight}}px; !-- 页面内容 -- /view注意页面的内容需要设置padding-top值为整个导航栏的总高度状态栏高度导航栏高度。但这里有个坑页面并不直接知道组件计算出的navHeight因此我通常把计算逻辑抽到一个公共JS文件中或者使用behavior让页面和组件共享同一份数据。如果只是快速验证可以忽略padding-top用开发者工具的模拟器切换机型观察导航栏是否保持在正确的位置。这时候看模拟器不同机型的胶囊按钮位置会变化但我们的导航栏应该始终和胶囊按钮对齐。如果没有对齐优先检查是否使用了有效的胶囊坐标接口其次检查样式中的padding-top是否错误地加在了custom-nav外面。4. 完整实例结合页面滚动和下拉效果的自定义导航栏4.1 页面级导航栏状态管理在实际项目中导航栏经常需要根据页面滚动改变背景色或文字颜色。比如一个商品详情页顶部是图片滚动前导航栏透明滚动后变成白色。这个效果不能只靠组件内部实现因为滚动事件在页面层。常见做法是页面把滚动状态传给导航栏组件组件根据状态切换样式。下面给一个页面wxml中结合滚动事件的写法custom-nav title{{pageTitle}} showBack{{true}} bgColor{{navBgColor}} frontColor{{navFrontColor}} /页面js中监听滚动Page({ data: { pageTitle: 商品详情, navBgColor: transparent, navFrontColor: #ffffff }, onPageScroll(e) { const scrollTop e.scrollTop; if (scrollTop 50) { this.setData({ navBgColor: #ffffff, navFrontColor: #000000 }); } else { this.setData({ navBgColor: transparent, navFrontColor: #ffffff }); } } });注意这里如果导航栏是transparent需要同时把状态栏背景也设置为透明否则会出现状态栏仍然是白色背景但下面的导航栏透明了视觉上断裂。不过实际上自定义导航栏后状态栏背景完全由页面背景决定所以只要页面最顶层视图中包含一个和状态栏高度相同的半透明或透明view状态栏背景就会呈现对应效果。设置transparent时前一个页面返回的动作可能会透出底部页面需要额外处理一般不推荐全透明可以用白色带透明度来做渐变。4.2 适配刘海屏、灵动岛和安卓挖孔屏不同机型的顶部安全区差异很大。iOS刘海屏状态栏高度一般是44px或47px灵动岛系列是59px安卓常见是24px到48px。胶囊按钮的位置也会随机型变化。我们的计算方案已经用statusBarHeight和menuRect动态适配不需要硬编码。但还有一个经常踩坑的地方当状态栏高度非常高比如灵动岛59px时navHeight仍然由胶囊位置计算组件顶部占位高度等于状态栏高度会使整个导航栏变得更高页面上下留白变大这是正常现象。安卓挖孔屏比较特殊部分机型的状态栏上边有摄像头微信返回的胶囊按钮会避开挖孔区域所以只要用wx.getMenuButtonBoundingClientRect()就能跟随胶囊位置不需要手动避开摄像头。另外有些安卓机型存在“状态栏背景颜色设置不生效”的老问题可以通过给状态栏占位view设置纯色背景并用!important或重新设置样式覆盖来解决。为了直观展示适配结果可以使用下面的参数表作为调试参考机型分类典型状态栏高度(px)胶囊top值(px)计算navHeight(px)iPhone 8202440iPhone 13 Pro475140iPhone 14 Pro Max596340华为Mate40 Pro323640小米11283240注意表中navHeight在大部分机型上接近40px这是因为胶囊按钮的上下边距通常各为4px高度为32px所以4 * 2 32 40。这个规律可以用于快速验证计算是否正确。如果某个机型得到的navHeight偏差很大说明接口返回异常或公式写错可以打印menuRect.top和statusBarHeight来排查。不同系统版本可能调整胶囊尺寸以上数据只做参考具体以wx.getMenuButtonBoundingClientRect()的实际返回为准。4.3 返回按钮、右侧视图与胶囊的间距控制导航栏中放返回按钮时左侧距离屏幕边缘有安全边距右侧则要避开胶囊。我用的是左侧固定padding右侧动态预留胶囊宽度。更精确的方法是把右侧视图宽度设为menuRect.width并加一个margin-right: 4px来保持视觉呼吸感。但有时候胶囊按钮右侧还有一小块空位实际生产环境中我通常直接使用menuRect.right到屏幕右侧的距离作为右侧占位宽度的一半实现左右视觉平衡。下面给出控制间距的样式片段.nav-bar__content { display: flex; align-items: center; justify-content: space-between; height: 100%; padding: 0 16px; } .nav-bar__right { width: 150px; /* 由js动态设置 */ height: 32px; display: flex; align-items: center; justify-content: flex-end; }这里的宽度不能只根据胶囊宽度设置因为胶囊左侧可能还有滑动返回区域。在iOS上从屏幕左缘向右滑动可以触发返回如果我们的自定义导航栏左侧放了一个按钮可能会遮挡手势区域。微信原生没有提供关闭这种手势的接口只能尽量让左侧按钮不要设计得太窄或者使用页面级enablePullDownRefresh等不会影响手势。4.4 横竖屏切换与尺寸变化微信小程序支持横屏后状态栏高度和胶囊位置可能会发生变化。组件的attached只在初始化时执行一次尺寸变化后数据是旧的。需要监听resize事件。可以在页面中注册wx.onWindowResize((res) { // 重新计算并setData });但要注意wx.onWindowResize在页面onUnload时需要移除使用同一个回调引用才能正常offWindowResize。更好的做法是组件内部监听但小程序组件生命周期中没有自带resize勾子需要页面通过this.selectComponent调用组件的更新方法。在完整实例中我会在组件中暴露一个updateNav()方法给页面调用methods: { updateNav() { const windowInfo wx.getWindowInfo(); const menuRect wx.getMenuButtonBoundingClientRect(); // 重新计算并setData } }然后在页面onResize或onWindowResize回调里调用。5. 把导航信息公共化用工具函数避免重复计算并做真机巡检到了项目后期多个页面都需要自定义顶部导航栏重复复制组件和计算逻辑会让状态管理变乱。我一般会把导航信息抽成一个公共模块导出getNavigationInfo()函数页面或组件都引用同一套逻辑。组件内部不再自己计算而是在attached时从公共函数读取一次同时暴露一个update方法。页面在滚动、旋转、或者从弹窗返回时可以手动刷新。具体做法是新建utils/nav.jslet cache null; function getNavigationInfo(force false) { if (cache !force) return cache; const windowInfo wx.getWindowInfo(); const menuRect wx.getMenuButtonBoundingClientRect(); const statusBarHeight windowInfo.statusBarHeight; const navHeight (menuRect.top - statusBarHeight) * 2 menuRect.height; const menuWidth menuRect.width; const menuHeight menuRect.height; const menuRight menuRect.right; const screenWidth windowInfo.screenWidth; cache { statusBarHeight, navHeight, navBarHeight: statusBarHeight navHeight, menuWidth, menuHeight, menuRight, contentTop: statusBarHeight navHeight, screenWidth }; return cache; } module.exports { getNavigationInfo };这里使用cache可以在同一页面多次调用时避免重复计算但当页面旋转后需要传true强制刷新。这个模块没有任何页面依赖也方便在开发者工具的console里直接执行验证。验证方法是打开页面在console中调用getNavigationInfo()对比打印出的statusBarHeight、navHeight和真机上的实际视觉位置。最后一个实用技巧是在页面onReady后通过wx.createSelectorQuery()选择导航栏节点获取它的boundingClientRect和getNavigationInfo()返回的navBarHeight做对比。如果两者有超过2px的偏差说明有样式被全局文件覆盖应该检查组件的styleIsolation设置。组件应当设置为styleIsolation: isolated避免页面外层样式穿透进入组件导致高度计算失效。这个检查步骤虽然简单但能省下大量真机调试时间。每次发布前我会拿一台iPhone、一台安卓中低端机和一台最新旗舰机跑一遍首页、详情页和个人页观察导航栏和胶囊按钮是否在一条水平线上。自定义顶部导航栏的最终目标不是让代码看起来复杂而是让用户在所有机型上都感觉不出你的导航栏是自定义的。做好这一步导航栏适配才算真正收敛。本文还有配套的精品资源点击获取
返回列表