运行环境:HarmonyOS NEXT 6.1.1(API 24)
开发工具:DevEco Studio
目标设备:手机 / 平板 / PC
代码仓库:本文所有示例代码均可在文末获取,已通过 PreBuildApp 编译验证


项目演示

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

一、引言:为什么选择 Navigation + toolbar?

在 HarmonyOS 应用开发中,底部工具栏(Bottom Navigation)是最常见的界面模式之一。无论是电商应用的「首页 / 分类 / 购物车 / 我的」,还是社交应用的「消息 / 联系人 / 动态 / 设置」,底部工具栏都承担着核心导航的角色。

1.1 HarmonyOS NEXT 的技术演进

HarmonyOS NEXT(API 24)是鸿蒙操作系统的重要里程碑,它彻底移除了 AOSP 兼容层,实现了全栈鸿蒙原生体验。与之前版本相比,NEXT 在以下方面进行了革命性升级:

特性 HarmonyOS 旧版 HarmonyOS NEXT (API 24)
UI 框架 ArkUI(部分兼容) ArkUI 纯原生,无兼容负担
开发语言 ArkTS / JS 纯 ArkTS,类型安全增强
应用模型 Stage + FA 仅 Stage 模型
组件导航 Navigation(基础) Navigation 2.0,支持 Transformer 模式
工具栏 toolBar(已废弃) toolbarConfiguration 全新设计
跨端能力 需要适配 一次开发,多端部署增强

1.2 底部工具栏的实现方案对比

在 HarmonyOS NEXT 中,实现底部工具栏主要有三种方案,每种方案各有适用场景:

方案一:Tabs + TabContent

Tabs({ index: currentIndex }) {
  TabContent() { HomePage() }
  TabContent() { MessagePage() }
  TabContent() { ProfilePage() }
}

优点:简单直接,内置动画效果;缺点:TabBar 样式受限,自定义程度低。

方案二:自定义 Row + 状态切换

Row() {
  Column() { Icon(...); Text('首页') }.onClick(() => { this.index = 0 })
  Column() { Icon(...); Text('消息') }.onClick(() => { this.index = 1 })
  Column() { Icon(...); Text('我的') }.onClick(() => { this.index = 2 })
}

优点:完全自定义布局;缺点:需要手动管理 Navigation,代码冗余。

方案三:Navigation + toolbarConfiguration(推荐)

Navigation(this.navStack) {
  ContentArea()
}
.toolbarConfiguration([
  { value: '首页', icon: $r('app.media.home'), action: () => { ... } },
  { value: '消息', icon: $r('app.media.msg'), action: () => { ... } },
  { value: '我的', icon: $r('app.media.me'), action: () => { ... } }
])

优点:官方推荐,与路由集成,性能最优;支持 Transformer 模式,动态隐藏工具栏;API 24 新特性,持续维护。缺点:学习曲线稍陡。

本文将聚焦于方案三,系统性地讲解如何使用 Navigation + toolbarConfiguration 构建高质量的底部工具栏。

1.3 本文学习路线图

为了帮助读者快速掌握,我们按照「理论 → 实战 → 进阶」的路径组织内容:

  • 第一章:Navigation 架构解析(三层结构、NavPathStack、显示模式)
  • 第二章:toolbarConfiguration 深度解析(ToolbarItem、与旧 API 对比、Transformer 模式)
  • 第三章:完整实战项目(项目配置、三页面实现、路由跳转、暗黑模式)
  • 第四章:进阶技巧(多设备适配、状态管理、性能优化)
  • 第五章:总结与展望

二、Navigation 组件架构深度解析

Navigation 是 HarmonyOS ArkUI 中最核心的导航组件之一。在 API 24 中,Navigation 经历了重大升级,新增了 Transformer 模式,优化了工具栏配置,使其成为底部工具栏布局的首选方案

2.1 三层架构模型

Navigation 组件采用经典的三层容器模型,从上到下分别是标题栏、内容区和工具栏:

┌─────────────────────────────────────────────────────────────────┐
│                         Navigation                                │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  Title Bar(标题栏)                                       │    │
│  │  ┌─────────┬─────────────────────┬─────────────────┐    │    │
│  │  │ 返回按钮 │   页面标题            │   右侧菜单       │    │    │
│  │  └─────────┴─────────────────────┴─────────────────┘    │    │
│  └─────────────────────────────────────────────────────────┘    │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  Content Area(内容区)                                    │    │
│  │  ┌─────────────────────────────────────────────────┐    │    │
│  │  │  NavDestination(当前页面)                        │    │    │
│  │  │  - 首页内容 / 消息列表 / 个人中心                  │    │    │
│  │  │  - 支持滚动、嵌套布局等                            │    │    │
│  │  └─────────────────────────────────────────────────┘    │    │
│  └─────────────────────────────────────────────────────────┘    │
│  ┌─────────────────────────────────────────────────────────┐    │
│  │  Tool Bar(工具栏)                                       │    │
│  │  ┌──────────┬──────────┬──────────┬──────────┐          │    │
│  │  │  首页 🏠 │  消息 💬 │  发现 🔍 │  我的 👤 │          │    │
│  │  └──────────┴──────────┴──────────┴──────────┘          │    │
│  └─────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘
2.1.1 标题栏(Title Bar)

标题栏位于 Navigation 顶部,用于展示当前页面的标题和提供常用操作入口。API 24 支持三种标题栏模式:

模式 枚举值 视觉效果 适用场景
Mini 模式 NavigationTitleMode.Mini 紧凑小巧 二级页面,不需要突出标题
Full 模式 NavigationTitleMode.Full 高度翻倍,标题醒目 一级页面,强调品牌或功能
Free 模式 NavigationTitleMode.Free 动态伸缩,跟随手势 滚动页面,沉浸式体验

基础配置示例:

Navigation(this.navStack) {
  ContentArea()
}
.title('我的应用')
.titleMode(NavigationTitleMode.Mini)
.hideBackButton(true)
2.1.2 内容区(Content Area)

内容区是 Navigation 的核心区域,实际承载页面内容。它由 NavPathStack 管理,采用栈结构存储页面。

内容区有两种状态:首页状态显示 Navigation 的直接子组件,子页状态显示 NavDestination 组件。

Navigation(this.navStack) {
  Column() {
    Text('首页内容').fontSize(24)
    Button('跳转到详情页')
      .onClick(() => {
        this.navStack.pushPathByName('DetailPage', { id: 123 })
      })
  }
}
.navDestination(this.pageMap)
2.1.3 工具栏(Tool Bar)

工具栏位于 Navigation 底部,是本文的重点。在 API 24 中,工具栏通过 toolbarConfiguration 属性配置,支持三种配置方式:

// 方式一:ToolbarItem 数组(基础方式)
.toolbarConfiguration([
  { value: '首页', icon: $r('app.media.home'), action: () => { } },
  { value: '消息', icon: $r('app.media.msg'), action: () => { } }
])

// 方式二:带 Transformer 配置(API 24 新增)
.toolbarConfiguration([...], {
  collapsible: true,
  collapseIcon: $r('app.media.more')
})

// 方式三:自定义 Builder(完全自定义布局)
.toolbarConfiguration(this.CustomToolbarBuilder)

2.2 NavPathStack 路由栈详解

NavPathStack 是 Navigation 的「大脑」,负责管理页面栈。每一个 Navigation 组件必须绑定一个 NavPathStack 实例。

2.2.1 创建 NavPathStack
@Entry
@Component
struct Index {
  // 创建 NavPathStack 实例,必须使用 @State 装饰器
  @State navStack: NavPathStack = new NavPathStack()

