鸿蒙原生 ArkTS 布局方式之 Navigation + toolbar 工具栏布局深度解析

一、引言

在移动应用开发中,底部工具栏(Bottom Toolbar) 是最常见的导航模式之一。微信的「微信/通讯录/发现/我」、淘宝的「首页/微淘/消息/购物车/我的」、京东、美团等主流应用均采用这种布局。其核心优势在于:操作热区固定在拇指最易触及的屏幕底部;入口层级扁平,用户无需深层跳转即可到达一级页面;视觉结构清晰,底部工具栏成为稳定的视觉锚点。

本文基于 API 24,深入剖析 Navigation + toolbar 布局的实现原理与最佳实践,并通过完整示例代码帮助读者快速掌握。


二、Navigation 组件概述

2.1 组件结构

Navigation 是 ArkUI 的路由容器组件,天然划分为三个功能区:

┌──────────────────────────────────┐
│         导航栏(NavBar)          │
│  ┌─ 标题区 ──── 菜单区 ─┐       │
│  │  鸿蒙示例       [搜][…]│      │
│  └────────────────────────┘      │
├──────────────────────────────────┤
│           内容区(Content)       │
├──────────────────────────────────┤
│ 首页 │ 分类 │ 消息 │ 购物车 │ 我的 │
│          工具栏(Toolbar)       │
└──────────────────────────────────┘

2.2 显示模式

模式 枚举 适用场景
单栏 NavigationMode.Stack 手机竖屏,标准导航
分栏 NavigationMode.Split 平板/折叠屏
自适应 NavigationMode.Auto 自动切换,响应式

底部工具栏场景主要用 Stack(默认值)。

2.3 API 24 关键变化

  • ToolbarItemBarMenuItem 成为内置类型,直接从全局空间使用,无需 import
  • ArkTS 严格模式:禁止 as any,对象字面量须显式标注类型,禁止类型名称冲突
  • 符号图标体系成熟$r('sys.symbol.*') 替代旧的 $r('sys.media.*')
  • 注意:不能声明与内置类型同名的接口(如 interface ToolbarItem),否则触发 arkts-no-decl-merging 错误

三、核心 API 详解

3.1 toolbarConfiguration

设置工具栏内容,核心配置入口:

toolbarConfiguration(
  value: Array<ToolbarItem> | CustomBuilder,
  options?: NavigationToolbarOptions
): NavigationAttribute;
参数 类型 说明
value ToolbarItem[]CustomBuilder 工具栏内容
options NavigationToolbarOptions 样式选项(背景色、模糊等)

3.2 ToolbarItem 接口

内置类型,无需导入:

interface ToolbarItem {
  value: ResourceStr;           // 文本标签(必填)
  icon?: ResourceStr;           // 图标(可选)
  action?: () => void;          // 点击回调(可选)
  status?: ToolbarItemStatus;   // NORMAL / ACTIVE / DISABLED
  badge?: ToolbarBadge;         // 角标
  activeIcon?: ResourceStr;     // ACTIVE 态专用图标
  symbolIcon?: SymbolGlyphModifier;       // Symbol 图标
  activeSymbolIcon?: SymbolGlyphModifier; // ACTIVE 态 Symbol
}

3.3 ToolbarItemStatus 枚举

说明
NORMAL 正常态,可交互
ACTIVE 激活态,显示 activeIcon
DISABLED 禁用态,灰色不可点

3.4 NavigationToolbarOptions

interface NavigationToolbarOptions {
  backgroundColor?: ResourceColor;
  backgroundBlurStyle?: BlurStyle;         // 背景模糊
  backgroundBlurStyleOptions?: BlurStyleOptions;
  hideItemValue?: boolean;                 // 是否隐藏文本(仅图标)
}

3.5 menus 与 BarMenuItem

右上角菜单按钮配置:

interface BarMenuItem {
  value: ResourceStr;
  icon?: ResourceStr;
  action?: () => void;
}

竖屏最多显示 3 个图标菜单,超出自动折叠。

3.6 titleMode

模式 说明
Full 大标题(112vp),适合首页
Mini 紧凑小标题(56vp),适合二级页
Free 滚动联动,标题随内容缩放

四、完整示例代码

@Entry
@Component
struct Index {
  @State currentTabIndex: number = 0;
  private readonly TAB_COUNT: number = 5;
  private readonly tabLabels: string[] = ['首页','分类','消息','购物车','我的'];
  private readonly tabIcon: Resource = $r('app.media.startIcon');
  private readonly tabEmojis: string[] = ['🏠','📂','💬','🛒','👤'];

  get toolbarItems(): ToolbarItem[] {
    const items: ToolbarItem[] = [];
    for (let i = 0; i < this.TAB_COUNT; i++) {
      const item: ToolbarItem = {
        value: this.tabLabels[i],
        icon: this.tabIcon,
        action: () => { this.onTabClicked(i); },
      };
      items.push(item);
    }
    return items;
  }

  get menuItems(): BarMenuItem[] {
    return [
      { value: '搜索', icon: $r('app.media.startIcon'),
        action: () => { console.info('点击搜索'); } },
      { value: '更多', icon: $r('app.media.startIcon'),
        action: () => { console.info('点击更多'); } },
    ];
  }

