鸿蒙原生 ArkTS 布局实战:Navigation + toolbar 底部工具栏深度解析
运行环境: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 模式特别适用于以下场景:
- 内容浏览型应用:阅读、视频等沉浸式体验
- 长列表页面:购物商品列表、新闻列表等
- 全屏操作:相机、绘图等需要最大化内容区域的场景
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 性能优化建议
- 避免每次重建 ToolbarItem:将 ToolbarItem 数组缓存为成员变量
- 合理使用 SymbolGlyph:图标字体比图片资源更轻量
- 减少不必要的状态更新:使用 @Observed / @ObjectLink 实现精准更新
- 开启硬件加速:复杂工具栏使用 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 配置启动参数
- 打开 DevEco Studio,点击右上角的「Run」按钮
- 在运行配置中选择「Phone」模拟器或连接的真机
- 点击「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 效果。
本章小结
本章我们完整实现了一个带底部工具栏的应用,包括:
- 项目初始化:配置项目结构和依赖
- 数据模型:定义消息、用户、发现项等数据模型
- 主页面实现:配置 Navigation、toolbar、页面路由
- 四个功能页面:首页、消息、发现、个人中心
- 资源配置:支持多语言和暗黑模式
- 调试技巧: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 提供了自动化测试能力:
- 编写测试用例
- 配置测试设备
- 运行测试并查看结果
- 生成测试报告
本章小结
本章介绍了进阶技巧,帮助你打造企业级应用:
- 多设备适配:使用 Auto 模式自动适配,折叠屏特殊处理
- 状态管理:使用 @Observed、@ObjectLink、Preferences 管理状态
- 性能优化:减少渲染、使用 LazyForEach、图片优化、硬件加速
- 安全与权限:权限声明、动态请求、安全存储
- 单元测试:编写测试用例、使用 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 正在快速发展,未来值得关注的方向:
- AI 集成:内置 AI 能力,支持智能推荐、语音交互
- 跨端协同:手机、平板、PC、IoT 设备无缝协同
- 性能优化:更轻量的框架、更快的渲染
- 开发体验:更强大的 DevEco Studio、更完善的测试工具
- 生态建设:更多的开发者社区、更丰富的组件库
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 布局的完整指南!希望本文能帮助你快速掌握底部工具栏的实现方法,开发出优秀的鸿蒙应用。
记住,好的应用不仅需要技术实力,更需要对用户体验的深入理解。持续学习、不断实践,你一定能在鸿蒙开发领域有所建树。
让我们一起,用代码创造更美好的鸿蒙生态!
更多推荐



所有评论(0)