  build() {
    Navigation(this.navStack) { }
  }
}
2.2.2 NavPathStack 核心 API
方法 说明 示例
pushPath() 入栈(带返回回调) pushPath({ name: ‘Page1’, param: data })
pushPathByName() 按名称入栈 pushPathByName(‘DetailPage’, 123)
pop() 出栈 pop()
popToName() 弹出到指定页面 popToName(‘HomePage’)
replacePath() 替换当前页 replacePath({ name: ‘NewPage’ })
clear() 清空栈到首页 clear()
size() 获取栈大小 const count = this.navStack.size()
top() 获取栈顶页面信息 const top = this.navStack.top()
2.2.3 带返回数据的跳转

从 API 24 开始,NavPathStack 支持更优雅的返回数据机制:

// 首页:跳转到详情页,设置返回回调
this.navStack.pushPathByName('DetailPage', { id: 123 }, (popInfo: PopInfo) => {
  console.info('返回数据:', popInfo.result)
})

// 详情页:返回首页,同时传递数据
NavDestination() {
  Button('提交并返回')
    .onClick(() => {
      this.navStack.pop({ result: { success: true, data: 'xxx' } })
    })
}
.title('详情页')

2.3 显示模式:多设备适配的基础

Navigation 的显示模式(Mode)决定了它在不同屏幕尺寸下的布局方式。

2.3.1 Stack 模式(单栏)

适用于手机等小屏设备,每次只显示一个页面:

Navigation(this.navStack) { }
.mode(NavigationMode.Stack)
2.3.2 Split 模式(分栏)

适用于平板、PC 等大屏设备,左右分栏显示:

Navigation(this.navStack) { }
.mode(NavigationMode.Split)
.navBarWidth(280)
.navBarWidthRange([200, 360])
2.3.3 Auto 模式(自适应)

最智能的模式,根据屏幕宽度自动在 Stack 和 Split 之间切换:

Navigation(this.navStack) { }
.mode(NavigationMode.Auto)

切换阈值:API 24 默认屏幕宽度 >= 600vp 时切换为 Split 模式,可通过 minContentWidth() 自定义。

2.3.4 模式选择建议
设备类型 推荐模式 说明
手机(Phone) Stack 单栏沉浸式体验
平板(Tablet) Auto 或 Split 大屏利用更高效
PC Split 充分利用宽屏
折叠屏 Auto 展开/折叠自动适配

本章小结

本章我们深入剖析了 Navigation 组件的三层架构模型:标题栏负责页面标识,内容区承载实际业务,工具栏提供导航入口。同时讲解了 NavPathStack 的路由管理能力和三种显示模式的适用场景。

在 API 24 中,Navigation 组件有以下关键更新需要记住:

  • 新增 Transformer 模式,支持动态隐藏工具栏
  • NavPathStack 支持返回数据的回调机制
  • 标题栏支持 Free 模式(跟随手势伸缩)
  • 旧版 toolBar 属性已废弃,请使用 toolbarConfiguration

下一章,我们将聚焦于核心重点:toolbarConfiguration 的深度配置技巧。


三、toolbarConfiguration 深度解析

toolbarConfiguration 是 Navigation 组件配置底部工具栏的核心属性。在 API 24 中,它取代了旧版的 toolBar(已废弃),提供了更灵活、更强大的工具栏配置能力。

3.1 新旧 API 对比与迁移指南

在学习新 API 之前,我们需要了解新旧 API 的差异,以便顺利迁移现有代码。

3.1.1 废弃的 toolBar 属性

旧版 toolBar 属性从 API 10 开始就已标记为废弃,但仍可使用。它的配置方式如下:

// 旧版 API(已废弃,仅作了解)
Navigation(this.navStack) {
  Content()
}
.toolBar({
  items: [
    {
      icon: 'path/to/home.png',
      value: '首页',
      action: () => { console.log('点击首页') }
    },
    {
      icon: 'path/to/message.png',
      value: '消息',
      action: () => { console.log('点击消息') }
    }
  ]
})
3.1.2 新版 toolbarConfiguration 属性

新版 API 采用了更清晰的配置结构,支持更多扩展选项:

// 新版 API(推荐使用,API 10+)
Navigation(this.navStack) {
  Content()
}
.toolbarConfiguration([
  {
    icon: $r('app.media.home'),  // 使用资源引用
    value: '首页',
    action: () => { console.log('点击首页') }
  },
  {
    icon: $r('app.media.message'),
    value: '消息',
    action: () => { console.log('点击消息') }
  }
])
3.1.3 新旧 API 对照表
特性 toolBar(废弃) toolbarConfiguration(新版)
引入版本 API 8 API 10
状态 已废弃 推荐使用
配置方式 对象(items 数组) 直接传数组
图标类型 仅支持路径字符串 支持 Resource、SymbolGlyph 等
扩展配置 支持 Transformer 等高级配置
兼容性 仍可使用但不建议 持续维护,新特性优先
迁移难度 - 低,结构相似

3.2 ToolbarItem 数据结构详解

ToolbarItem 是 toolbarConfiguration 的核心数据类型,每个 ToolbarItem 代表一个工具栏按钮。

3.2.1 基础属性
// ToolbarItem 的结构定义(简化)
interface ToolbarItem {
  // 必填:图标资源,支持多种类型
  icon: Resource | string | SymbolGlyph;
  
  // 必填:显示的文字
  value: string;
  
  // 必填:点击事件回调
  action: () => void;

  // 可选:是否启用徽章(API 12+)
  badge?: string;

  // 可选:是否禁用(API 12+)
  enabled?: boolean;

  // 可选:键盘快捷键(API 12+,PC/平板有效)
  shortcut?: ToolbarItemShortcut;
}
3.2.2 icon 属性的多种类型

icon 属性是 ToolbarItem 中最灵活的字段,支持多种资源类型:

类型一:资源引用(推荐)

icon: $r('app.media.home')  // 引用 resources/media 下的资源

类型二:网络图片

icon: 'https://example.com/icons/home.png'

类型三:SymbolGlyph(图标字体,API 11+)

icon: SymbolGlyphResource.HOME  // 使用系统图标
3.2.3 完整配置示例

下面是一个完整的工具栏配置示例,展示了 ToolbarItem 的所有属性:

@Entry
@Component
struct FullToolbarExample {
  @State navStack: NavPathStack = new NavPathStack()
  @State currentTab: number = 0
  @State unreadCount: number = 3  // 未读消息数

  build() {
    Navigation(this.navStack) {
      this.ContentArea()
    }
    .title('我的应用')
    .toolbarConfiguration([
      {
        icon: $r('app.media.startIcon'),
        value: '首页',
        action: () => { this.currentTab = 0 }
      },
      {
        icon: $r('app.media.startIcon'),
        value: '消息',
        // 使用徽章显示未读消息数(API 12+)
        badge: this.unreadCount.toString(),
        action: () => { 
          this.currentTab = 1
          this.unreadCount = 0  // 点击后清除徽章
        }
      },
      {
        icon: $r('app.media.startIcon'),
        value: '发现',
        action: () => { this.currentTab = 2 }
      },
      {
        icon: $r('app.media.startIcon'),
        value: '我的',
        action: () => { this.currentTab = 3 }
      }
    ])
    .hideToolBar(false)
    .mode(NavigationMode.Stack)
    .width('100%')
    .height('100%')
  }

  @Builder
  ContentArea() {
    // 根据 currentTab 显示对应内容
    if (this.currentTab === 0) {
      this.HomeContent()
    } else if (this.currentTab === 1) {
      this.MessageContent()
    } else if (this.currentTab === 2) {
      this.DiscoverContent()
    } else {
      this.ProfileContent()
    }
  }

  @Builder
  HomeContent() {
    Column() {
      Text('🏠 首页').fontSize(24).fontWeight(FontWeight.Bold)
      Text('欢迎来到首页!').fontSize(14).fontColor('#666666').margin({ top: 10 })
    }
    .width('100%').height('100%')
    .justifyContent(FlexAlign.Center)
  }