  onTabClicked(index: number): void {
    if (this.currentTabIndex === index) return;
    this.currentTabIndex = index;
  }

  build() {
    Navigation() {
      Scroll() {
        Column() {
          Text(this.tabLabels[this.currentTabIndex])
            .fontSize(30).fontWeight(FontWeight.Bold)
            .margin({ top: 20, bottom: 2 });

          Row().width(28).height(4)
            .backgroundColor('#FF007AFF').borderRadius(2)
            .margin({ bottom: 24 });

          Column() {
            Text(this.tabEmojis[this.currentTabIndex]).fontSize(72);
            Text(`${this.tabLabels[this.currentTabIndex]}」页面`)
              .fontSize(20).fontWeight(FontWeight.Medium);
            Text('点击底部工具栏切换页面').fontSize(14)
              .fontColor('#FF999999');
          }
          .width('90%').padding(24)
          .backgroundColor('#FFF8F9FA').borderRadius(16)
          .alignItems(HorizontalAlign.Center);
        }
        .width('100%').padding({ top: 8 })
        .alignItems(HorizontalAlign.Center);
      }
      .width('100%').height('100%').scrollBar(BarState.Off);
    }
    .titleMode(NavigationTitleMode.Full)
    .title('鸿蒙示例')
    .menus(this.menuItems)
    .toolbarConfiguration(this.toolbarItems)
    .hideToolBar(false)
    .navBarWidth('100%').height('100%');
  }
}

代码要点

  • 响应式状态@State currentTabIndex 驱动工具栏选中态与内容联动
  • 计算属性get toolbarItems() 动态构建数组,状态变化时自动重算
  • 闭包捕获action: () => { this.onTabClicked(i); } 用块级作用域正确捕获 i
  • 链式配置:Navigation 属性全部通过链式调用配置

五、布局要点与最佳实践

5.1 工具栏项数量控制

竖屏模式下最多显示 5 个图标项,多余的会被自动放入「更多」菜单。设计上建议保持 4~5 项——过少显得空旷,过多则核心入口被折叠降低效率。工具栏选项的排列顺序也有讲究:最重要、最常用的入口放在最左侧和中间,因为这两个位置拇指点击最方便,符合用户从左到右的阅读习惯。

5.2 选中态图标切换

如果希望选中和未选中显示不同图标,可以利用 activeIcon 字段:

const item: ToolbarItem = {
  value: '首页',
  icon: $r('app.media.ic_home_normal'),      // 未选中
  activeIcon: $r('app.media.ic_home_active'), // 选中态
  action: () => {},
};

系统会将 ACTIVE 态选项自动切换为 activeIcon。如果使用 Symbol 图标体系(推荐),则用 symbolIconactiveSymbolIcon

5.3 角标配置

工具栏项支持角标来展示未读消息数、购物车商品数等动态信息:

badge: {
  count: 99,
  maxCount: 99,        // 超过 maxCount 显示 "99+"
  position: BadgePosition.TOP_RIGHT,
}

实现时注意:count 为 0 时应动态移除角标对象或设为 undefined,避免显示「0」给用户造成困惑。

5.4 工具栏纯图标模式

.toolbarConfiguration(this.toolbarItems, {
  hideItemValue: true,  // 仅显示图标
})

适合 Tab 数量少且图标辨识度高的场景,比如底部只有 3 个功能截然不同的入口。

5.5 背景模糊效果

.toolbarConfiguration(items, {
  backgroundBlurStyle: BlurStyle.COMPONENT_REGULAR,
})

毛玻璃效果让工具栏与内容区产生空间层次感,配合沉浸式内容尤其出色。注意模糊效果会增加渲染开销,低端设备谨慎使用。

5.6 结合路由系统

真正的工程化项目中,工具栏入口通常需要配合 NavPathStack 实现路由跳转:

private navPathStack: NavPathStack = new NavPathStack();
Navigation(this.navPathStack) { }
.toolbarConfiguration([
  { value: '首页', icon: $r('app.media.ic_home'),
    action: () => { this.navPathStack.clear(); } },
  { value: '详情', icon: $r('app.media.ic_detail'),
    action: () => { this.navPathStack.pushPath({ name: 'Detail' }); } },
])

pushPath 入栈新页面、pop 返回上一页、clear 回到根页面——路由栈的操作与工具栏无缝配合。

5.7 自定义工具栏(CustomBuilder)

当均分模式不够灵活时(如需要居中大按钮),使用 CustomBuilder 完全自定义:

@Builder
customToolbar() {
  Row() {
    Button('发布').width(56).height(56)
      .backgroundColor('#FF007AFF').borderRadius(28);
  }
  .width('100%').justifyContent(FlexAlign.Center);
}
.toolbarConfiguration(() => { this.customToolbar(); })

自定义模式失去自动均分和 ACTIVE 态管理能力,一切由开发者自己控制,适合发布按钮、扫码入口等特殊交互。


六、常见编译错误排查

