尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

微信小程序自定义导航栏全攻略:从原理到实战适配

微信小程序自定义导航栏全攻略:从原理到实战适配 1. 为什么需要自定义顶部导航栏做微信小程序开发如果你只是写个简单的工具或者内容展示页用微信自带的导航栏配置一下颜色和标题基本就够用了。但一旦涉及到稍微复杂一点的UI设计比如设计师给了一个沉浸式背景图要求导航栏背景透明或者要在导航栏区域塞入搜索框、返回按钮加分享入口这种组合控件原生的导航栏就显得力不从心了。这时候自定义导航栏就成了必选项。我最早遇到这个需求是做一个电商类的小程序。产品经理希望首页的顶部是一个渐变色的背景上面有城市定位、搜索框和消息入口。原生导航栏是纯色块根本没法实现这种设计。另一个常见的场景是视频或阅读类小程序为了最大化内容展示区域需要把导航栏做得非常简洁甚至隐藏标题只保留一个返回的胶囊按钮。这些需求都指向同一个技术点关闭原生导航栏自己用View组件从头绘制一个。听起来简单不就是画个View放在顶部吗但真动手做你会发现一堆坑等着你不同手机状态栏高度不一样iOS和Android不一样小程序右上角的胶囊按钮位置是固定的你的自定义导航栏高度必须完美适配它否则内容就会被遮挡或者出现难看的空白在全面屏手机上还需要考虑安全区域。这些细节如果处理不好轻则UI错乱重则功能按钮点不到。网上很多教程只给代码片段缺了核心的原理分析和“踩坑”经验照着做很容易出问题。所以我把自己多次实战后总结的超详细流程和适配方案写下来附上效果图和关键代码希望能帮你一次搞定。2. 核心原理与关键API剖析自定义导航栏的本质是关闭微信小程序默认的导航栏并在页面最顶部自己用View、Image、Text等基础组件搭建一个相同功能的区域。这里涉及到两个最核心的API全局配置的navigationStyle和用于获取胶囊按钮位置的wx.getMenuButtonBoundingClientRect()。2.1 启用自定义模式navigationStyle第一步你需要在全局或页面的JSON配置文件中将navigationStyle设置为custom。这告诉微信“这个页面的导航栏我自己来画你不用管了。”全局设置 (app.json):{ window: { navigationStyle: custom } }全局设置意味着所有页面都会使用自定义导航栏。如果你只有部分页面需要自定义我更推荐在页面级进行设置这样更灵活也避免对不需要的页面产生意外影响。页面设置 (页面对应的.json文件):{ navigationStyle: custom }设置为custom后原生导航栏会消失页面内容会直接顶到屏幕最顶部位于手机状态栏显示时间、电量的那一栏的下方。这时候你的页面顶部就是一片“空地”需要你自己用代码把导航栏“填”上去。2.2 获取布局的尺子wx.getMenuButtonBoundingClientRect这是整个自定义导航栏适配的灵魂。右上角的胶囊按钮“...”菜单是微信客户端绘制的我们无法控制它的位置和大小。我们的自定义导航栏必须为这个胶囊按钮“让出”空间或者以其为基准进行布局。wx.getMenuButtonBoundingClientRect()这个API的作用就是获取这个胶囊按钮在屏幕中的位置信息。它返回一个对象包含以下关键属性单位pxwidth: 胶囊按钮的宽度。height: 胶囊按钮的高度。top: 胶囊按钮上边界到屏幕顶部的距离。right: 胶囊按钮右边界到屏幕左边的距离。bottom: 胶囊按钮下边界到屏幕顶部的距离。left: 胶囊按钮左边界到屏幕左边的距离。这里有一个非常重要的概念top和bottom是相对于屏幕顶部的而不是页面顶部。因为此时原生导航栏已消失页面顶部就是屏幕顶部。top的值基本上就是手机状态栏的高度加上一些系统预留的间距。通过这个API我们可以动态计算出我们自定义导航栏需要的高度。一个常见的公式是自定义导航栏总高度 胶囊按钮bottom值 自定义的底部间距例如8px或者更保守一点自定义导航栏总高度 胶囊按钮高度 (胶囊按钮top值 - 系统状态栏高度) * 2实际上我们通常直接使用胶囊按钮.bottom 8px作为总高度因为bottom已经包含了从屏幕顶部到胶囊按钮底部的全部距离再加一点余量确保导航栏内容完全在胶囊按钮下方不会重叠。2.3 系统状态栏高度的获取胶囊按钮的top值包含了状态栏高度但有时我们需要单独知道状态栏的高度以便将标题或其他内容垂直居中。微信提供了wx.getSystemInfoSync()API其中的statusBarHeight字段就是系统状态栏的高度单位px。所以一个完整的自定义导航栏区域通常由两部分组成状态栏占位区域高度为statusBarHeight通常设置一个背景色或者透明。导航内容区域从状态栏底部开始到胶囊按钮底部结束或加上余量。这个区域里放置返回图标、标题、搜索框等自定义内容。将这两部分的高度加起来就是整个自定义导航栏的View容器应该设置的高度。3. 从零开始的详细实现步骤下面我将以一个最常见的场景为例实现一个带有返回按钮、居中标题和胶囊按钮保留原功能的自定义导航栏。我们会创建可复用的组件。3.1 第一步项目结构与配置创建组件在项目根目录下新建一个components文件夹如果还没有在里面创建custom-navigation-bar文件夹并新建对应的组件文件custom-navigation-bar.wxml,custom-navigation-bar.wxss,custom-navigation-bar.json,custom-navigation-bar.js。声明组件在custom-navigation-bar.json中将组件设置为自定义组件。{ component: true, usingComponents: {} }页面配置在你需要使用自定义导航栏的页面的JSON文件中例如index.json进行两项配置{ usingComponents: { custom-navigation-bar: /components/custom-navigation-bar/custom-navigation-bar }, navigationStyle: custom }3.2 第二步组件逻辑层JS实现custom-navigation-bar.js是核心负责计算所有关键尺寸。// custom-navigation-bar.js Component({ properties: { // 组件外部传入的属性 title: { type: String, value: 默认标题 }, backgroundColor: { type: String, value: #ffffff }, color: { type: String, value: #000000 }, showBack: { type: Boolean, value: true } }, data: { // 内部数据 statusBarHeight: 0, // 状态栏高度 navigationBarHeight: 0, // 整个自定义导航栏的高度 menuButtonHeight: 0, // 胶囊按钮高度 menuButtonTop: 0, // 胶囊按钮上边界距屏幕顶部距离 navigationContentHeight: 0, // 导航内容区域不含状态栏的高度 }, lifetimes: { attached() { // 在组件实例进入页面节点树时执行 this.calculateBarHeight(); } }, methods: { calculateBarHeight() { // 1. 获取系统信息 const systemInfo wx.getSystemInfoSync(); // 2. 获取胶囊按钮信息 const menuButtonInfo wx.getMenuButtonBoundingClientRect(); // 3. 计算关键高度 // 状态栏高度 const statusBarHeight systemInfo.statusBarHeight; // 导航内容区域高度胶囊按钮高度 (胶囊按钮上边距 - 状态栏高度) * 2 // 这个公式的目的是计算出从状态栏底部到胶囊按钮底部这个区域的高度。 // (menuButtonInfo.top - statusBarHeight) 是胶囊按钮上方的间隙。 // 通常上下间隙是对称的所以乘以2。再加上胶囊按钮自身高度就是内容区域总高。 const navigationContentHeight (menuButtonInfo.top - statusBarHeight) * 2 menuButtonInfo.height; // 4. 整个导航栏高度 状态栏高度 导航内容区域高度 const navigationBarHeight statusBarHeight navigationContentHeight; // 5. 更新组件数据 this.setData({ statusBarHeight: statusBarHeight, navigationBarHeight: navigationBarHeight, menuButtonHeight: menuButtonInfo.height, menuButtonTop: menuButtonInfo.top, navigationContentHeight: navigationContentHeight }); // 6. 可选将计算出的导航栏高度传递给页面方便页面内容容器设置上边距 this.triggerEvent(heightChange, { height: navigationBarHeight }); }, // 返回按钮点击事件 handleBack() { // 先尝试触发自定义事件由页面决定如何处理例如判断页面栈 this.triggerEvent(back); // 如果页面没有处理则默认返回上一页 if (!this._backEventHandled) { // 假设页面处理后会设置一个标志这里仅为示意 wx.navigateBack(); } } } })关键计算逻辑解读menuButtonInfo.top - statusBarHeight这部分计算的是胶囊按钮顶部到状态栏底部之间的间隙。这个间隙是系统预留的用于视觉平衡。为什么乘以2因为通常胶囊按钮上下方的间隙是相等的。所以(menuButtonInfo.top - statusBarHeight)是上间隙我们再假设下间隙与之相等那么上下间隙的总和就是(menuButtonInfo.top - statusBarHeight) * 2。最终的内容区域高度 上下间隙总和 胶囊按钮自身高度。这个高度能确保你的导航内容如标题在垂直方向上与胶囊按钮大致处于同一水平线视觉上最协调。3.3 第三步组件视图层WXML与样式WXSSWXML 结构 (custom-navigation-bar.wxml)!-- 最外层容器高度为计算出的总高度背景色可自定义 -- view classcustom-navigation-bar styleheight: {{navigationBarHeight}}px; background-color: {{backgroundColor}}; !-- 状态栏占位区域 -- view classstatus-bar styleheight: {{statusBarHeight}}px;/view !-- 导航内容区域 -- view classnavigation-content styleheight: {{navigationContentHeight}}px; !-- 左侧返回按钮区域 -- view classleft-area block wx:if{{showBack}} view classback-btn bindtaphandleBack !-- 这里可以用图片或Unicode字符例如左箭头 -- text classback-icon‹/text !-- 或者 image src/images/back.png modewidthFix/image -- /view /block /view !-- 中间标题区域 -- view classcenter-title stylecolor: {{color}}; {{title}} /view !-- 右侧区域用于放置胶囊按钮或其他自定义内容 -- !-- 注意我们需要在这里为微信原生的胶囊按钮留出空间 -- view classright-area !-- 这个占位块的宽度和位置必须与胶囊按钮对齐 -- view classmenu-button-placeholder stylewidth: {{menuButtonInfo ? menuButtonInfo.width : 0}}px; !-- 占位不放置任何可见内容 -- /view /view /view /viewWXSS 样式 (custom-navigation-bar.wxss)/* 清除默认边距 */ .custom-navigation-bar { width: 100%; position: fixed; /* 固定定位悬浮在页面顶部 */ top: 0; left: 0; z-index: 9999; /* 确保导航栏在最上层 */ box-sizing: border-box; } .status-bar { width: 100%; /* 状态栏区域背景色通常与导航内容区域一致或透明 */ } .navigation-content { width: 100%; display: flex; align-items: center; /* 垂直居中 */ justify-content: space-between; /* 左右两侧贴边中间标题居中 */ padding: 0 16rpx; /* 左右内边距可根据设计调整 */ box-sizing: border-box; } .left-area { flex-shrink: 0; /* 防止被压缩 */ display: flex; align-items: center; height: 100%; } .back-btn { padding: 16rpx; /* 增加点击热区 */ display: flex; align-items: center; justify-content: center; } .back-icon { font-size: 48rpx; line-height: 1; } .center-title { flex: 1; /* 占据剩余所有空间实现居中 */ text-align: center; font-size: 36rpx; /* 小程序默认导航栏标题大小 */ font-weight: bold; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 20rpx; /* 防止标题过长时紧贴两侧 */ } .right-area { flex-shrink: 0; display: flex; align-items: center; height: 100%; } .menu-button-placeholder { height: 100%; /* 这个占位块的高度由父级.navigation-content决定宽度由JS传入的胶囊按钮宽度决定 */ /* 它本身是透明的目的只是为了把中间标题的“flex:1”区域挤开为胶囊按钮留出空间 */ }注意.menu-button-placeholder这个元素至关重要。因为微信的胶囊按钮是原生控件始终悬浮在最上层。我们不能用我们的元素去覆盖它。所以策略是在导航栏的右侧区域放置一个与胶囊按钮等宽的透明占位块。这样通过Flex布局的space-between或flex:1的特性中间标题区域就会自动避开这个占位块从而也避开了胶囊按钮的实际位置实现了视觉上的对齐和功能上的不冲突。3.4 第四步在页面中使用组件在页面的WXML文件中引入并使用组件!-- index.wxml -- !-- 引入自定义导航栏组件 -- custom-navigation-bar idcustomNavBar title我的首页 backgroundColor#f8f8f8 color#333 showBack{{false}} !-- 首页通常不显示返回按钮 -- bind:heightChangeonNavigationBarHeightChange bind:backonBack / !-- 页面内容区域 -- !-- 关键内容区域必须设置一个上边距(padding-top或margin-top)其值等于自定义导航栏的总高度否则内容会被导航栏遮挡 -- view classpage-content stylepadding-top: {{navBarHeight}}px; !-- 你的页面具体内容在这里 -- text这里是页面的主体内容/text /view在页面的JS文件中接收导航栏高度并处理事件// index.js Page({ data: { navBarHeight: 0 // 用于存储导航栏高度动态设置给内容容器 }, onLoad() { // 如果需要可以在这里获取组件实例并读取高度 // 但更推荐通过事件监听 }, // 监听导航栏组件的高度变化事件 onNavigationBarHeightChange(e) { const height e.detail.height; this.setData({ navBarHeight: height }); // 也可以将高度存入全局变量或本地存储供其他页面组件使用 getApp().globalData.navBarHeight height; }, // 处理返回按钮事件 onBack() { const pages getCurrentPages(); if (pages.length 1) { wx.navigateBack(); } else { // 如果是首页可以跳转到指定页或提示 wx.switchTab({ url: /pages/index/index }); } } })4. 深入适配全面屏、iOS与Android的差异处理上面的基础实现能覆盖大部分情况但在更复杂的设备和场景下还需要进一步打磨。4.1 安全区域Safe Area适配全面屏手机如iPhone X及以上机型、很多Android刘海屏、水滴屏手机的屏幕角落是圆角顶部有刘海底部有黑条Home Indicator。为了确保内容不被这些区域遮挡需要引入“安全区域”的概念。微信小程序提供了wx.getSystemInfoSync()中的safeArea对象。它包含top,bottom,left,right,width,height属性标识出了屏幕中绝对安全的内容区域。对于自定义导航栏我们主要关心safeArea.top。这个值表示安全区域上边界到屏幕顶部的距离对于有刘海的手机这个值就是状态栏高度加上刘海的物理高度。优化方案 在计算状态栏占位高度时不再直接使用statusBarHeight而是使用safeArea.top。因为safeArea.top已经包含了状态栏和刘海的高度这样能确保你的导航栏内容起始于安全区域之内。修改组件JS中的计算calculateBarHeight() { const systemInfo wx.getSystemInfoSync(); const menuButtonInfo wx.getMenuButtonBoundingClientRect(); // 使用 safeArea.top 作为状态栏区域的高度基准 const statusBarHeight systemInfo.safeArea.top; // 关键修改点 // 注意胶囊按钮的top是相对于屏幕顶部的包含了safeArea.top以上的部分。 // 所以后续计算导航内容区域高度的逻辑 (menuButtonInfo.top - statusBarHeight) * 2 menuButtonInfo.height // 依然成立因为它计算的是从安全区域顶部(safeArea.top)到胶囊按钮底部的距离。 // 但是这里有一个潜在的坑在非全面屏手机上safeArea.top 可能等于 statusBarHeight。 // 而在一些Android异形屏上safeArea.top 可能为0。我们需要一个兼容性处理。 const safeTop systemInfo.safeArea ? systemInfo.safeArea.top : systemInfo.statusBarHeight; const statusBarHeight safeTop 0 ? safeTop : systemInfo.statusBarHeight; const navigationContentHeight (menuButtonInfo.top - statusBarHeight) * 2 menuButtonInfo.height; const navigationBarHeight statusBarHeight navigationContentHeight; this.setData({ statusBarHeight: statusBarHeight, navigationBarHeight: navigationBarHeight, // ... 其他数据 }); }同时页面内容容器的上边距也应该使用这个更新后的navigationBarHeight。4.2 iOS与Android的胶囊按钮差异实测中发现在部分Android机型特别是某些厂商定制系统上wx.getMenuButtonBoundingClientRect()返回的胶囊按钮top值可能为0或异常小。这会导致我们的计算公式失效。处理方案设置一个兜底的最小高度。calculateBarHeight() { // ... 获取 systemInfo 和 menuButtonInfo let statusBarHeight systemInfo.statusBarHeight; const safeTop systemInfo.safeArea ? systemInfo.safeArea.top : statusBarHeight; statusBarHeight safeTop 0 ? safeTop : statusBarHeight; // 处理胶囊按钮信息异常 let menuTop menuButtonInfo.top; let menuHeight menuButtonInfo.height; // 如果获取到的top值异常例如为0或小于状态栏高度使用一个经验值 // 经验值iOS通常约48px, Android约56px这里指从屏幕顶部到胶囊按钮中心的估算距离 // 我们可以根据状态栏高度来估算一个合理的menuTop if (!menuTop || menuTop statusBarHeight) { // 根据不同平台设置一个默认的胶囊按钮上边距 const isIOS systemInfo.platform ios; menuTop statusBarHeight (isIOS ? 6 : 8); // 经验值可根据UI效果微调 menuHeight isIOS ? 32 : 40; // 经验值 } const navigationContentHeight (menuTop - statusBarHeight) * 2 menuHeight; const navigationBarHeight statusBarHeight navigationContentHeight; this.setData({ statusBarHeight: statusBarHeight, navigationBarHeight: navigationBarHeight, menuButtonHeight: menuHeight, menuButtonTop: menuTop, navigationContentHeight: navigationContentHeight }); }4.3 页面滚动与吸顶效果自定义导航栏使用了position: fixed固定在顶部。当页面滚动时它始终会停留在顶部这通常是我们想要的效果即吸顶。但需要注意一个细节fixed定位的元素是脱离正常文档流的。这意味着页面内容容器设置的padding-top等于导航栏高度是为了在初始位置给导航栏留出空间。当页面滚动时导航栏悬浮在上方这个padding-top区域在视觉上就变成了导航栏后面的背景。如果你的导航栏背景是半透明或渐变的滚动时页面内容在这个区域“滑动”的效果需要被正确处理。通常这不是问题。但如果你发现滚动时导航栏背景和内容衔接处有闪烁或不自然可以尝试给导航栏容器加上backdrop-filter: blur(10px)iOS毛玻璃效果安卓支持不佳或确保其背景色为纯色。5. 进阶效果与实战踩坑记录掌握了基础实现和适配后我们可以玩出更多花样同时也分享几个我实际踩过的坑。5.1 实现沉浸式渐变背景导航栏设计师常要求导航栏背景是一张渐变色或者一张延伸到导航栏的背景图的一部分。由于导航栏是fixed定位我们需要让页面内容区域向上延伸覆盖掉导航栏的padding-top区域并通过z-index控制层级。思路页面内容区域不再设置padding-top而是设置一个margin-top值为导航栏高度。这样内容是从导航栏下方开始布局的。在页面内容的最顶部创建一个与导航栏等高的背景元素例如一个View给它设置渐变色或背景图并赋予position: sticky或position: relative定位。通过z-index控制让自定义导航栏z-index: 9999悬浮在这个背景元素之上但位于其内容如文字、按钮之下。页面WXML调整示例!-- index.wxml -- custom-navigation-bar title沉浸式首页 backgroundColortransparent !-- 导航栏背景设为透明 -- color#fff !-- 标题和图标设为白色 -- showBack{{true}} / view classpage-content stylemargin-top: {{navBarHeight}}px; !-- 顶部背景元素 -- view classtop-background styleheight: {{navBarHeight}}px; background: linear-gradient(to right, #ff5e7d, #ff9900); !-- 这个元素只提供背景 -- /view !-- 实际页面内容 -- view classcontent-wrapper styleposition: relative; z-index: 1; !-- 你的页面具体内容在这里 -- /view /view页面WXSS.custom-navigation-bar { /* 确保导航栏内容在顶层 */ z-index: 10000; } .top-background { position: absolute; top: -{{navBarHeight}}px; /* 向上偏移覆盖margin-top留出的空间 */ left: 0; width: 100%; z-index: 0; /* 层级低于导航栏高于内容 */ } .content-wrapper { position: relative; background-color: #fff; /* 内容区域有自己的背景 */ border-radius: 24rpx 24rpx 0 0; /* 可选顶部圆角 */ margin-top: -20rpx; /* 可选让内容稍微上移与背景融合 */ }这样滚动时.top-background会随着页面内容移动而.custom-navigation-bar则固定在上方透过其透明背景看到下面的渐变实现了沉浸式效果。5.2 胶囊按钮区域的自定义交互微信原生的胶囊按钮点击会弹出菜单。但有时我们希望在胶囊按钮所在的区域放置我们自己的图标比如分享、更多。由于无法隐藏或覆盖原生胶囊按钮我们可以采用“扩展点击区域”的思路。在之前的结构中我们已经在右侧区域放置了一个与胶囊按钮等宽的.menu-button-placeholder作为占位。我们可以在这个占位块的旁边左侧放置我们的自定义图标。!-- 在 .right-area 内修改 -- view classright-area !-- 自定义分享按钮放在胶囊按钮占位块的左边 -- view classcustom-share-btn bindtaponShare wx:if{{showShare}} image src/images/share.png modewidthFix stylewidth: 40rpx; height: 40rpx;/image /view !-- 胶囊按钮占位块 -- view classmenu-button-placeholder stylewidth: {{menuButtonWidth}}px;/view /view.right-area { display: flex; align-items: center; } .custom-share-btn { padding: 16rpx; margin-right: 16rpx; /* 与胶囊按钮占位块保持间距 */ }这样用户点击分享图标时触发我们的事件点击右侧的占位块区域即原生胶囊按钮上方则会弹出微信菜单。两者互不干扰。5.3 实际开发中遇到的坑与解决方案坑1自定义导航栏在部分Android机型上闪烁或抖动。现象页面滚动时导航栏有时会轻微上下移动或闪烁。原因这可能与页面滚动事件、CSS的transform或fixed定位在旧版本WebView中的渲染bug有关。也可能是计算高度时数值不是整数导致亚像素渲染问题。解决确保计算出的navigationBarHeight等值是整数用Math.floor()取整。给导航栏容器添加CSS属性transform: translateZ(0);或will-change: transform;来开启GPU加速促进图层合成减少渲染问题。检查页面是否有频繁的setData操作影响了布局。坑2页面切换如wx.navigateTo时自定义导航栏会短暂消失再出现。现象从A页面跳转到B页面B页面有自定义导航栏B页面的导航栏会先显示原生样式或空白然后才渲染出自定义组件。原因页面跳转动画期间新页面开始渲染。自定义组件需要时间进行初始化、计算尺寸和渲染。而原生导航栏的隐藏navigationStyle: custom与应用组件渲染之间存在时间差。解决这是一个视觉体验问题很难彻底消除。可以尝试以下方法缓解给导航栏容器设置一个与最终背景色相同的初始背景色避免出现白色闪屏。将导航栏高度的计算逻辑提前例如在app.js的onLaunch中计算并存入全局变量页面组件直接从全局变量读取减少计算和渲染时间。使用wx.nextTick确保在页面初次渲染完成后再进行可能引起布局抖动的操作。坑3在tabBar页面使用自定义导航栏切换Tab时布局错乱。现象Tab页A有自定义导航栏切换到Tab页B没有自定义导航栏再切回来A页面的导航栏位置或样式不对。原因Tab页切换不是完整的页面生命周期attached等组件生命周期函数可能不会每次都触发。高度计算可能未执行。解决在组件中使用pageLifetimes来监听页面显示事件。// custom-navigation-bar.js Component({ // ... properties, data 等 pageLifetimes: { show() { // 当组件所在的页面被展示时执行包括Tab切换回来 this.calculateBarHeight(); }, }, // ... lifetimes, methods });坑4导航栏标题过长被截断或在小屏手机上布局拥挤。解决在中间标题的样式中我们已经设置了overflow: hidden; text-overflow: ellipsis; white-space: nowrap;来处理长标题。对于小屏手机可能需要动态调整字体大小或左右区域的宽度。可以通过wx.getSystemInfoSync().screenWidth获取屏幕宽度在组件中根据屏幕宽度动态计算一个最大标题宽度并通过样式绑定到标题元素上。
返回列表