  @Builder
  MessageContent() {
    Column() {
      Text('💬 消息中心').fontSize(24).fontWeight(FontWeight.Bold)
      List({ space: 12 }) {
        ListItem() { this.MessageItem('系统通知', '欢迎使用', '10:30') }
        ListItem() { this.MessageItem('订单消息', '您的订单已发货', '09:15') }
      }
      .width('100%').layoutWeight(1).margin({ top: 20 })
    }
    .width('100%').height('100%').padding(16)
  }

  @Builder
  MessageItem(title: string, content: string, time: string) {
    Row() {
      Column() {
        Text(title).fontSize(16).fontWeight(FontWeight.Medium).fontColor('#333333')
        Text(content).fontSize(14).fontColor('#666666').margin({ top: 4 })
      }
      .layoutWeight(1).alignItems(HorizontalAlign.Start)
      Text(time).fontSize(12).fontColor('#999999')
    }
    .width('100%').padding(16)
    .backgroundColor(Color.White).borderRadius(8)
  }

  @Builder
  DiscoverContent() {
    Column() {
      Text('🔍 发现').fontSize(24).fontWeight(FontWeight.Bold)
      Text('探索更多精彩内容').fontSize(14).fontColor('#666666').margin({ top: 10 })
    }
    .width('100%').height('100%')
    .justifyContent(FlexAlign.Center)
  }

