1. 项目概述与组件背景
1.1 HarmonyOS6 时代的底部导航痛点
如果说手机应用的视觉门面是哪一块,我大概率会投票给底部导航栏。用户在单手操作模式下,大拇指覆盖频率最高的区域就是屏幕下半部分,导航栏的形状、反馈、动效直接影响操作效率和手感。在 HarmonyOS 生态里,底部导航一直是开发者又爱又恨的模块:ArkTS 的声明式写法本身不复杂,官方也提供了 Tab 相关的容器能力,但真要做出“有辨识度”的导航栏,比如中间凸起的发布按钮、内凹的扇形凹槽、带细小动效的选中态,光靠基础组件堆叠要写不少代码,平台差异和触摸事件处理还会带来一堆小坑。
rc_concave_tabbar 正是为了解决这类场景而生的一个底部导航栏组件,名字里的“concave”直译是凹面、内凹,核心视觉特征就是中间或局部区域向内凹陷,形成一个类似“弧形缺口”的形态。这种设计在电商类、工具类、社区类 App 里非常流行,用来安放核心操作按钮,比如“扫一扫”“发布”这种高频且希望被一眼看到的入口。
这套组件面向的是 HarmonyOS 6 以及对应的 API 版本,基于 ArkTS 语言与声明式 UI 体系实现。如果你正在用 DevEco Studio 做鸿蒙原生应用开发,又恰好需要在底部导航里做出中间凸起、两侧常规 Tab 的布局,或者想要自定义选中动画、角标、图标切换逻辑,这个组件能省下不少重复造轮子的时间。即使你是第一次接触 HarmonyOS 组件封装,通过读完这篇文章的拆解,也能理解它背后的实现套路:Stack 层叠布局、手势事件、自定义绘制、动画插值,这些才是真正值钱的部分。
1.2 组件能解决什么具体问题
用最直白的话说,没有这类组件之前,你要实现一个“中间凹槽导航栏”,大概要经历以下流程:
- 用 Row 排列三个或五个按钮;
- 中间按钮通过 marginTop 或者 translate 往上偏移,制造“凸出”假象;
- 凹槽的弧形背景需要自定义绘制或者直接用一张切好的背景图;
- 点击态、选中态的图标颜色切换需要手动维护状态变量;
- 遇到页面切换、返回键同步 Tab 状态,还需要自己写事件通信。
这套流程如果只做一次还好,多来几个页面模块就会很痛苦,尤其是背景切图在不同屏幕宽度、不同字体缩放下的适配问题,能把人逼疯。rc_concave_tabbar 把这些东西统一收敛到一个组件里,对外只暴露配置型 API,比如凹槽位置、高度、图标资源、选中事件回调。你把参数传进去,剩下的布局计算、触摸态、动画切换都由组件内部处理。
另外,HarmonyOS 6 的 API 版本对组件生命周期、手势冲突处理、安全区适配都有一些新要求,直接拿旧项目里的自定义组件代码过来改,很容易出现编译告警或者触摸响应异常。这个组件在命名上明确标注了 HarmonyOS6,至少说明作者在适配过程中已经处理过这些版本相关的问题,省得你再从头踩一遍。
2. 整体设计与思路拆解
2.1 为什么用“凹槽”而不是纯粹的“凸起”按钮
我之前见过很多开发者在做类似设计时,上来就说“我要中间一个按钮鼓出来”。鼓出来这个需求本身没问题,但在实际视觉呈现上,“凸起”如果处理不好,和两边的普通 Tab 在视觉重量上会产生割裂感:整个导航栏带一条横向分割线,中间一个圆形按钮浮在上面,像补丁一样。
concave 思路是反过来的:不是“中间突出来一块”,而是“在导航栏的边界处挖掉一个弧形”,让核心按钮正好嵌入这个弧形的空缺里。这样做的好处有两点。
第一,视觉过渡更自然。内凹的弧线引导视线从两侧向中心汇聚,按钮与导航栏之间是镶嵌关系而不是叠放关系,整体浑然一体。第二,触摸热区可以做大。按钮的实际尺寸可以略大于视觉缺口区域,用户更容易点中。
这种设计在一些大厂 App 中很常见,比如闲鱼底部的“发布”按钮,就是典型的中间凸起加两侧凹槽过渡。rc_concave_tabbar 的内凹形态本质上复刻了这条成熟的交互路径。组件叫“concave”,说明作者在设计时把视觉重心放在“凹”这个动作上,而不是简单地把按钮放大加阴影。
2.2 从使用方视角看组件设计的好坏
判断一个自定义组件设计得是否好用,我会重点关注三个维度:配置量、扩展性、侵入性。
配置量指的是我传入一个组件,需要写多少参数才能跑起来。如果组件暴露了十几个必填项,每个都要看文档才明白,那么即使功能强大,使用成本也偏高。rc_concave_tabbar 从名字和常见开源组件习惯推测,对外 API 应该集中在 items 配置数组、选中下标、凹槽参数、回调函数这几类。基本场景下,只需定义好 Tab 数据源并绑定事件就能看到效果,属于“配置驱动”而不是“代码驱动”。
扩展性关注的是图标和文字的灵活性。有些组件写死了只支持本地资源图片,想换成系统符号字体或者自定义绘制图形就很难。灵活的做法是暴露一个 builder 槽位或者支持自定义图标构建函数,让使用方自由决定每个 Tab 长什么样。如果 rc_concave_tabbar 支持传入自定义 builder,那么上手的想象空间就大了很多。
侵入性关注的是组件会不会污染你的业务代码结构。好的组件应该像一颗螺丝钉,拧进去就行,而不是要求你把页面架构推倒重来。它应该只负责展示和反馈,至于点击之后跳转到哪个页面、如何切换路由,那是业务层的事情,组件内部不该硬编码。
2.3 选型对比:手写 vs 开源组件 vs 官方容器
我在项目里其实尝试过三条路径,这里可以给大家一个参考。
第一条是纯手写。用 ArkTS 的 Row、Stack 加上 Canvas 自定义绘制背景弧线,把导航栏包裹在一个圆角容器内。手写的好处是逻辑透明,想怎么改就怎么改,坏处是代码量大且容易出边界问题。比如凹槽弧线的贝塞尔曲线控制点参数需要反复调,不同机型下安全区高度还不一样,整体花费的调试时间远超预期。
第二条是用官方容器组件 TabContent 加自定义 BottomBar。这种方案的问题在于 TabContent 自带一整套页面缓存和滑动联动逻辑,如果你各个页面之间并不需要通过左右滑动来回切换,只是单纯想做一个“点击底部按钮切换页面”的导航架构,官方容器反而显得笨重,尤其是想要中间凹槽这种非标准布局时,需要做很多布局 hack。
第三方独立组件是第三条路,也是我最终认为性价比最高的方案。它专注于“底部导航栏”这一件事,没有捆绑页面路由机制,也没有强制你使用某种状态管理框架。你可以把它嵌入任意页面框架中,Link 到自己的状态变量即可。
3. 核心细节解析与实操要点
3.1 凹槽区域的几何参数
凹槽是整个组件的灵魂,理解它怎么算,后面调样式才不会瞎试。
直角坐标系下,底部导航栏可以看作一个矩形区域,矩形顶边是内容区的下边界,矩形底边是屏幕安全区的上边界。concave 形态就是在矩形顶边的某个位置,让边界线向下方凹陷一个弧形深度。假设凹槽中心点位于顶边上的 x 坐标,凹槽深度为 d,则弧线可以看作一段贝塞尔曲线或者圆弧,从顶边的左侧点开始,画到右侧点结束。
在实现层面,如果使用 Canvas 绘制,通常会定义这样几个参数:
- 凹槽中心横坐标,用于确定左右偏移;
- 凹槽宽度,决定缺口在水平方向上的跨度;
- 凹槽深度,决定缺口向下挖的幅度;
- 圆角半径,决定弧线过渡的平滑程度。
注意这里有一个视觉和触觉的差异:视觉凹槽宽度是背景上的缺口,实际中间按钮的宽度通常会略大于凹槽宽度,否则按钮和缺口之间缝隙过多,看起来不够饱满。一般建议按钮宽度比凹槽宽度大 8vp 到 12vp,凸出高度比凹槽深度多 4vp 到 6vp。听起来像是细节,但这两个差值正是决定组件精致度的关键。
3.2 状态变量管理与父子通信
ArkTS 的组件通信方式比较清晰:父组件通过普通参数或者 @Prop 向子组件传值,子组件通过 @Emit 或者回调函数向外通知事件。rc_concave_tabbar 在状态管理上需要注意以下设计点。
当前选中 Tab 的索引应该由谁持有?如果组件内部持有,使用方无法外部重置选中状态,比如你在页面 A 点击一个按钮,希望导航栏强行切到某个 Tab,这时外部无法控制。如果完全由外部持有,组件内部又要维护大量同步逻辑。
常见的折中方案是:组件暴露一个名为 selectedIndex 的 @Prop 参数,使用方在父组件里维护这个变量。每次点击导航栏项时,组件触发 onSelect 回调并把新的索引传出去,父组件在回调里更新自己的状态变量。由于状态变量更新后通过 @Prop 传回组件,界面重新渲染,选中态自然切换。
这种做法虽然多了一次“回调再赋值”的往返,但好处是状态源唯一,不会出现组件内外两套状态各改各的、最终渲染不一致的问题。如果你用了 @StorageLink 或者 AppStorage 来做全局状态,也可以直接用全局变量驱动 selectedIndex,效果是一样的。
3.3 图标切换与角标展示
Tab bar 的常规能力除了图标和文字,角标也是高频需求。角标的典型使用场景是“购物车”“消息中心”这类有未读计数的入口。
在 HarmonyOS 中,角标可以通过 Badge 控件实现,也可以封装一个自定义容器,在右上角叠加一个 Text。使用 rc_concave_tabbar 时,我倾向于在 items 配置数据里预留 badge 字段,值为数字或字符串。组件内部判断字段是否为空,再决定是否绘制角标。角标的样式可以包含颜色、圆角、最大宽度显示省略号等参数。
图标切换涉及一个常见的实现细节:每个 Tab 通常有两套图标,一套是未选中态灰色,一套是选中态主题色。在 ArkTS 里比较推荐用 Image 组件配合显式切换,而不是依赖着色器滤镜去改变原始图片颜色,因为后者在部分设备上存在兼容性问题,而且颜色渲染的准确度难以保证。
如果你不想维护两套图标文件,也可以用 symbol 字体或者系统预置图标,运行时通过参数指定颜色。HarmonyOS 系统图标库覆盖面还行,常见的基础图形能覆盖到,但像“发布”“扫一扫”这类业务特征强的图标,自己准备的素材往往更有辨识度。
3.4 安全区和底部横条适配
做过移动端开发的都知道,底部导航栏最怕的就是不和屏幕底部的安全区正确适配。HarmonyOS 设备中有些是全面屏,系统会提供一个手势操作条区域;有些是物理三键导航,底部就有一条额外的系统导航栏区域。
组件的做法应该是测量安全区高度,然后在布局底部加上这个高度作为 padding。如果组件内部没有内置安全区适配,使用方需要手动传入一个 bottomOffset 参数。
检测安全区高度在 HarmonyOS 中一般依赖组件自身的展开区域信息,或者是在页面级获取窗口的避免区域。在 rc_concave_tabbar 的使用场景中,我更推荐组件内部自动处理,因为使用方如果忘了传 bottomOffset,导航栏就会被系统手势区顶住,非常难看。
4. 实操过程与核心环节实现
4.1 集成组件与最小可用示例
第一步当然是引入组件。假设你已经把 rc_concave_tabbar 的源码或 har 包放到了工程中,并通过 ohpm 或者本地 module 完成依赖配置。
在项目的 entry 模块中,找到需要展示底部导航的页面,然后在 build 方法里加入组件。先说一个最小示例的逻辑:
@Entry @Component struct IndexPage { @State currentIndex: number = 0 private tabItems: Array<TabItem> = [ { title: '首页', iconSelected: $r('app.media.home_selected'), iconNormal: $r('app.media.home_normal') }, { title: '发现', iconSelected: $r('app.media.discover_selected'), iconNormal: $r('app.media.discover_normal') }, { title: '发布', isCenter: true, centerIcon: $r('app.media.publish') }, { title: '消息', iconSelected: $r('app.media.message_selected'), iconNormal: $r('app.media.message_normal') }, { title: '我的', iconSelected: $r('app.media.mine_selected'), iconNormal: $r('app.media.mine_normal') } ] build() { Stack() { Column() { Text('当前页面:' + this.currentIndex.toString()) .fontSize(20) .fontWeight(FontWeight.Bold) } .width('100%') .layoutWeight(1) RcConcaveTabbar({ items: this.tabItems, selectedIndex: this.currentIndex, onSelect: (index: number) => { this.currentIndex = index } }) } } }这里最值得注意的一点是中间 Tab 的声明方式:isCenter 字段用于告诉组件“这一项需要特殊渲染”,也就是画凹槽、放凸起按钮。组件遍历 items 时,一旦遇到 isCenter 为 true 的项,就不再按照普通 Tab 的横向均分逻辑处理,而是单独安排位置。
4.2 背景弧线绘制的两种实现路径
凹槽背景的绘制是组件最有技术含量的地方,常见有两种路径,这里都给大家拆一下。
第一种是使用 ArkTS 提供的 Canvas 组件。在 Canvas 的 onReady 回调里获取绘图上下文,然后按照几何参数绘制带凹槽的背景形状。伪代码逻辑大致如下:
private drawBackground(ctx: CanvasRenderingContext2D, width: number, height: number) { // 先画一个铺满导航栏的矩形 ctx.fillStyle = '#FFFFFF' ctx.fillRect(0, 0, width, height) // 画凹槽:用一段圆弧把矩形顶边中间部分“挖”掉 ctx.beginPath() ctx.moveTo(0, 0) // 左边直线 ctx.lineTo(this.leftStartX, 0) // 凹槽弧线 ctx.arcTo(centerX, approxRadius, this.rightEndX, 0, approxRadius) // 右边直线 ctx.lineTo(width, 0) ctx.lineTo(width, height) ctx.lineTo(0, height) ctx.closePath() ctx.fillStyle = '#FFFFFF' ctx.fill() }在实际代码中,arcTo 的参数需要反复调试,因为弧形半径和凹槽深度之间并非简单的线性关系。我通常会给凹槽深度设置一个基准值,然后根据弧线的顺滑度微调半径。调的时候有个经验:半径越大,凹槽过渡越平缓,视觉上越像“浅碗”;半径越小,凹槽边缘越锐利,视觉上越像“被切了一刀”。产品侧如果没有明确要求,我一般建议把深度控制在 30vp 到 40vp 之间,半径控制在 60vp 到 80vp 之间,出来的效果最自然。
第二种是直接在组件基础布局上使用背景图片。把凹槽形状切好成 png 资源,用 Image 组件拉伸铺满导航栏区域。这种方案代码简单,但弊端很明显:一旦导航栏高度或者安全区高度变化,背景图的位置就会对不齐,需要重新做图。
我个人更推荐 Canvas 动态绘制方案,虽然首次写起来费点劲,但后续调整参数只需要改几个常量,不用跟设计师反复要图。
4.3 中间按钮的凸出布局与点击热区
凹槽背景画完之后,中间的凸起按钮需要放在一个独立的层中。在 ArkTS 里,组件整体外层可以用 Stack 布局,底部放画好的背景层,上层放按钮层。
按钮的定位关键是计算它的实际位置。假设导航栏宽度为 W,凹槽中心横坐标为 centerX,按钮的视觉宽度为 btnWidth,则按钮的左边距应该是 centerX - btnWidth / 2。按钮的纵坐标则更讲究:如果直接把按钮的顶部对齐导航栏顶部,那么凸出效果是在导航栏上方“站起来”,整体形态比较常见;如果想要按钮像“坐”在凹槽里,则需要把按钮中心对准凹槽弧线的底部点,让按钮的下半部分嵌入导航栏内部。
这里涉及触摸热区的问题。默认情况下,按钮的点击区域是它的边界矩形,但视觉上用户能感知的按钮区域是圆形或圆角矩形。用 ArkTS 的 Gesture 处理器做点击响应时,默认判定区域就是组件本身尺寸,不受形状裁剪影响。如果你希望“只有点到视觉圆内才算有效点击”,需要自己实现命中测试,不过这属于极致交互体验的要求,常规项目中可以直接用矩形热区,用户感知并不明显。
我建议把按钮尺寸做小一点,然后通过扩展手势判定区域来处理热区。具体做法是在按钮外侧套一层透明的容器,容器尺寸比按钮大 8vp 左右,在透明容器上绑定点击事件。这样用户即使点到按钮边缘外一点,依然能触发,容错率更高。
4.4 选中动画的插值思路
选中动画是提升细节质感的重要环节,通常包括图标切换、文字颜色渐变、以及一个小尺寸的指示条平移。
在 rc_concave_tabbar 中,如果你传入的 items 包含 title 字段,组件会根据选中状态切换文字颜色和字体权重。这个切换过程最好使用 animateTo 包裹状态变量的修改,让颜色和位置平滑过渡。
animateTo({ duration: 200, curve: Curve.EaseOut }, () => { this.currentIndex = newIndex this.indicatorOffsetX = newIndex * itemWidth })曲线选择上,EaseOut 比 Linear 更有“跟随手指快速响应,然后缓慢停稳”的手感。时长控制在 180ms 到 250ms 之间,不要太短,会显得生硬;也不要太长,会拖低操作效率。
如果你想让选中效果更有质感,还可以给图标加一个微缩放:选中时 scale(1.1),未选中时 scale(1.0)。但要注意,中间凸起按钮在业务上通常承担“核心操作”角色,不建议做缩放动效,否则会把导航栏的视觉重心放大到一种浮夸的程度。
5. 常见问题与排查技巧实录
5.1 动画失效,切换时没有过渡效果
我在用这类自定义组件时遇到最多的现象就是:点击 Tab 能切换内容,但图标颜色是“跳变”的,没有动画。排查思路主要看两个地方。
第一,animatable 属性是否被正确设置。ArkTS 里颜色变化如果想要被动画插值,需要给组件属性标记为 animatable,或者把状态放在 animateTo 的可变范围内。很多新手会在状态变量上直接做三元判断,然后不包裹 animateTo,这样颜色变化只会在下一次渲染瞬间生效。第二,animateTo 的调用时机是否在点击事件同步作用域内。如果你点击事件里先调用了一个异步函数,等数据回来后你再修改状态,动画往往会变成跳变。正确的做法是点击时立刻在同步代码段里修改状态,或者手动触发动画。
5.2 中间按钮点击穿透
偶发情况是点中间按钮,结果触发到了底层的页面列表点击事件。这个问题本质是布局层级遮挡关系不对。
ArkTS 中的 Stack 默认后续子组件会覆盖在先前的组件之上,但如果中间按钮的外层容器没有设置合适的高度,或者在视觉上是凸出,实际布局矩形还在导航栏内部,那么顶部留白区域会穿透点击到底层页面。
解决方案:在中间按钮外层套一个容器,显式设置可点击区域。如果按钮凸出导航栏顶部,需要把外层容器的高度扩展为“导航栏高度 + 凸出高度”,然后再设置 hitTestBehavior 为 HitTestMode.Block。这样凸出部分的点击事件会被容器截获,不再穿透。
另外要留意不可见区域的透明颜色值。如果你给透明容器设置了一个 alpha 接近 0 的占位颜色,有时反而会触发布局重绘,建议直接用 .opacity(0) 或者 .visibility(Visibility.None)。
5.3 深色模式下凹槽背景颜色异常
HarmonyOS 支持深色模式切换,如果你的页面自适应开启深色模式,而凹槽背景色固定为白色,那么在深色背景下会出现一块显眼的浅色“补丁”。
组件在设计时应该读取系统颜色模式,或者使用资源限定符定义不同模式下的颜色。实操上,我会在组件内部根据当前环境配置维护一个 backgroundColor 状态:
if (this.isDarkMode()) { this.backgroundColor = '#1A1C1E' } else { this.backgroundColor = '#FFFFFF' }如果你只是临时使用组件,可以用 withTheme 相关 API 或者直接从 Context 获取颜色模式。记住一个原则:不要在任何自定义组件中硬编码背景色,除非你确定业务方永远不会开深色模式。
5.4 响应式布局下凹槽中心偏移
iOS 和 Android 开发里都有类似问题,鸿蒙这边也一样:当屏幕宽度变化,或者应用支持折叠屏展开,凹槽中心如果写死在一个固定横坐标,展开后就会偏到一侧。
组件内部要根据 onAreaChange 事件动态获取宽度,然后重新计算 centerX。centerX 的计算逻辑通常是:
centerX = width / 2如果凹槽位置是可配置的,组件暴露一个 centerIndex 参数指定中心位置在第几个 Tab,然后根据总宽度和 items 数量,计算出第该 Tab 项的中心点:
const itemWidth = totalWidth / totalCount centerX = itemWidth * (centerIndex + 0.5)每次屏幕宽度变化时,需要在 onAreaChange 回调里重新计算并触发状态更新。
5.5 快速连续点击导致状态不同步
最后一个高频问题:用户快速点击不同的 Tab,可能会出现页面切换与导航栏选中态不同步。这通常是因为在 onSelect 回调里做了异步页面跳转,而页面跳转的耗时导致最后点击的索引覆盖了之前的状态。
解决方案是统一使用同步状态更新。在回调中只是修改 currentIndex,页面内容的切换通过 @Watch 监听 currentIndex 的变化去执行对应逻辑。注意不要在 onSelect 回调里直接调用路由跳转,因为此时组件内部还在渲染,多个路由压栈可能导致页面栈混乱。
我在实际项目中踩过几次这个坑,最后的架构成型为:导航栏组件只负责“我想切到第几个”,页面容器通过监听状态变量决定加载哪个子页面。几十个 Tab 切换场景下来没有出过状态错乱。
6. 提升使用体验的扩展思路
6.1 自定义样式与主题化配置
rc_concave_tabbar 如果做得足够通用,应该支持通过外部传入样式对象来定义颜色、尺寸、圆角、阴影等属性。使用方可以根据自身品牌需求定义一套主题,再传入不同页面的导航栏,保持全局风格统一。
常见的参数设计包括:
- 导航栏背景色;
- 选中态主题色与未选中态灰色;
- 文字字号与字重;
- 凹槽深度与宽度;
- 中间按钮的阴影颜色与模糊半径。
主题化配置的意义在于:当多个模块都需要底部导航栏,而每个模块的品牌配色不同,就不用复制粘贴组件代码改样式,而是传不同的主题对象进去。后续如果要整体调整圆角风格,只改一处主文件即可。
6.2 触感反馈与手势细节
移动应用越来越重视触感反馈,底部导航栏作为高频点击区域,加入轻量级振动反馈可以显著提升操作确认感。HarmonyOS 的 Vibration 接口使用起来很直接:
import vibrator from '@ohos.vibrator' vibrator.startVibration({ type: 'time', duration: 20, }, { usage: 'touch' })但要注意震动时长不要太长,10ms 到 20ms 的轻微震动即可。用户只是切个 Tab,震得手麻反而画蛇添足。另外,系统设置里如果用户关闭了触感反馈,你的振动调用会被忽略,所以不需要额外做开关拦截,交给系统统一控制即可。
6.3 结合路由框架的导航联动
单页应用通常需要底部导航栏与路由联动。rc_concave_tabbar 的 onSelect 回调可以根据点击的索引切换到对应页面。这里有两种架构选择。
第一种是 Navigation + NavDestination。把每个 Tab 对应的页面映射到一个自定义 NavDestination,切换时通过 router.pushUrl 或者 replaceUrl 完成。这种方式页面栈会比较深,需要注意返回箭头消失问题。
第二种是 Tabs 容器 + 子页面的懒加载。在 Tabs 组件里放置五个子页面,把当前 Tab 索引与 Tabs 的 currentIndex 双向绑定。这种方式的缓存和生命周期管理由 Tabs 容器统一负责,更符合主流 App 的底部导航布局。不过 Tabs 容器内部自带滑动切换手势,这一点和很多产品需求冲突,在产品侧要求“禁止左右滑动切换页面”时需要手动关闭。
我之前在实际项目里选的是第二种,因为底部导航栏的核心目的是让用户快速切换主模块,页面缓存能保留滚动位置和表单输入内容,体验明显好于每次切换都重新创建。
7. 一点心得与建议
组件用起来是一层皮,真正关键的是理解它背后的布局思想和状态同步模式。rc_concave_tabbar 看起来只是处理了底部导航栏这一小块需求,但它涉及的自定义绘制、安全区适配、触控事件透传、主题切换,几乎涵盖了 HarmonyOS 自定义组件开发中的所有常见场景。把这个组件吃透,你再去封装别的高级组件,会发现很多思路是相通的。
最后说一个小技巧:无论你最终用的组件长什么样,在集成之前,都要先在自己工程的 demo 页面里跑一遍,检查深色模式、大字体、不同宽高比三种情况下的表现。这三种环境是最容易暴露自定义组件问题的,也是最容易被开发者和设计者忽略的。做好了这三个场景的适配,你的导航栏在大多数真机上都不会出大差错。