在 API 24 的 ArkTS 严格模式下,以下错误最为高频。掌握它们的根因和修复方法,可以大幅减少编译调试时间。

6.1 声明冲突 arkts-no-decl-merging

ERROR: Declaration merging is not supported (arkts-no-decl-merging)

原因:在代码中声明了与 SDK 内置类型同名的接口。API 24 中 ToolbarItemBarMenuItemMenuItem 等类型均已内置在全局空间中,不需要也不允许重复声明。

解决:删除本地接口声明,直接使用内置类型名称。

6.2 禁止 any (arkts-no-any-unknown)

ERROR: Use explicit types instead of "any", "unknown" (arkts-no-any-unknown)

原因:使用了 as anyas unknown 类型断言,这被 ArkTS 严格模式禁止。

解决:为变量添加准确类型注解,而非靠断言逃避类型检查:

// ❌ 错误
const item = { value: '首页', icon: icon, action: fn } as any;

// ✅ 正确
const item: ToolbarItem = { value: '首页', icon: icon, action: fn };

6.3 对象字面量未标注类型 (arkts-no-untyped-obj-literals)

ERROR: Object literal must correspond to some explicitly declared class or interface

原因:直接使用对象字面量而未标注类型。

解决:在变量声明处添加类型标注:

const item: ToolbarItem = { value: '首页', icon: icon, action: fn };

6.4 toolbarBorder 属性不存在

ERROR: Property 'toolbarBorder' does not exist on type 'NavigationAttribute'

原因:API 24 已移除该属性,工具栏边框样式通过 NavigationToolbarOptions 配置。

解决:删除 .toolbarBorder() 或改用选项参数中的 backgroundColor 等间接实现。


七、性能优化

7.1 缓存工具栏数组

工具栏配置项如果内容固定(如只有文字和图标变化),可以在 aboutToAppear 中预构建一次,避免每次 @State 变化都重新计算:

private cachedItems: ToolbarItem[] = [];

aboutToAppear(): void {
  this.cachedItems = this.tabLabels.map((label, idx) => ({
    value: label,
    icon: this.tabIcon,
    action: () => { this.onTabClicked(idx); },
  }));
}

get toolbarItems(): ToolbarItem[] {
  return this.cachedItems;
}

注意闭包中引用的 idxmap 回调中已被正确捕获,不会出现「所有按钮都指向最后一个索引」的问题。

7.2 SVG 矢量图标

工具栏图标推荐使用 SVG 矢量图,尺寸建议 24vp × 24vp。相比 PNG,SVG 体积小、无锯齿、支持主题色随系统自动切换。图标资源放在 resources/base/media/ 目录下,通过 $r('app.media.xxx') 引用。

7.3 懒加载列表

如果工具栏中间的内容区包含长列表(如电商首页的商品流),务必使用 LazyForEach 替代 ForEach

LazyForEach(this.dataSource, (item: Item) => {
  ListItem() { ItemCard({ data: item }); }
}, (item: Item) => item.id)

LazyForEach 只渲染可见区域的列表项,内存占用和渲染性能远优于全量渲染的 ForEach


八、总结

Navigation + toolbar 布局是 HarmonyOS NEXT 开发中最核心的页面模式之一,也是每个鸿蒙应用开发者必须掌握的技能。本文从组件结构、API 详解、完整示例、最佳实践、常见错误到性能优化,覆盖了该布局的完整知识体系。

核心要点回顾:

  1. Navigation 三区结构:导航栏 + 内容区 + 工具栏,一组件承载完整页面框架
  2. toolbarConfiguration 配置:通过 ToolbarItem[] 数组快速构建底部入口
  3. 状态管理@State 驱动选中态切换,响应式更新页面内容
  4. API 24 语法规范:内置类型直达使用、对象字面量显式标注、禁止 any 断言
  5. 错误排查:声明冲突、类型标注遗漏、已废弃属性引用——掌握这三类即可解决 90% 的编译问题

实践建议:

  • 优先使用系统内置的 ToolbarItemBarMenuItem 类型,不要自定义同名接口
  • 保持工具栏数量在 4~5 个,避免超出后自动折叠影响用户体验
  • 使用 SVG 矢量图标兼顾清晰度与性能,尺寸统一为 24vp
  • 确保 formap 循环中正确捕获闭包变量,避免按钮点击全部指向最后一项
  • 遇到编译错误时先确认是否为 API 内置类型与本地声明的冲突

HarmonyOS NEXT 的 ArkUI 框架正在快速发展,Navigation 组件的 API 也在持续演进。本文基于 API 24 编写,核心概念在后续版本中应保持兼容,但具体语法细节请以华为官方文档为准。建议开发者关注每次 SDK 升级的 API 变更日志,及时调整代码以利用新特性和修复。


附录:API 版本速查

功能 API 10~11 API 12~13 API 24
工具栏类型 ToolbarItem(需导入) ToolbarItem(需导入) ToolbarItem(内置)
菜单类型 NavigationMenuItem BarMenuItem BarMenuItem(内置)
工具栏样式 基础 NavigationToolbarOptions 同左
严格模式 宽松 逐步收紧 严格

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