  @Builder
  ProfileContent() {
    Column() {
      Text('👤 个人中心').fontSize(24).fontWeight(FontWeight.Bold)
      Stack() {
        Circle().width(80).height(80).fill('#007DFF')
        Text('👤').fontSize(40)
      }.margin({ top: 20 })
      Text('用户昵称').fontSize(18).margin({ top: 10 })
      Text('user@example.com').fontSize(14).fontColor('#999999')
    }
    .width('100%').height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

3.3 Transformer 模式:API 24 的新亮点

Transformer 模式是 API 24 引入的创新功能,允许工具栏根据页面状态动态变化。这为沉浸式体验和复杂交互提供了可能。

3.3.1 Transformer 的基本概念

Transformer 模式下,工具栏不再是固定显示的,而是可以根据条件实现:

  • 自动隐藏/显示
  • 改变位置(顶部/底部)
  • 调整透明度
  • 应用平滑动画
3.3.2 配置 Transformer 模式
Navigation(this.navStack) {
  Scroll() {
    Column({ space: 16 }) {
      ForEach(this.dataList, (item: string) => {
        Text(item).width('100%').padding(16).backgroundColor(Color.White)
      }, (item: string) => item)
    }
    .width('100%').padding(16)
  }
  .scrollBar(BarState.On)
  .scrollBarWidth(2)
}
.title('可滚动页面')
// 开启 Transformer 模式
.enableTransformerMode(true)
// 配置工具栏的 Transformer 行为
.toolbarConfiguration(this.toolbarItems, {
  // 滚动时隐藏工具栏
  hideWhenScroll: true,
  // 隐藏动画时长(毫秒)
  animationDuration: 200,
  // 隐藏阈值:滚动超过此数值时隐藏
  hideThreshold: 100,
  // 滚动方向:垂直滚动时触发
  scrollDirection: ScrollDirection.Vertical
})
3.3.3 Transformer 配置对象详解
// toolbarConfiguration 的第二个参数配置
interface ToolbarTransformerConfig {
  // 滚动时是否自动隐藏工具栏
  hideWhenScroll?: boolean;

  // 隐藏/显示动画时长(毫秒)
  animationDuration?: number;

  // 触发隐藏的滚动阈值(vp)
  hideThreshold?: number;

  // 滚动方向限制(API 12+)
  scrollDirection?: ScrollDirection;

  // 最大隐藏距离(API 12+)
  maxHideDistance?: number;

  // 是否在拖动结束后立即隐藏(API 12+)
  hideOnDragEnd?: boolean;
}
3.3.4 Transformer 模式的使用场景

Transformer 模式特别适用于以下场景:

  1. 内容浏览型应用:阅读、视频等沉浸式体验
  2. 长列表页面:购物商品列表、新闻列表等
  3. 全屏操作:相机、绘图等需要最大化内容区域的场景

3.4 工具栏的显示控制

Navigation 提供了多个属性来精细控制工具栏的显示状态。

3.4.1 showHideToolBar:显隐切换
Navigation(this.navStack) {
  Content()
}
.toolbarConfiguration(this.toolbarItems)
// 切换工具栏显示状态
.showHideToolBar(this.toolbarVisible)
3.4.2 hideToolBar:强制隐藏
Navigation(this.navStack) {
  Content()
}
.toolbarConfiguration(this.toolbarItems)
// 强制隐藏工具栏(优先级高于 showHideToolBar)
.hideToolBar(true)
3.4.3 NavDestination 级别控制

可以在子页面级别单独控制工具栏显示,实现不同页面不同策略:

// 首页:显示工具栏
NavDestination() {
  HomePage()
}
.title('首页')
.hideToolBar(false)

// 详情页:隐藏工具栏
NavDestination() {
  DetailPage()
}
.title('详情页')
.hideToolBar(true)

// 全屏页面:完全沉浸式
NavDestination() {
  FullScreenPage()
}
.title('全屏浏览')
.hideToolBar(true)
.hideTitleBar(true)

3.5 最佳实践与注意事项

3.5.1 工具栏数量建议
数量 推荐度 说明
2-4 个 强烈推荐 信息密度适中,操作清晰
5 个 可以接受 注意布局空间
超过 5 个 不推荐 建议合并或使用更多菜单
3.5.2 状态栏颜色适配

工具栏需要与状态栏协调显示。推荐使用系统 API 自动适配:

Navigation(this.navStack) {
  Content()
}
// 状态栏样式(API 12+)
.statusBarStyle(StatusBarStyle.LIGHT_CONTENT)
// 状态栏背景色
.statusBarBackgroundColor(this.isDarkMode ? '#000000' : '#FFFFFF')
// 沉浸式状态栏
.statusBarEnable(true)
3.5.3 安全区域适配

确保工具栏在不同设备(如异形屏、全面屏)上正确显示:

Navigation(this.navStack) {
  Content()
}
// 自动适配安全区域(API 11+ 默认启用)
.expandSafeArea(
  [SafeAreaType.SYSTEM, SafeAreaType.KEYBOARD, SafeAreaType.CUTOUT],
  [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]
)
// 也可使用 padding 手动适配
.padding({ bottom: this.safeAreaBottom })
3.5.4 性能优化建议
  1. 避免每次重建 ToolbarItem:将 ToolbarItem 数组缓存为成员变量
  2. 合理使用 SymbolGlyph:图标字体比图片资源更轻量
  3. 减少不必要的状态更新:使用 @Observed / @ObjectLink 实现精准更新
  4. 开启硬件加速:复杂工具栏使用 renderGroup(true)
// 推荐:缓存 ToolbarItem 数组
@Component
struct OptimizedToolbar {
  // 在 aboutToAppear 中初始化一次
  private cachedItems: Array<ToolbarItem> = []

  aboutToAppear() {
    this.cachedItems = this.createToolbarItems()
  }

  private createToolbarItems(): Array<ToolbarItem> {
    return [
      { icon: $r('app.media.home'), value: '首页', action: () => {} },
      { icon: $r('app.media.msg'), value: '消息', action: () => {} },
    ]
  }

  build() {
    Navigation(this.navStack) { /* ... */ }
    .toolbarConfiguration(this.cachedItems)
  }
}

本章小结

本章深入解析了 toolbarConfiguration 的核心用法。我们首先对比了新旧 API 的差异,然后详细讲解了 ToolbarItem 的数据结构和配置方法,接着介绍了 API 24 新引入的 Transformer 模式,最后提供了工具栏显示控制的多种方案和最佳实践。

核心要点回顾:

  • toolbarConfiguration 是 API 24 推荐的工具栏配置方式
  • ToolbarItem 支持徽章、禁用、快捷键等扩展属性
  • Transformer 模式实现沉浸式滚动体验
  • 工具栏数量建议控制在 2-5 个
  • 注意安全区域和状态栏适配

下一章,我们将进入实战环节,从零开始完整实现一个带底部工具栏的应用。


四、完整实战:搭建底部工具栏应用

理论学习后,我们将从零开始,一步步搭建一个完整的底部工具栏应用。这个应用包含首页、消息、发现、个人中心四个页面,支持页面切换、路由跳转、暗黑模式等功能。

4.1 项目初始化与配置

4.1.1 创建项目

打开 DevEco Studio,选择「Create Project」,选择「Empty Ability」模板,配置项目信息:

配置项
Project name NavigationToolbarDemo
Bundle name com.example.navigationtoolbardemo
Save location 自定义路径
Compile SDK HarmonyOS NEXT (API 24)
Model Stage
4.1.2 项目结构

创建完成后,项目结构如下:

entry/src/main/
├── ets/
│   ├── entryability/
│   │   └── EntryAbility.ets       // 入口 Ability
│   ├── pages/
│   │   └── Index.ets              // 主页面
│   ├── components/                 // 自定义组件目录
│   │   ├── HomePage.ets           // 首页组件
│   │   ├── MessagePage.ets        // 消息页组件
│   │   ├── DiscoverPage.ets       // 发现页组件
│   │   └── ProfilePage.ets        // 个人中心组件
│   ├── model/                      // 数据模型目录
│   │   └── DataModel.ets          // 数据模型
│   └── utils/                      // 工具类目录
│       └── Constants.ets          // 常量定义
├── resources/
│   ├── base/
│   │   ├── element/
│   │   │   ├── string.json        // 字符串资源
│   │   │   ├── color.json         // 颜色资源
│   │   │   └── float.json         // 尺寸资源
│   │   └── media/                 // 图片资源
│   └── dark/                       // 暗黑模式资源目录
│       └── element/
│           └── color.json         // 暗黑模式颜色
├── module.json5                    // 模块配置
└── oh-package.json5               // 依赖配置

4.2 编写常量与数据模型

首先创建常量定义文件,集中管理项目中的配置项:

// ets/utils/Constants.ets

export class AppConstants {
  // 工具栏配置
  static readonly TOOLBAR_TITLES: Array<string> = ['首页', '消息', '发现', '我的'];
  static readonly TOOLBAR_ICONS: Array<Resource> = [
    $r('app.media.startIcon'),
    $r('app.media.startIcon'),
    $r('app.media.startIcon'),
    $r('app.media.startIcon')
  ];

  // 页面路由名称
  static readonly PAGE_HOME: string = 'HomePage';
  static readonly PAGE_MESSAGE: string = 'MessagePage';
  static readonly PAGE_DISCOVER: string = 'DiscoverPage';
  static readonly PAGE_PROFILE: string = 'ProfilePage';
  static readonly PAGE_DETAIL: string = 'DetailPage';

  // 主题色
  static readonly COLOR_PRIMARY: string = '#007DFF';
  static readonly COLOR_BACKGROUND: string = '#F5F5F5';
  static readonly COLOR_TEXT_PRIMARY: string = '#333333';
  static readonly COLOR_TEXT_SECONDARY: string = '#666666';
  static readonly COLOR_TEXT_TERTIARY: string = '#999999';

  // 尺寸常量
  static readonly SIZE_TOOLBAR_HEIGHT: number = 56;
  static readonly SIZE_CARD_RADIUS: number = 12;
  static readonly SIZE_CARD_PADDING: number = 16;
}

创建数据模型:

// ets/model/DataModel.ets

// 消息模型
export class MessageModel {
  id: string;
  title: string;
  content: string;
  time: string;
  isRead: boolean;

  constructor(id: string, title: string, content: string, time: string, isRead: boolean) {
    this.id = id;
    this.title = title;
    this.content = content;
    this.time = time;
    this.isRead = isRead;
  }
}

// 用户模型
export class UserModel {
  userId: string;
  userName: string;
  email: string;
  avatarUrl: string;
  isVip: boolean;

  constructor(userId: string, userName: string, email: string, avatarUrl: string, isVip: boolean) {
    this.userId = userId;
    this.userName = userName;
    this.email = email;
    this.avatarUrl = avatarUrl;
    this.isVip = isVip;
  }
}

// 发现项模型
export class DiscoverItemModel {
  id: string;
  title: string;
  description: string;
  imageUrl: string;
  category: string;

  constructor(id: string, title: string, description: string, imageUrl: string, category: string) {
    this.id = id;
    this.title = title;
    this.description = description;
    this.imageUrl = imageUrl;
    this.category = category;
  }
}

4.3 实现主页面 Index.ets

现在创建主页面,配置 Navigation 和 toolbar:

// ets/pages/Index.ets

import { AppConstants } from '../utils/Constants';
import { UserModel, MessageModel } from '../model/DataModel';

@Entry
@Component
struct Index {
  // 导航栈
  @State navStack: NavPathStack = new NavPathStack();
  
  // 当前选中的工具栏索引
  @State currentIndex: number = 0;
  
  // 页面标题
  @State pageTitle: string = '首页';
  
  // 当前用户
  @State currentUser: UserModel = new UserModel(
    '001', '开发者', 'dev@example.com', '', true
  );
  
  // 未读消息数量
  @State unreadCount: number = 3;
  
  // 是否暗黑模式
  @State isDarkMode: boolean = false;

  build() {
    Navigation(this.navStack) {
      Column() {
        // 根据当前选中的工具栏显示对应内容
        this.ContentArea()
      }
      .width('100%')
      .height('100%')
      .backgroundColor(this.isDarkMode ? '#1A1A1A' : AppConstants.COLOR_BACKGROUND)
    }
    // 配置标题栏
    .title(this.pageTitle)
    .titleMode(NavigationTitleMode.Mini)
    .hideBackButton(true)
    
    // ========== 核心配置:工具栏 ==========
    .toolbarConfiguration(this.createToolbarItems())
    
    // 工具栏显示控制
    .hideToolBar(false)
    
    // 状态栏颜色
    .statusBarStyle(this.isDarkMode ? 
      StatusBarStyle.LIGHT_CONTENT : StatusBarStyle.DARK_CONTENT)
    
    // 设置显示模式
    .mode(NavigationMode.Stack)
    
    // 页面路由配置
    .navDestination(this.pageMap)
    
    // 全屏显示
    .width('100%')
    .height('100%')
    .backgroundColor(this.isDarkMode ? '#000000' : Color.White)
  }

  /**
   * 创建工具栏项
   */
  private createToolbarItems(): Array<ToolbarItem> {
    return [
      {
        icon: AppConstants.TOOLBAR_ICONS[0],
        value: AppConstants.TOOLBAR_TITLES[0],
        action: () => {
          this.switchTab(0);
        }
      },
      {
        icon: AppConstants.TOOLBAR_ICONS[1],
        value: AppConstants.TOOLBAR_TITLES[1],
        // 显示未读消息徽章
        badge: this.unreadCount > 0 ? this.unreadCount.toString() : '',
        action: () => {
          this.switchTab(1);
          this.unreadCount = 0; // 点击后清除徽章
        }
      },
      {
        icon: AppConstants.TOOLBAR_ICONS[2],
        value: AppConstants.TOOLBAR_TITLES[2],
        action: () => {
          this.switchTab(2);
        }
      },
      {
        icon: AppConstants.TOOLBAR_ICONS[3],
        value: AppConstants.TOOLBAR_TITLES[3],
        action: () => {
          this.switchTab(3);
        }
      }
    ];
  }

  /**
   * 切换工具栏
   */
  private switchTab(index: number): void {
    this.currentIndex = index;
    this.pageTitle = AppConstants.TOOLBAR_TITLES[index];
  }

  /**
   * 页面路由映射
   */
  @Builder
  pageMap(name: string, param: Object) {
    if (name === AppConstants.PAGE_DETAIL) {
      this.DetailPage(param);
    }
  }

  /**
   * 主内容区域
   */
  @Builder
  ContentArea() {
    if (this.currentIndex === 0) {
      this.HomeContent()
    } else if (this.currentIndex === 1) {
      this.MessageContent()
    } else if (this.currentIndex === 2) {
      this.DiscoverContent()
    } else {
      this.ProfileContent()
    }
  }

  /**
   * 首页内容
   */
  @Builder
  HomeContent() {
    Column({ space: 16 }) {
      // 欢迎区域
      this.WelcomeSection()
      
      // 功能入口区域
      this.FunctionSection()
      
      // 推荐内容区域
      this.RecommendSection()
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .scrollBar(BarState.On)
    .scrollBarColor('#CCCCCC')
  }

  @Builder
  WelcomeSection() {
    Column() {
      Text('欢迎回来')
        .fontSize(14)
        .fontColor(AppConstants.COLOR_TEXT_SECONDARY)
      
      Text(this.currentUser.userName)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.isDarkMode ? Color.White : AppConstants.COLOR_TEXT_PRIMARY)
        .margin({ top: 4 })
    }
    .width('100%')
    .alignItems(HorizontalAlign.Start)
  }

  @Builder
  FunctionSection() {
    Row({ space: 12 }) {
      this.FunctionCard('📦', '我的订单', () => {
        this.navStack.pushPathByName(AppConstants.PAGE_DETAIL, { type: 'orders' })
      })
      this.FunctionCard('🎫', '优惠券', () => {
        this.navStack.pushPathByName(AppConstants.PAGE_DETAIL, { type: 'coupons' })
      })
      this.FunctionCard('📍', '收货地址', () => {
        this.navStack.pushPathByName(AppConstants.PAGE_DETAIL, { type: 'address' })
      })
    }
    .width('100%')
    .justifyContent(FlexAlign.SpaceBetween)
  }

  @Builder
  FunctionCard(icon: string, title: string, onClick: () => void) {
    Column() {
      Text(icon).fontSize(28)
      Text(title)
        .fontSize(12)
        .fontColor(this.isDarkMode ? '#CCCCCC' : AppConstants.COLOR_TEXT_SECONDARY)
        .margin({ top: 8 })
    }
    .layoutWeight(1)
    .height(80)
    .backgroundColor(this.isDarkMode ? '#2A2A2A' : Color.White)
    .borderRadius(AppConstants.SIZE_CARD_RADIUS)
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .onClick(onClick)
  }

  @Builder
  RecommendSection() {
    Column({ space: 12 }) {
      Text('为你推荐')
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .fontColor(this.isDarkMode ? Color.White : AppConstants.COLOR_TEXT_PRIMARY)
        .width('100%')
      
      Column({ space: 12 }) {
        for (let i = 0; i < 3; i++) {
          this.RecommendItem(`推荐内容 ${i + 1}`, '这是一段推荐内容的描述...')
        }
      }
    }
    .width('100%')
  }

  @Builder
  RecommendItem(title: string, desc: string) {
    Row() {
      Column() {
        Text(title)
          .fontSize(14)
          .fontWeight(FontWeight.Medium)
          .fontColor(this.isDarkMode ? Color.White : AppConstants.COLOR_TEXT_PRIMARY)
        
        Text(desc)
          .fontSize(12)
          .fontColor(AppConstants.COLOR_TEXT_TERTIARY)
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .margin({ top: 4 })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)
      
      Text('查看')
        .fontSize(12)
        .fontColor(AppConstants.COLOR_PRIMARY)
    }
    .width('100%')
    .padding(AppConstants.SIZE_CARD_PADDING)
    .backgroundColor(this.isDarkMode ? '#2A2A2A' : Color.White)
    .borderRadius(AppConstants.SIZE_CARD_RADIUS)
  }

  /**
   * 消息内容
   */
  @Builder
  MessageContent() {
    Column() {
      // 消息列表
      this.MessageList()
    }
    .width('100%')
    .height('100%')
    .padding(16)
  }

  @Builder
  MessageList() {
    List({ space: 12 }) {
      ListItem() {
        this.MessageItem('系统通知', '欢迎使用 NavigationToolbarDemo', '刚刚', false)
      }
      ListItem() {
        this.MessageItem('订单消息', '您的订单已发货,请查收', '10:30', true)
      }
      ListItem() {
        this.MessageItem('活动消息', '双十一大促即将开始', '昨天', true)
      }
      ListItem() {
        this.MessageItem('好友动态', '您的好友发布了新动态', '2天前', false)
      }
    }
    .width('100%')
    .layoutWeight(1)
  }

  @Builder
  MessageItem(title: string, content: string, time: string, isRead: boolean) {
    Row() {
      Column() {
        Row() {
          Text(title)
            .fontSize(15)
            .fontWeight(isRead ? FontWeight.Normal : FontWeight.Medium)
            .fontColor(this.isDarkMode ? Color.White : AppConstants.COLOR_TEXT_PRIMARY)
          
          if (!isRead) {
            // 未读标记
            Circle()
              .width(8)
              .height(8)
              .fill(Color.Red)
              .margin({ left: 8 })
          }
        }
        
        Text(content)
          .fontSize(13)
          .fontColor(AppConstants.COLOR_TEXT_SECONDARY)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .margin({ top: 4 })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)
      
      Text(time)
        .fontSize(11)
        .fontColor(AppConstants.COLOR_TEXT_TERTIARY)
    }
    .width('100%')
    .padding(AppConstants.SIZE_CARD_PADDING)
    .backgroundColor(this.isDarkMode ? '#2A2A2A' : Color.White)
    .borderRadius(AppConstants.SIZE_CARD_RADIUS)
  }

  /**
   * 发现内容
   */
  @Builder
  DiscoverContent() {
    Column({ space: 12 }) {
      // 搜索框
      this.SearchBar()
      
      // 分类标签
      this.CategoryTabs()
      
      // 发现列表
      this.DiscoverList()
    }
    .width('100%')
    .height('100%')
    .padding(16)
  }

  @Builder
  SearchBar() {
    Row() {
      Text('🔍')
        .fontSize(18)
        .margin({ right: 8 })
      Text('搜索内容')
        .fontSize(14)
        .fontColor(AppConstants.COLOR_TEXT_TERTIARY)
    }
    .width('100%')
    .height(40)
    .padding({ left: 12, right: 12 })
    .backgroundColor(this.isDarkMode ? '#2A2A2A' : Color.White)
    .borderRadius(20)
    .alignItems(VerticalAlign.Center)
  }

  @Builder
  CategoryTabs() {
    Row({ space: 12 }) {
      this.CategoryTab('推荐', true)
      this.CategoryTab('热门', false)
      this.CategoryTab('最新', false)
      this.CategoryTab('关注', false)
    }
    .width('100%')
  }

  @Builder
  CategoryTab(title: string, isSelected: boolean) {
    Text(title)
      .fontSize(14)
      .fontWeight(isSelected ? FontWeight.Medium : FontWeight.Normal)
      .fontColor(isSelected ? AppConstants.COLOR_PRIMARY : 
        (this.isDarkMode ? '#CCCCCC' : AppConstants.COLOR_TEXT_SECONDARY))
      .padding({ left: 12, right: 12, top: 8, bottom: 8 })
      .backgroundColor(isSelected ? 
        (this.isDarkMode ? '#1A3A5C' : '#E8F4FD') : Color.Transparent)
      .borderRadius(16)
  }

  @Builder
  DiscoverList() {
    Grid() {
      GridItem() {
        this.DiscoverCard('发现内容 1', '描述信息')
      }
      GridItem() {
        this.DiscoverCard('发现内容 2', '描述信息')
      }
      GridItem() {
        this.DiscoverCard('发现内容 3', '描述信息')
      }
      GridItem() {
        this.DiscoverCard('发现内容 4', '描述信息')
      }
    }
    .columnsTemplate('1fr 1fr')
    .columnsGap(12)
    .rowsGap(12)
    .width('100%')
    .layoutWeight(1)
  }

  @Builder
  DiscoverCard(title: string, desc: string) {
    Column() {
      // 图片占位
      Column()
        .width('100%')
        .height(100)
        .backgroundColor(this.isDarkMode ? '#3A3A3A' : '#E8E8E8')
        .borderRadius({ topLeft: 8, topRight: 8 })
      
      Column({ space: 4 }) {
        Text(title)
          .fontSize(14)
          .fontWeight(FontWeight.Medium)
          .fontColor(this.isDarkMode ? Color.White : AppConstants.COLOR_TEXT_PRIMARY)
        
        Text(desc)
          .fontSize(12)
          .fontColor(AppConstants.COLOR_TEXT_SECONDARY)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
      }
      .width('100%')
      .padding(12)
    }
    .width('100%')
    .backgroundColor(this.isDarkMode ? '#2A2A2A' : Color.White)
    .borderRadius(8)
    .onClick(() => {
      this.navStack.pushPathByName(AppConstants.PAGE_DETAIL, { title: title })
    })
  }

  /**
   * 个人中心内容
   */
  @Builder
  ProfileContent() {
    Column() {
      // 用户信息区域
      this.UserInfoSection()
      
      // 设置列表
      this.SettingList()
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .scrollBar(BarState.On)
  }

  @Builder
  UserInfoSection() {
    Row() {
      // 头像
      Stack() {
        Circle()
          .width(64)
          .height(64)
          .fill(AppConstants.COLOR_PRIMARY)
        Text('👤')
          .fontSize(32)
      }
      
      Column() {
        Row({ space: 8 }) {
          Text(this.currentUser.userName)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(this.isDarkMode ? Color.White : AppConstants.COLOR_TEXT_PRIMARY)
          
          if (this.currentUser.isVip) {
            Text('VIP')
              .fontSize(10)
              .fontColor('#FFD700')
              .padding({ left: 4, right: 4, top: 2, bottom: 2 })
              .backgroundColor('#FFF8DC')
              .borderRadius(4)
          }
        }
        
        Text(this.currentUser.email)
          .fontSize(13)
          .fontColor(AppConstants.COLOR_TEXT_TERTIARY)
          .margin({ top: 4 })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)
      .margin({ left: 12 })
    }
    .width('100%')
    .padding(AppConstants.SIZE_CARD_PADDING)
    .backgroundColor(this.isDarkMode ? '#2A2A2A' : Color.White)
    .borderRadius(AppConstants.SIZE_CARD_RADIUS)
  }

  @Builder
  SettingList() {
    Column({ space: 1 }) {
      this.SettingItem('主题设置', this.isDarkMode ? '暗黑模式' : '浅色模式', () => {
        this.isDarkMode = !this.isDarkMode
      })
      this.SettingItem('消息通知', '已开启', () => {})
      this.SettingItem('账户安全', '', () => {})
      this.SettingItem('关于我们', 'v1.0.0', () => {})
      this.SettingItem('退出登录', '', () => {})
    }
    .width('100%')
    .margin({ top: 16 })
    .backgroundColor(this.isDarkMode ? '#2A2A2A' : Color.White)
    .borderRadius(AppConstants.SIZE_CARD_RADIUS)
  }

  @Builder
  SettingItem(title: string, subtitle: string, onClick: () => void) {
    Row() {
      Text(title)
        .fontSize(15)
        .fontColor(this.isDarkMode ? Color.White : AppConstants.COLOR_TEXT_PRIMARY)
      
      Column() {
        if (subtitle.length > 0) {
          Text(subtitle)
            .fontSize(13)
            .fontColor(AppConstants.COLOR_TEXT_TERTIARY)
        }
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.End)
      
      Text('›')
        .fontSize(20)
        .fontColor(AppConstants.COLOR_TEXT_TERTIARY)
        .margin({ left: 8 })
    }
    .width('100%')
    .padding({ left: 16, right: 16, top: 14, bottom: 14 })
    .onClick(onClick)
  }

  /**
   * 详情页(路由跳转目标)
   */
  @Builder
  DetailPage(param: Object) {
    NavDestination() {
      Column() {
        Text('这是详情页')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 16 })
        
        Text('参数:' + JSON.stringify(param))
          .fontSize(14)
          .fontColor(AppConstants.COLOR_TEXT_SECONDARY)
        
        Button('返回')
          .margin({ top: 32 })
          .onClick(() => {
            this.navStack.pop()
          })
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.Center)
      .alignItems(HorizontalAlign.Center)
    }
    .hideBackButton(false)
  }
}

4.4 配置资源文件

为了支持多语言和暗黑模式,需要配置资源文件:

4.4.1 字符串资源
// resources/base/element/string.json
{
  "string": [
    {
      "name": "app_name",
      "value": "NavigationToolbarDemo"
    },
    {
      "name": "toolbar_home",
      "value": "首页"
    },
    {
      "name": "toolbar_message",
      "value": "消息"
    },
    {
      "name": "toolbar_discover",
      "value": "发现"
    },
    {
      "name": "toolbar_profile",
      "value": "我的"
    }
  ]
}
4.4.2 颜色资源
// resources/base/element/color.json
{
  "color": [
    {
      "name": "primary_color",
      "value": "#007DFF"
    },
    {
      "name": "background_color",
      "value": "#F5F5F5"
    },
    {
      "name": "text_primary",
      "value": "#333333"
    },
    {
      "name": "text_secondary",
      "value": "#666666"
    }
  ]
}
4.4.3 暗黑模式颜色
// resources/dark/element/color.json
{
  "color": [
    {
      "name": "primary_color",
      "value": "#0A84FF"
    },
    {
      "name": "background_color",
      "value": "#000000"
    },
    {
      "name": "text_primary",
      "value": "#FFFFFF"
    },
    {
      "name": "text_secondary",
      "value": "#CCCCCC"
    }
  ]
}

4.5 运行与调试

4.5.1 配置启动参数
  1. 打开 DevEco Studio,点击右上角的「Run」按钮
  2. 在运行配置中选择「Phone」模拟器或连接的真机
  3. 点击「OK」开始运行
4.5.2 调试技巧

使用 HiLog 日志

import hilog from '@ohos.hilog';

// 在代码中添加日志
hilog.info(0x0001, 'NavigationDemo', '工具栏切换到: %{public}d', this.currentIndex);
hilog.error(0x0001, 'NavigationDemo', '路由跳转失败: %{public}s', error.message);

使用断点调试

在 DevEco Studio 中,可以在特定行设置断点,当代码执行到该行时会暂停,方便检查变量值和调用栈。

使用预览器

DevEco Studio 提供了实时预览功能,可以在不运行应用的情况下查看 UI 效果。


本章小结

本章我们完整实现了一个带底部工具栏的应用,包括:

