第1.8篇:组合式 API 实践------从零搭建一个 Tab 主页
难度 :⭐⭐ 进阶 | 前置知识:1.1 ~ 1.7
涉及源文件:
products/default/src/main/ets/pages/Index.ets本文定位:第一章综合实战篇,汇总前 7 篇文章的知识点,分析如何用 ArkUI 的组合式 API 搭建一个完整的 Tab 主页。

1. 引言
前 7 篇文章我们分别学习了:
- 1.1 ArkUI 声明式 UI 基础
- 1.2 常用组件与布局
- 1.3
@State与状态管理 - 1.4
@Builder封装复用 - 1.5
@StorageLink与BreakpointSystem响应式布局 - 1.6 页面路由与生命周期
- 1.7 资源管理与国际化
现在,是时候把它们融会贯通了。
「画伴梦工厂」的 Index.ets 是整个应用中最复杂的页面,包含了底部三 Tab 导航、首页内容区、AI 聊天创作页、作品播放详情页、成长分析页、我的世界页等多个功能模块。本篇将以它为例,展示如何用 ArkUI 的组合式 API 搭建一个结构清晰、功能完整的主页。
2. 架构总览
Index.ets 的页面结构可以用一棵树表示:
Index (build)
├─ Stack (Bottom alignment)
│ ├─ Column
│ │ └─ CurrentPage() ← 根据状态切换
│ │ ├─ HomePage() [currentTab === 0]
│ │ ├─ CreationPage() [currentTab === 1]
│ │ ├─ WorldPage() [currentTab === 2]
│ │ ├─ WorksPage() [secondaryPage === 1]
│ │ └─ AnalysisPage() [secondaryPage === 2]
│ └─ BottomBar() ← 底部导航栏
核心设计思想:通过 @State 状态控制所有页面的切换,避免实际的路由跳转,从而实现平滑的 Tab 切换体验。
3. 核心状态设计
typescript
@Entry
@Component
struct Index {
// ── Tab 控制 ──
@State private currentTab: number = 0; // 0:首页, 1:创作, 2:我的
@State private secondaryPage: number = 0; // 0:无, 1:作品, 2:分析
// ── 数据状态 ──
@State private savedWorks: VideoWork[] = [];
@State private chatMessages: ChatMessage[] = [...];
@State private selectedWorkIndex: number = 0;
@State private videoProgress: number = 0;
// ... 20+ 个 @State 变量
// ── 响应式断点 ──
@StorageLink('mainBreakpoint') currentBreakpoint: string = 'md';
}
洞察 :这是一个"大状态树"的设计模式------所有 UI 状态集中在一个组件内,通过
@State驱动。优点是状态一致性容易保证;缺点是组件体积较大。对于中型应用,这种模式是完全可以接受的。
3.1 页面切换逻辑
typescript
private openPrimaryTab(index: number): void {
this.currentTab = Math.max(0, Math.min(NAV_ITEMS.length - 1, index));
this.secondaryPage = SECONDARY_NONE; // 关闭次级页面
this.noticeText = '';
}
private openWorksSecondary(): void {
this.secondaryPage = SECONDARY_WORKS;
this.refreshWorks();
}
private closeSecondaryPage(): void {
this.secondaryPage = SECONDARY_NONE;
}
CurrentPage() Builder 根据状态决定渲染哪个页面:
typescript
@Builder
private CurrentPage() {
if (this.secondaryPage === SECONDARY_WORKS) {
this.WorksPage()
} else if (this.secondaryPage === SECONDARY_ANALYSIS) {
this.AnalysisPage()
} else if (this.currentTab === 0) {
this.HomePage()
} else if (this.currentTab === 1) {
this.CreationPage()
} else {
this.WorldPage()
}
}
4. Tab 导航设计
4.1 NavIcon Builder
底部 Tab 的每个图标使用 @Builder NavIcon 封装:
typescript
@Builder
private NavIcon(index: number) {
Column() {
// 图标符号
Text(NAV_ICONS[index])
.fontSize(20)
.fontColor(this.isPrimaryTabActive(index) ? this.brandPurple : '#8E94A6')
// 文字标签
Text(NAV_ITEMS[index])
.fontSize(12)
.fontColor(this.isPrimaryTabActive(index) ? this.brandPurple : '#747A92')
}
.layoutWeight(1)
.backgroundColor(this.isPrimaryTabActive(index) ? '#FFFFFF' : Color.Transparent)
.borderRadius(24)
.onClick(() => { this.openPrimaryTab(index); })
}
4.2 BottomBar 容器
typescript
@Builder
private BottomBar() {
Stack({ alignContent: Alignment.Center }) {
Row() {
this.NavIcon(0) // 首页
this.NavIcon(1) // 创作
this.NavIcon(2) // 我的
}
.width('100%')
.height(54)
}
.width(this.bottomBarWidth()) // 响应式宽度
.height(54)
.backgroundColor('#FEF4FC')
.backgroundBlurStyle(BlurStyle.BACKGROUND_THIN)
.borderRadius(33)
.shadow({ radius: 22, color: '#1A000000', offsetY: 8 })
}
设计亮点 :底部导航采用半透明毛玻璃效果(
backgroundBlurStyle),并响应不同屏幕宽度(bottomBarWidth()根据断点返回不同百分比),兼顾美观和适配。
5. 首页组合(HomePage)
首页是三个 Tab 中最复杂的页面,采用分层 @Builder 结构:
HomePage()
├─ GridRow + GridCol (响应式网格)
│ └─ Hero 区域 (Stack)
│ ├─ Image (背景图)
│ ├─ 标题文字
│ └─ FeatureCard × 2 (拍照识别 / 自由涂鸦)
├─ SectionTitle('创作广场')
├─ 水平滚动作品列表 (Scroll + Row + WorkThumbnail)
├─ SectionTitle('今日灵感')
├─ InspirationPanel
└─ SafetyPanel
5.1 Hero 大图区
typescript
GridRow({ columns: { sm: 4, md: 8, lg: 12 } }) {
GridCol({ span: { sm: 4, md: 8, lg: 12 } }) {
Stack({ alignContent: Alignment.TopStart }) {
Image($r('app.media.hero_portal'))
.width('100%')
.height(this.heroHeight()) // 响应式高度
.objectFit(ImageFit.Cover)
.borderRadius(26)
Column() {
Text('让孩子笔下的')
.fontSize(this.heroTitleSize()) // 响应式字号
Text('画作活起来')
.fontSize(this.heroTitleSize())
}
// 入口卡片定位在 Hero 底部的 FeatureCard
Row() {
this.FeatureCard('拍照识别', '把绘画变成动画', '', '#E5E2FB', 0)
Blank().width(60)
this.FeatureCard('自由涂鸦', '画布直达生成', '', '#FCF1DF', 1)
}
.position({ x: this.pageEdge() * 2 - 10, y: this.heroFeatureY() })
}
}
}
5.2 FeatureCard 入口卡片
typescript
@Builder
private FeatureCard(title: string, desc: string, icon: string, color: string, modeIndex: number) {
Row() {
Column() {
Text(title).fontSize(15).fontWeight(FontWeight.Bold).fontColor(this.ink)
Text(desc).fontSize(12).fontColor('#8A8FA4').margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 8 })
}
.layoutWeight(1)
.height(72)
.backgroundColor(color)
.borderRadius(12)
.shadow({ radius: 12, color: '#18000000', offsetY: 4 })
.onClick(() => { this.openCreationFlow(modeIndex); })
}
5.3 水平滚动作品列表
typescript
Scroll() {
Row() {
ForEach(WORKS, (item: Artwork) => {
this.WorkThumbnail(item)
}, (item: Artwork) => item.id.toString())
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.height(126)
每个缩略图包含一个彩色背景占位 + 图片封面:
typescript
@Builder
private WorkThumbnail(item: Artwork) {
Column() {
Stack() {
Rect().width(96).height(78).fill(item.color).radius(12)
Image(this.getWorkCover(item.id - 1)).width(96).height(78).borderRadius(12)
}
Text(item.title).fontSize(11).maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.onClick(() => {
this.selectWork(item.id - 1);
this.openWorksSecondary();
})
}
6. 创作页组合(CreationPage)
创作页是 AI 对话交互的核心页面,由三部分组成:
CreationPage()
├─ Header('智能问答', subtitle, showBack=true)
├─ Scroll (消息列表)
│ └─ ChatBubble × N (用户消息 / AI 回复)
└─ ChatInputBar (底部输入区,覆盖在 Scroll 之上)
6.1 消息气泡 ChatBubble
typescript
@Builder
private ChatBubble(message: ChatMessage) {
Column() {
Text(message.text).fontSize(13).lineHeight(20)
if (message.imageUri !== '') {
Image(message.imageUri).width('100%').height(168).borderRadius(14)
Row() {
Button('重新生成').onClick(() => { /* ... */ })
Button('去生成视频').onClick(() => { this.generateVideoFromChat(message); })
}
}
}
.width(message.role === 'user' ? '82%' : '100%')
.backgroundColor(message.role === 'user' ? this.brandPurple : '#FFFFFF')
.borderRadius(16)
.margin({
left: message.role === 'user' ? 54 : 0,
right: message.role === 'user' ? 0 : 28
})
}
6.2 底部输入区 ChatInputBar
输入区固定在 Stack 底部,覆盖在消息列表之上:
typescript
@Builder
private ChatInputBar() {
Column() {
// 语音状态提示
if (this.voiceStatus !== '') { /* 状态行 */ }
Row() {
Button('语音').onClick(() => { this.startVoiceInput(); })
TextInput({ placeholder: '说出角色、场景和故事', text: this.chatInput })
Button('发送').onClick(() => { this.sendChatPrompt(); })
}
}
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.backgroundColor('#FFFFFF')
.shadow({ radius: 18, color: '#18000000', offsetY: -5 })
.margin({ bottom: 82 }) // 避免被底部 Tab 遮挡
}
设计技巧 :
Stack的Bottom对齐 +margin({ bottom: 82 })确保输入区悬浮在底部 Tab 栏之上,同时消息列表滚动到底部时不会被遮挡。
7. 作品详情页(WorksPage)
作品播放页是次级页面(secondaryPage === SECONDARY_WORKS),完整结构如下:
WorksPage()
├─ Header('动画播放', title, showBack=true)
├─ Stack (播放器 + 进度条覆盖层)
│ ├─ AnimationVideoScene (Video 组件)
│ └─ 进度条 Row (播放/暂停 + 时间 + Progress)
├─ 作品信息卡片
│ ├─ 标题 + 编辑按钮
│ ├─ 类型标签 + 日期 + 评分
│ ├─ Pill 标签云
│ ├─ 故事描述
│ └─ ActionButton × 5 (重播/收藏/分享/碰一碰/下载)
└─ SectionTitle('推荐作品')
└─ 水平推荐列表
7.1 Video 组件与进度控制
typescript
Video({
src: this.getWorkVideo(this.selectedWorkIndex),
previewUri: this.getCurrentWorkCover(),
controller: this.videoController
})
.width('100%')
.height(this.videoSceneHeight())
.controls(false) // 隐藏默认控件
.autoPlay(true)
.onPrepared((event) => {
this.videoDuration = Math.max(1, event.duration);
})
.onUpdate((event) => {
this.videoProgress = Math.min(this.videoDuration, event.time);
})
.onFinish(() => {
this.isPlaying = false;
this.videoProgress = this.videoDuration;
})
使用自定义进度条覆盖层代替系统控件,提供更统一的视觉体验:
typescript
Row() {
Text(this.isPlaying ? 'II' : '>') // 播放/暂停
Text(this.formatVideoTime(this.videoProgress))
Progress({ value: this.videoProgress, total: this.videoDuration })
Text(this.formatVideoTime(this.videoDuration))
}
7.2 操作按钮
typescript
Row() {
this.ActionButton('重播', 'R')
this.ActionButton('收藏', 'F')
this.ActionButton('分享', 'S')
this.ActionButton('碰一碰', 'H')
this.ActionButton('下载', 'D')
}
每个按钮的激活状态由对应的 @State 布尔变量控制:
typescript
@Builder
private ActionButton(text: string, icon: string) {
Column() {
this.IconBubble(icon, 36,
(text === '收藏' && this.favoriteActive) ||
(text === '分享' && this.sharedActive) ||
(text === '碰一碰' && this.knockShareReady) ||
(text === '下载' && this.downloadedActive))
Text(text).fontSize(11).fontColor(this.ink).margin({ top: 6 })
}
.onClick(() => {
if (text === '重播') this.replayVideo();
else if (text === '收藏') this.favoriteActive = !this.favoriteActive;
else if (text === '分享') this.shareCurrentVideo();
else if (text === '碰一碰') this.enableKnockShare();
else this.saveCurrentVideo();
})
}
8. 响应式适配细节
页面中大量使用 BreakPointType 实现响应式布局:
typescript
private pageEdge(): number {
return new BreakPointType<number>({ sm: 12, md: 24, lg: 36, xl: 56 })
.getValue(this.currentBreakpoint);
}
private panelWidth(): string {
return new BreakPointType<string>({ sm: '90%', md: '86%', lg: '74%', xl: '64%' })
.getValue(this.currentBreakpoint);
}
private heroHeight(): number {
return new BreakPointType<number>({ sm: 420, md: 430, lg: 460, xl: 480 })
.getValue(this.currentBreakpoint);
}
规律总结:小屏幕用更大的百分比宽度(90%),大屏幕用更小的百分比(64%),配合更大的内边距和字号,让布局在手机、平板、折叠屏上都呈现最佳视觉效果。
9. 知识链路回顾
通过 Index.ets 的完整分析,我们可以清楚地看到前 7 篇文章的知识点如何组合在一起:
| 知识点 | 所在文章 | 在 Index.ets 中的应用 |
|---|---|---|
@Entry / @Component |
1.1 | 组件声明 (struct Index) |
| 基础组件 (Text/Image/Stack/Column) | 1.2 | 页面中几乎所有 UI 元素 |
@State 状态管理 |
1.3 | currentTab、secondaryPage、chatMessages 等 20+ 状态 |
@Builder 封装复用 |
1.4 | 18 个 @Builder 方法(NavIcon/HomePage/ChatBubble 等) |
@StorageLink + BreakpointSystem |
1.5 | currentBreakpoint + 响应式尺寸计算 |
| Router 路由 | 1.6 | pushUrl 跳转到独立页面,aboutToAppear 中初始化 |
$r() / $rawfile() |
1.7 | 引用图片和视频资源 |
这些知识点在 Index.ets 中不是孤立使用的,而是相互配合:
@State驱动的currentTab控制CurrentPage()的渲染分支@Builder封装了可复用的 UI 片段(如 NavIcon、Header、SectionTitle)- 生命周期方法
aboutToAppear中完成路由参数读取、系统服务注册和定时器启动 - 响应式断点控制所有尺寸相关的数值
- 资源引用贯穿整个页面的图片和视频显示
10. 总结
本篇通过对「画伴梦工厂」Index.ets 的全面剖析,展示了如何使用 ArkUI 的组合式 API 构建一个生产级别的多 Tab 主页。
核心设计原则:
- 状态驱动视图 ------所有页面切换由
@State变量控制 - Builder 拆分职责 ------每个功能区域封装为独立的
@Builder - 响应式适配 ------通过
BreakPointType实现多屏幕适配 - 生命周期管理 ------
aboutToAppear/aboutToDisappear中做好初始化和清理 - 路由解耦 ------Tab 内切换用状态控制,出 Tab 跳转用
pushUrl
至此,第一章(鸿蒙 ArkUI 基础与组合式 API)全部完结。我们从最基础的声明式 UI 语法开始,一路学习了组件、布局、状态管理、Builder 封装、响应式布局、路由与生命周期、资源管理,最终完成了这个综合性 Tab 主页的分析。
下章预告:第二章 鸿蒙多媒体与交互开发。我们将深入学习 Video 播放、语音识别、动画系统、碰一碰分享等多媒体和交互能力,敬请期待!
思考题 :
Index.ets有将近 2700 行代码。如果让你重构,你会怎么拆分?例如:把 HomePage、CreationPage、WorksPage 分别拆成独立的@Component文件?需要考虑状态共享的问题,你会如何设计?