  1. 项目初始化:配置项目结构和依赖
  2. 数据模型:定义消息、用户、发现项等数据模型
  3. 主页面实现:配置 Navigation、toolbar、页面路由
  4. 四个功能页面:首页、消息、发现、个人中心
  5. 资源配置:支持多语言和暗黑模式
  6. 调试技巧:HiLog 日志、断点调试、实时预览

关键技术点:

  • NavPathStack 的 push/pop 路由操作
  • ToolbarItem 的 badge 徽章功能
  • @Builder 装饰器构建 UI 组件
  • 暗黑模式下的颜色适配

下一章,我们将介绍进阶技巧,包括多设备适配、状态管理和性能优化。


五、进阶技巧:打造企业级应用

掌握了基础用法后,我们需要学习一些进阶技巧,才能打造出真正高质量、可维护的企业级应用。

5.1 多设备响应式适配

HarmonyOS 的核心理念是「一次开发,多端部署」。Navigation 组件提供了强大的多设备适配能力。

5.1.1 使用 Auto 模式自动适配

最推荐的方式是使用 Auto 模式,让系统根据屏幕大小自动选择最佳布局:

Navigation(this.navStack) {
  Content()
}
.mode(NavigationMode.Auto)
// 自定义切换阈值
.minContentWidth(400)  // 内容区最小宽度,低于此值切换为 Stack 模式
5.1.2 响应式布局实战

下面是一个完整的响应式布局示例,适配手机和平板:

@Entry
@Component
struct ResponsiveNavigation {
  @State navStack: NavPathStack = new NavPathStack()
  @State windowWidth: number = 0

  aboutToAppear() {
    // 获取窗口宽度
    window.getLastWindow(this.context).then((win) => {
      const properties = win.getWindowProperties()
      this.windowWidth = properties.windowRect.width
    })
  }

  build() {
    Navigation(this.navStack) {
      if (this.isTablet()) {
        // 平板布局:左右分栏
        this.TabletLayout()
      } else {
        // 手机布局:全屏
        this.PhoneLayout()
      }
    }
    .mode(this.isTablet() ? NavigationMode.Split : NavigationMode.Stack)
    .navBarWidth(this.isTablet() ? 280 : 0)
    .toolbarConfiguration(this.toolbarItems)
  }

  // 判断是否为平板
  private isTablet(): boolean {
    return this.windowWidth >= 600
  }

  @Builder
  TabletLayout() {
    // 平板左侧导航栏
    Column() {
      this.TabButton('首页', 0)
      this.TabButton('消息', 1)
      this.TabButton('发现', 2)
      this.TabButton('我的', 3)
    }
    .width('100%')
    .padding(16)
  }

  @Builder
  TabletContent() {
    // 平板右侧内容区
    Column() {
      // 根据选中项显示内容
    }
    .width('100%')
    .layoutWeight(1)
  }

  @Builder
  PhoneLayout() {
    // 手机全屏布局
    Column() {
      // 内容
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  TabButton(title: string, index: number) {
    Row() {
      Text(title)
        .fontSize(16)
        .fontColor(Color.Black)
    }
    .width('100%')
    .height(48)
    .padding({ left: 16 })
    .borderRadius(8)
  }
}
5.1.3 折叠屏适配

折叠屏是鸿蒙的特色设备,需要特殊处理:

import display from '@ohos.display'

@Entry
@Component
struct FoldableNavigation {
  @State navStack: NavPathStack = new NavPathStack()
  @State foldState: display.FoldStatus = display.FoldStatus.FOLD_STATUS_UNKNOWN

  aboutToAppear() {
    // 监听折叠状态变化
    display.on('foldStatusChange', (foldStatus: display.FoldStatus) => {
      this.foldState = foldStatus
      this.updateLayout()
    })
  }

  private updateLayout() {
    switch (this.foldState) {
      case display.FoldStatus.FOLD_STATUS_EXPANDED:
        // 展开状态:分栏显示
        break
      case display.FoldStatus.FOLD_STATUS_FOLDED:
        // 折叠状态:单栏显示
        break
      case display.FoldStatus.FOLD_STATUS_HALF_FOLDED:
        // 半折叠:双列显示
        break
    }
  }

  build() {
    Navigation(this.navStack) {
      // 根据折叠状态选择布局
    }
    .mode(this.getNavMode())
  }

  private getNavMode(): NavigationMode {
    if (this.foldState === display.FoldStatus.FOLD_STATUS_EXPANDED) {
      return NavigationMode.Split
    }
    return NavigationMode.Stack
  }
}

5.2 状态管理最佳实践

随着应用复杂度增加,良好的状态管理变得至关重要。

5.2.1 使用 @Observed 和 @ObjectLink

对于复杂的状态对象,推荐使用 @Observed 和 @ObjectLink:

// 状态类定义
@Observed
class AppState {
  selectedTab: number = 0
  unreadCount: number = 0
  isDarkMode: boolean = false

  updateUnreadCount(count: number) {
    this.unreadCount = count
  }
}

// 使用状态
@Entry
@Component
struct StateManagedNavigation {
  @State appState: AppState = new AppState()

  build() {
    Navigation(this.navStack) {
      Column() {
        this.ContentArea()
      }
    }
    .toolbarConfiguration(this.toolbarItems)
  }

  @Builder
  ContentArea() {
    if (this.appState.selectedTab === 0) {
      this.HomePage()
    }
  }
}
5.2.2 跨组件状态共享

使用 @Provide 和 @Consume 实现跨组件状态共享:

// 在父组件提供状态
@Entry
@Component
struct ParentComponent {
  @Provide('AppState') appState: AppState = new AppState()

  build() {
    ChildComponent()
  }
}

// 在子组件消费状态
@Component
struct ChildComponent {
  @Consume('AppState') appState: AppState

  build() {
    Text(`未读消息:${this.appState.unreadCount}`)
  }
}
5.2.3 使用 Preferences 持久化状态

使用 Preferences API 持久化应用状态:

import preferences from '@ohos.data.preferences'

class StateManager {
  private static instance: StateManager
  private pref: preferences.Preferences | null = null

  static getInstance(): StateManager {
    if (!StateManager.instance) {
      StateManager.instance = new StateManager()
    }
    return StateManager.instance
  }

  async init(context: Context): Promise<void> {
    this.pref = await preferences.getPreferences(context, 'AppPreferences')
  }

  async saveState(key: string, value: Object): Promise<void> {
    if (this.pref) {
      await this.pref.put(key, JSON.stringify(value))
      await this.pref.flush()
    }
  }

  async loadState(key: string, defaultValue: Object): Promise<Object> {
    if (this.pref) {
      const value = await this.pref.get(key, JSON.stringify(defaultValue))
      return JSON.parse(value as string)
    }
    return defaultValue
  }
}

5.3 性能优化技巧

5.3.1 减少不必要的渲染

使用 @Builder 缓存和条件渲染:

@Component
struct OptimizedComponent {
  @State data: Array<string> = []
  private cachedBuilder: CustomBuilder = () => {
    // 缓存的 Builder
  }

  build() {
    Column() {
      // 仅在数据变化时重新渲染
      if (this.data.length > 0) {
        this.DataList()
      } else {
        this.EmptyView()
      }
    }
  }

  @Builder
  DataList() {
    // 列表渲染
  }

  @Builder
  EmptyView() {
    // 空状态视图
  }
}
5.3.2 使用 LazyForEach 处理大数据

对于长列表,使用 LazyForEach 替代 ForEach:

// 实现 IDatasource 接口
class MyDataSource implements IDatasource {
  private dataArray: Array<string> = []

  constructor(data: Array<string>) {
    this.dataArray = data
  }

  totalCount(): number {
    return this.dataArray.length
  }

  getData(index: number): string {
    return this.dataArray[index]
  }

  registerDataChangeListener(listener: DataChangeListener): void { }
  unregisterDataChangeListener(listener: DataChangeListener): void { }
}

@Component
struct ListComponent {
  private dataSource: MyDataSource = new MyDataSource([])

  build() {
    List() {
      LazyForEach(this.dataSource, (item: string) => {
        ListItem() {
          Text(item)
        }
      }, (item: string, index: number) => item + index)
    }
  }
}
5.3.3 图片资源优化

使用合适的图片格式和尺寸:

Image($r('app.media.avatar'))
  .width(40)
  .height(40)
  // 使用 clip 避免图片超出容器
  .clip(true)
  // 设置解码尺寸,减少内存占用
  .decodingSize({ width: 80, height: 80 })
  // 使用缓存
  .cached(true)
5.3.4 开启硬件加速

对于复杂的动画和效果,开启硬件加速:

Column() {
  // 复杂的 UI 内容
}
.renderGroup(true)  // 开启硬件加速
.clipContent(true)

5.4 安全与权限

5.4.1 权限声明

在 module.json5 中声明所需权限:

{
  "requestPermissions": [
    {
      "name": "ohos.permission.INTERNET",
      "reason": "用于网络请求",
      "usedScene": {
        "abilities": ["EntryAbility"],
        "when": "inuse"
      }
    },
    {
      "name": "ohos.permission.LOCATION",
      "reason": "用于定位功能",
      "usedScene": {
        "abilities": ["EntryAbility"],
        "when": "inuse"
      }
    }
  ]
}
5.4.2 动态请求权限

在运行时动态请求权限:

import abilityAccessCtrl from '@ohos.abilityAccessCtrl'
import bundleManager from '@ohos.bundle.bundleManager'

class PermissionManager {
  private atManager: abilityAccessCtrl.AtManager | null = null

  async requestPermission(context: Context): Promise<boolean> {
    this.atManager = abilityAccessCtrl.createAtManager()

    const tokenId = bundleManager.getBundleInfoForSelf(
      bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
    ).appInfo.accessTokenId

    const permissions: Array<string> = [
      'ohos.permission.INTERNET',
      'ohos.permission.LOCATION'
    ]

    const grantResult = await this.atManager.requestPermissionsFromUser(
      context,
      tokenId,
      permissions
    )

    return grantResult.authResults.every((result: number) => result === 0)
  }
}
5.4.3 安全存储敏感数据

使用 AppStoragePreferences 或加密存储敏感信息:

import { preferences } from '@kit.ArkData'

class SecureStorage {
  private static instance: SecureStorage
  private pref: preferences.Preferences | null = null

  static getInstance(): SecureStorage {
    if (!SecureStorage.instance) {
      SecureStorage.instance = new SecureStorage()
    }
    return SecureStorage.instance
  }

  async init(context: Context): Promise<void> {
    this.pref = await preferences.getPreferences(context, 'SecureStorage')
  }

  async save(key: string, value: string): Promise<void> {
    if (this.pref) {
      // 对敏感数据进行加密(此处为示例,实际应使用加密算法)
      const encrypted = this.encrypt(value)
      await this.pref.put(key, encrypted)
      await this.pref.flush()
    }
  }

  private encrypt(data: string): string {
    // 实际应用中应使用 AES 等加密算法
    return btoa(data)
  }

  private decrypt(data: string): string {
    return atob(data)
  }
}

5.5 单元测试与调试

5.5.1 编写单元测试

使用 ArkTS 测试框架编写单元测试:

// tests/unit/NavigationTest.ets
import describe from '@ohos.hypium.describe'
import it from '@ohos.hypium.it'
import expect from '@ohos.hypium.expect'

describe('NavigationTest', () => {
  it('should create Navigation component', () => {
    // 测试代码
    expect(true).assertTrue()
  })

  it('should handle toolbar click', () => {
    // 测试工具栏点击事件
  })

  it('should navigate to detail page', () => {
    // 测试页面导航
  })
})
5.5.2 使用 DevEco Studio 调试

调试技巧

  • 使用 Logcat 查看应用日志
  • 使用断点调试定位问题
  • 使用内存分析器检测内存泄漏
  • 使用性能分析器优化性能

常见问题排查

问题 可能原因 解决方案
工具栏不显示 未设置 toolbarConfiguration 添加工具栏配置
点击无反应 action 回调为空 实现回调逻辑
图标不显示 资源路径错误 检查资源引用
样式错乱 未适配暗黑模式 添加 dark 目录资源
5.5.3 使用 DevEco Testing

DevEco Testing 提供了自动化测试能力:

  1. 编写测试用例
  2. 配置测试设备
  3. 运行测试并查看结果
  4. 生成测试报告

本章小结

本章介绍了进阶技巧,帮助你打造企业级应用:

  1. 多设备适配:使用 Auto 模式自动适配,折叠屏特殊处理
  2. 状态管理:使用 @Observed、@ObjectLink、Preferences 管理状态
  3. 性能优化:减少渲染、使用 LazyForEach、图片优化、硬件加速
  4. 安全与权限:权限声明、动态请求、安全存储
  5. 单元测试:编写测试用例、使用 DevEco Studio 调试

掌握这些技巧后,你可以开发出高质量、高性能、安全可靠的鸿蒙应用。

下一章,我们将总结全文,并展望 HarmonyOS 的未来发展。


六、总结与展望

6.1 全文总结

本文系统地介绍了如何在 HarmonyOS NEXT (API 24) 中使用 Navigation + toolbarConfiguration 实现底部工具栏布局。主要内容包括:

基础篇

  • HarmonyOS NEXT 技术栈与 API 24 新特性
  • Navigation 组件的三层架构(标题栏、内容区、工具栏)
  • NavPathStack 路由栈的使用方法

核心篇

  • toolbarConfiguration 的完整配置
  • ToolbarItem 的数据结构与扩展属性
  • Transformer 模式(API 24 新特性)

实战篇

  • 完整的项目实现(首页、消息、发现、我的四个页面)
  • 暗黑模式适配
  • 响应式布局

进阶篇

  • 多设备适配(手机、平板、折叠屏)
  • 状态管理(@Observed、Preferences)
  • 性能优化(LazyForEach、硬件加速)
  • 安全与权限

6.2 最佳实践清单

序号 最佳实践 优先级
1 使用 Auto 模式自动适配多设备
2 缓存 ToolbarItem 数组
3 合理使用 LazyForEach 处理长列表
4 适配暗黑模式
5 添加未读消息徽章
6 使用 Preferences 持久化状态
7 开启硬件加速
8 权限声明最小化
9 编写单元测试
10 使用 SymbolGlyph 优化图标

6.3 常见问题解答

Q1: 如何设置工具栏的选中状态?
A: toolbarConfiguration 本身不直接支持选中高亮,需要通过状态管理(@State)来实现。可以在每个 ToolbarItem 的 action 回调中更新选中索引,然后根据索引动态生成 ToolbarItem 数组(改变图标颜色或样式)。

Q2: 工具栏可以动态隐藏吗?
A: 可以。使用 Transformer 模式(API 24 推荐)或 showHideToolBar() 方法实现动态隐藏。

Q3: 如何实现自定义工具栏样式?
A: toolbarConfiguration 第二个参数可以传入自定义 Builder,完全自定义工具栏布局。

Q4: NavPathStack 和 Router 有什么区别?
A: NavPathStack 是 Navigation 的内部路由机制,与 Navigation 紧密集成;Router 是全局路由,用于跨页面跳转。推荐使用 NavPathStack 配合 Navigation。

Q5: 如何处理折叠屏的适配?
A: 使用 display.on(‘foldStatusChange’) 监听折叠状态变化,根据状态动态调整布局模式(Stack/Split)。

6.4 未来展望

HarmonyOS NEXT 正在快速发展,未来值得关注的方向:

  1. AI 集成:内置 AI 能力,支持智能推荐、语音交互
  2. 跨端协同:手机、平板、PC、IoT 设备无缝协同
  3. 性能优化:更轻量的框架、更快的渲染
  4. 开发体验:更强大的 DevEco Studio、更完善的测试工具
  5. 生态建设:更多的开发者社区、更丰富的组件库

6.5 学习资源

官方文档

  • HarmonyOS 官网:https://www.harmonyos.com
  • API 参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-references
  • 开发指南:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides

社区资源

  • 华为开发者论坛:https://developer.huawei.com/consumer/cn/forum
  • CSDN HarmonyOS 专区:https://blog.csdn.net/harmonyos
  • 掘金鸿蒙标签:https://juejin.cn/tag/HarmonyOS

视频教程

  • B 站华为官方账号
  • 华为云学院
  • 各类在线教育平台

6.6 结语

感谢你阅读完这篇关于 HarmonyOS NEXT Navigation + toolbar 布局的完整指南!希望本文能帮助你快速掌握底部工具栏的实现方法,开发出优秀的鸿蒙应用。

记住,好的应用不仅需要技术实力,更需要对用户体验的深入理解。持续学习、不断实践,你一定能在鸿蒙开发领域有所建树。

让我们一起,用代码创造更美好的鸿蒙生态!


Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