HarmonyOS NEXT ArkTS 布局体系深度解析 —— 从 Column 到 Scroll,从组件化到数据驱动


1. 引言:鸿蒙原生开发的范式之变
HarmonyOS NEXT 是华为全栈自研的操作系统,从内核到框架彻底去除了 AOSP 代码。对于应用开发者来说,最直观的变化是:不再支持 Android 应用兼容,必须使用鸿蒙原生语言开发。
ArkTS 是鸿蒙原生应用的首选开发语言,它基于 TypeScript 语法扩展,继承了 TypeScript 的类型安全特性,同时增加了 ArkUI 声明式 UI 框架所需的 @Component、@State、@Prop 等装饰器语法。
这套技术栈的核心思路可以概括为:
ArkTS(语言层)→ 声明式 UI 描述 → ArkUI 运行时(渲染层)→ 鸿蒙内核
本文将以一个完整的信息流应用为例,从最简单的 Column 布局开始,逐步深入到 Scroll 滚动容器、组件化架构、响应式数据管理和算法实现,帮助读者系统掌握鸿蒙 NEXT 应用开发的核心技能。
2. 项目全景:从结构到运行
先来看项目目录结构,理解一个鸿蒙 NEXT 应用是如何组织的:
MyApplication/
├── AppScope/ # 应用全局配置
│ ├── app.json5 # 应用级配置(bundleName、版本号等)
│ └── resources/ # 全局资源
├── entry/ # 主模块(entry 类型)
│ ├── src/main/
│ │ ├── ets/ # ArkTS 源码目录
│ │ │ ├── components/ # 可复用组件
│ │ │ ├── entryability/ # Ability(页面生命周期)
│ │ │ ├── model/ # 数据模型与算法
│ │ │ └── pages/ # 页面
│ │ ├── module.json5 # 模块配置
│ │ └── resources/ # 模块级资源
│ ├── build-profile.json5 # 构建配置
│ └── oh-package.json5 # 依赖管理
└── build-profile.json5 # 项目级构建配置
核心配置文件解读:
build-profile.json5 确定了 SDK 版本:
{
"app": {
"products": [
{
"name": "default",
"targetSdkVersion": "6.1.1(24)",
"compatibleSdkVersion": "6.1.1(24)",
"runtimeOS": "HarmonyOS"
}
]
}
}
这里 targetSdkVersion 和 compatibleSdkVersion 均为 6.1.1(24),即 API 24,对应 HarmonyOS NEXT 6.1.1。
EntryAbility.ets 是应用的入口,通过 windowStage.loadContent 加载初始页面:
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/ColumnScrollPage', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'EnglishApp', 'Failed to load content: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'EnglishApp', 'Succeeded in loading content.');
});
}
}
每个 Ability 对应一个应用入口,loadContent 接受页面路径(基于 src/main/ets/pages/ 目录相对路径)。这里的 pages/ColumnScrollPage 对应 pages/ColumnScrollPage.ets 文件。
3. ArkTS 语言速览:声明式的力量
ArkTS 本质上是一种装饰器驱动的声明式 UI 语言。与传统的命令式编程不同,ArkTS 开发者只需描述 UI 应该是什么样子,框架自动负责渲染和更新。
3.1 核心装饰器
| 装饰器 | 作用 | 类比 |
|---|---|---|
@Component |
标记一个结构体为可复用的 UI 组件 | React 的 class component |
@Entry |
标记页面入口 | Vue 的页面级组件 |
@State |
声明响应式状态变量(可变) | React 的 useState |
@Prop |
声明从父组件传入的属性(不可变) | React 的 props |
@BuilderParam |
注入自定义构建内容 | Vue 的 slot 插槽 |
3.2 声明式语法的基本模式
@Component
struct MyComponent {
@State private count: number = 0;
build() {
Column() {
Text(`计数:${this.count}`)
.fontSize(16)
Button('增加')
.onClick(() => {
this.count++; // 状态变化 → 自动重渲染
})
}
.width('100%')
.padding(12)
}
}
关键点:
build()方法是组件的渲染入口- 链式调用
.attribute(value)设置样式属性 @State变量变化时,框架自动重新执行build()更新 UI- 不需要手动操作 DOM,也不需要维护 View 层状态同步
3.3 与标准 TypeScript 的差异
ArkTS 对 TypeScript 做了严格的限制以保证运行时性能:
- 不支持
any类型:所有变量必须有明确的类型 - 不支持 JavaScript 动态特性:如
eval、with、原型链操作 - 严格空安全:变量默认不可为
null/undefined,需要显式声明 - 有限制的表达式:部分复杂表达式需要在
build()之外计算
这些限制初看让人不习惯,但实际开发中能避免大量运行时错误,并且让框架的编译时优化成为可能。
4. 布局基石:Column 容器的深度拆解
在 ArkUI 中,布局容器是 UI 构建的核心。Column 是最基础的容器之一——它将子组件沿**垂直方向(主轴)**依次排列。
4.1 Column 的基本使用
Column() {
Text('第一项').fontSize(16)
Text('第二项').fontSize(16)
Text('第三项').fontSize(16)
}
.width('100%')
以上代码将三个文本垂直排列。Column 的特点:
| 特性 | 说明 |
|---|---|
| 主轴(Main Axis) | 垂直方向(从上到下) |
| 交叉轴(Cross Axis) | 水平方向(从左到右) |
| 主轴对齐控制 | justifyContent() |
| 交叉轴对齐控制 | alignItems() |
4.2 Column 与 Row 的对比
Column 和 Row 是 ArkUI 中最基础的两个布局容器,可以理解为 Flexbox 的两个方向特化:
Column = flex-direction: column
Row = flex-direction: row
| 容器 | 主轴方向 | 主轴控制 | 交叉轴控制 |
|---|---|---|---|
| Column | 垂直 ↓ | justifyContent | alignItems(HorizontalAlign) |
| Row | 水平 → | justifyContent | alignItems(VerticalAlign) |
4.3 实战:信息卡片组件的 Column 布布局
在我们的项目中,InfoCard 组件内部使用了 Column 来组织标题和描述:
@Component
struct InfoCard {
private item: InfoItem = { title: '', desc: '' };
private index: number = 0;
build() {
Column() {
// 标题
Text(this.item.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1a1a2e')
.lineHeight(22)
// 描述文字
Text(this.item.desc)
.fontSize(13)
.fontColor('#666666')
.lineHeight(20)
.margin({ top: 6 })
}
.alignItems(HorizontalAlign.Start) // 卡片内文字左对齐
.width('100%')
.padding(14)
.backgroundColor('#f8f9fc')
.borderRadius(10)
.shadow({ radius: 4, color: '#20000000', offsetX: 0, offsetY: 2 })
.margin({ bottom: 10 })
}
}
这里展示了几点:
alignItems(HorizontalAlign.Start):让卡片内的所有子组件水平左对齐,这是最常见的文字列表排版方式。- 链式调用的顺序:虽然链式调用可以任意排列,但推荐按"布局→视觉→交互"的顺序组织,提高可读性。
- 阴影的写法:
shadow属性接受一个对象,包含radius(模糊半径)、color(颜色,含 alpha)、offsetX/offsetY(偏移量)。
4.4 layoutWeight:弹性权重的妙用
在 Column 或 Row 中,layoutWeight 属性可以让子组件按比例分配剩余空间:
Column() {
Text('固定高度内容')
.height(40)
Text('占满剩余空间')
.layoutWeight(1) // 类似 flex: 1
.width('100%')
}
.width('100%')
.height(200)
这与 CSS Flexbox 中的 flex: 1 概念一致。在页面布局中,这是实现"固定头尾 + 中间撑满"的核心手段。
5. 主轴对齐:justifyContent 的精妙控制
justifyContent 控制子组件在主轴方向的排列方式。对于 Column 来说,就是垂直方向上的排列。
5.1 FlexAlign 枚举全览
enum FlexAlign {
Start, // 顶部起始排列(默认值)
Center, // 垂直居中排列
End, // 底部排列
SpaceBetween, // 均匀分布,首尾贴边
SpaceAround, // 均匀分布,两侧间距为中间的一半
SpaceEvenly, // 完全均匀分布,所有间距相等
}
5.2 实战:justifyContent(FlexAlign.Start)
在 Index.ets 的核心演示区中:
Column() {
// ... 大量子组件
}
.alignItems(HorizontalAlign.Start)
.justifyContent(FlexAlign.Start) // ★ 主轴顶部起始
.width('100%')
.height(0)
.layoutWeight(1)
.padding(16)
这里设置 height(0) + layoutWeight(1) 的组合值得注意:
height(0)先设置一个初始高度layoutWeight(1)告诉父容器:本 Column 占满所有剩余空间
当 Column 的高度被撑满后,justifyContent(FlexAlign.Start) 的效果就是:所有子组件紧凑排列在容器顶部,底部留空。
这与 FlexAlign.Center 的区别十分直观:
FlexAlign.Start: [A][B][C]_________
FlexAlign.Center: ______[A][B][C]______
FlexAlign.End: _________[A][B][C]
5.3 justifyContent 的调试思路
在鸿蒙开发中,布局不符合预期时,优先排查:
- 父容器是否有明确的高度/宽度? 没有明确尺寸时,justifyContent 可能不起作用。
- layoutWeight 是否正确设置? 在 Column 中,子组件需要撑满高度时,设置
layoutWeight(1)。 - 是否有 margin 或 padding 干扰? margin 在
justifyContent计算之外。
一个实用的调试技巧:给容器设置鲜艳的背景色,观察子组件实际的分布区域。
6. 交叉轴对齐:alignItems 的配合艺术
如果说 justifyContent 控制主轴方向(纵轴),那么 alignItems 就控制交叉轴方向(横轴)。在 Column 中,交叉轴是水平方向。
6.1 HorizontalAlign 枚举
enum HorizontalAlign {
Start, // 左对齐
Center, // 水平居中(默认值)
End, // 右对齐
}
注意 Column 的默认 alignItems 是 HorizontalAlign.Center,这与 CSS Flexbox 的 align-items: stretch 不同——ArkUI 的 Column 默认不会拉伸子组件的宽度。
6.2 对齐方式的组合效果
通过 justifyContent + alignItems 的不同组合,可以快速实现 9 种布局定位:
| justifyContent | alignItems(Start) | alignItems(Center) | alignItems(End) |
|---|---|---|---|
| Start | ⬆⬅ 左上 | ⬆ 上居中 | ⬆➡ 右上 |
| Center | 中左 | 正中 | 中右 |
| End | ⬇⬅ 左下 | ⬇ 下居中 | ⬇➡ 右下 |
6.3 父子组件对齐的传递与覆盖
父容器的 alignItems 会作用于所有直接子组件,但子组件可以通过自身的属性来调整:
Column() {
Text('左对齐')
.align(Alignment.Start) // 自定义对齐方式
Text('居中')
// 继承父容器的 alignItems
}
.alignItems(HorizontalAlign.Start)
align() 用法更灵活,可以精确控制单个组件在父容器中的对齐位置。
7. Scroll 滚动容器:突破屏幕边界
当内容超出屏幕尺寸时,滚动容器就是必需品。在 ArkUI 中,Scroll 组件提供了这个能力。
7.1 Scroll + Column 的核心模式
这是鸿蒙开发中最常见的滚动列表模式:
Scroll(scroller) {
Column({ space: 12 }) {
// 大量子组件...
ForEach(dataList, (item: Item) => {
ItemCard({ item: item })
}, (item: Item) => item.id.toString())
}
.width('100%')
}
.layoutWeight(1)
.width('100%')
.scrollBar(BarState.Auto)
核心原理:
┌─────────────────────────┐
│ 外层 Column(全屏) │
│ ┌───────────────────┐ │
│ │ 固定标题区 │ │ ← 固定高度,不参与滚动
│ ├───────────────────┤ │
│ │ Scroll 容器 │ │ ← layoutWeight(1) 撑满剩余
│ │ ┌───────────────┐ │ │
│ │ │ Column(内容) │ │ │ ← 高度由内容撑开
│ │ │ 卡片1 │ │ │
│ │ │ 卡片2 │ │ │
│ │ │ 卡片3 │ │ │
│ │ │ ... │ │ │
│ │ └───────────────┘ │ │
│ ├───────────────────┤ │
│ │ 固定底部状态栏 │ │ ← 固定高度,不参与滚动
│ └───────────────────┘ │
└─────────────────────────┘
关键洞察:Scroll 不关心内部 Column 有多少内容,它只负责两件事——① 检测内容高度是否超过自身容器高度;② 如果超过,启用滚动交互。
7.2 ForEach 的高性能渲染
ForEach 是 ArkUI 中的列表渲染函数,对应 React 的 map() 或 Vue 的 v-for:
ForEach(
array: any[], // 数据源
(item: any, index?: number) => void, // 内容生成器
key?: (item: any, index?: number) => string // 键值生成器
)
第三个参数 key 函数非常重要:它为每个列表项提供唯一标识,让框架在列表更新时可以复用已有组件,避免全量销毁重建。
ForEach(
this.getDisplayData(),
(item: ListEntry) => {
ListItemCard({ item: item })
},
(item: ListEntry) => item.id.toString() // 以 id 作为键
)
当 id 不变时,即使列表重新渲染,已有 ListItemCard 组件也不会重新创建,性能大大提升。
7.3 实战:江湖大事记的 50 条动态数据
我们的 ColumnScrollPage 包含 50 条精心编写的江湖内容数据,每条内容长度不一,自然地产生了不同高度的卡片:
private readonly fullData: ListEntry[] = [
{
id: 1,
category: '江湖轶事',
title: '无名剑客三招败尽江南七怪',
content: '昨夜西湖畔,一名无名剑客仅凭三招剑法便连败江南七怪...',
author: '百晓生',
likes: 342,
time: '巳时三刻',
tag: '热门'
},
// ... 49 条更多内容
];
每条 content 的长度从 50 到 200 字不等,对应渲染出的卡片高度也不同,让滚动效果更自然真实。
8. 三段式页面架构:固定头 + 滚动体 + 固定底
这是移动端最经典的页面架构之一。其核心在于:外层 Column 不可滚动,仅作为布局容器;内部的 Scroll 组件单独管理滚动行为。
8.1 架构实现
Column() {
// ── 段 1:顶部标题区(固定高度,不滚动)──
Column() {
Text('📜 江湖大事记')
.fontSize(20).fontWeight(FontWeight.Bold)
.fontColor('#ffffff')
Text('Column + Scroll 滚动纵向布局 · 内容超屏自动滚动')
.fontSize(12).fontColor('#cce0ff')
.margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding({ top: 20, bottom: 12, left: 20, right: 20 })
.backgroundColor('#2d5f8a')
// ── 段 2:核心 — Scroll 接管滚动 ──
Scroll(this.scroller) {
Column({ space: 12 }) {
ForEach(this.getDisplayData(), (item: ListEntry) => {
ListItemCard({ item: item })
}, (item: ListEntry) => item.id.toString())
}
.width('100%')
.padding({ left: 12, right: 12, top: 10, bottom: 10 })
}
.layoutWeight(1) // ★ 占满剩余空间
.width('100%')
.scrollBar(this.currentBarState)
.edgeEffect(EdgeEffect.Spring)
// ── 段 3:底部状态条(固定高度,不滚动)──
Row() {
// 状态信息...
}
.width('100%')
.height(36)
.backgroundColor('#ffffff')
}
.width('100%')
.height('100%')
8.2 为什么外层 Column 不能滚动?
这是一个容易犯的错误:如果把滚动的能力放在外层 Column 上,那么整个页面都会滚动,包括标题栏和底部状态栏——这会导致标题栏滚动出屏幕,底部状态栏消失在视口之外,这是不符合移动端设计规范的。
正确做法是:只让内容区滚动,头部和底部始终固定。
8.3 layoutWeight 在分段布局中的作用
layoutWeight 在分段布局中的作用类似于 Android 的 layout_weight 或 CSS Flexbox 的 flex: 1:
- 标题区:固定高度(由内部内容撑开)
- 控制面板:固定高度(由内部内容撑开)
- Scroll 区:
layoutWeight(1)→ 吃掉父容器去掉头部和底部后的所有剩余空间 - 底部状态栏:固定高度(
height(36))
这种布局方式确保了 Scroll 区总能得到精确的可用高度,不多不少,滚动行为自然正确。
9. 编程式滚动:Scroller 控制器的妙用
Scroller 是 ArkUI 中控制 Scroll 容器的编程接口,可以实现编程式滚动、获取滚动状态等高级功能。
9.1 Scroller 的基本用法
// 1. 创建 Scroller 实例
private scroller: Scroller = new Scroller();
// 2. 绑定到 Scroll
Scroll(this.scroller) {
// ...
}
// 3. 编程控制
this.scroller.scrollTo({
xOffset: 0,
yOffset: 0,
animation: { duration: 500 } // 动画时长 500ms
});
9.2 滚动到指定位置
// 滚动到顶部(带动画)
this.scroller.scrollTo({
xOffset: 0,
yOffset: 0,
animation: { duration: 500 }
});
// 滚到底部(使用 scrollEdge)
this.scroller.scrollEdge(Edge.Bottom);
scrollTo 和 scrollEdge 的区别:
| 方法 | 适用场景 | 特点 |
|---|---|---|
scrollTo({xOffset, yOffset, animation}) |
精确位置 | 指定具体偏移量,可带动画 |
scrollEdge(Edge.Top/Bottom) |
到顶/到底 | 直接滚动到边缘,精确快速 |
9.3 滚动事件监听
Scroll(this.scroller) {
// ...
}
.onScroll((xOffset: number, yOffset: number) => {
// ★ 实时获取滚动偏移量
this.currentOffset = `${Math.round(yOffset)}vp`;
})
.onScrollStop(() => {
hilog.info(0x0000, TAG, 'Scroll stopped at: %s', this.currentOffset);
})
.onScrollEdge((side: Edge) => {
if (side === Edge.Top) {
hilog.info(0x0000, TAG, 'Reached top');
} else if (side === Edge.Bottom) {
hilog.info(0x0000, TAG, 'Reached bottom');
}
})
三个事件的配合十分实用:
onScroll:实时反馈,适合更新 UI(如位置指示器、回到顶部按钮的显隐)onScrollStop:滚动停止后触发,适合记录日志、触发数据加载onScrollEdge:触边事件,适合实现"加载更多"(到底部触发)
9.4 实战:滚动控制面板
我们的应用包含一个完整的滚动控制面板,提供:
- 实时滚动位置显示:
偏移:1256vp - 一键回到顶部 / 滚到底部
- 动态增加列表项(每次 +5 条)
- 重置列表(回到 10 条)
- 滚动条模式切换(Auto / On / Off)
// 回到顶部
Button('⬆ 回到顶部')
.onClick(() => {
this.scroller.scrollTo({
xOffset: 0,
yOffset: 0,
animation: { duration: 500 }
});
})
// 滚到底部
Button('⬇ 滚到底部')
.onClick(() => {
this.scroller.scrollEdge(Edge.Bottom);
})
这个面板不仅方便开发和调试,也是向用户展示"我可以编程控制滚动"的绝佳交互示例。
10. 滚动条状态管理:Auto / On / Off
scrollBar() 方法控制滚动条的显示行为,接受 BarState 枚举值:
enum BarState {
Auto, // 滚动时显示,停止后自动隐藏(默认)
On, // 始终显示
Off, // 始终隐藏
}
10.1 状态管理与交互反馈
在我们的应用中,滚动条状态通过 @State 变量管理,并提供可视化切换:
@State private currentBarState: BarState = BarState.Auto;
// 切换状态
private setBarState(state: BarState): void {
this.currentBarState = state;
const labels: string[] = ['自动显示', '常时显示', '始终隐藏'];
const idx = [BarState.Auto, BarState.On, BarState.Off].indexOf(state);
promptAction.showToast({
message: `滚动条模式:${labels[idx]}`,
duration: 1500
});
}
UI 上的切换按钮使用条件样式来指示当前激活状态:
Text('Auto')
.fontColor(this.currentBarState === BarState.Auto ? '#fff' : '#666')
.backgroundColor(this.currentBarState === BarState.Auto ? '#3a7bd5' : '#f0f0f0')
.onClick(() => { this.setBarState(BarState.Auto); })
10.2 滚动条宽度定制
.scrollBarWidth(6) // 自定义滚动条宽度为 6vp
鸿蒙对滚动条的定制能力相当灵活,包括宽度、颜色、圆角等都可以在主题中统一配置。
10.3 边界回弹效果
.edgeEffect(EdgeEffect.Spring) // 弹簧回弹效果
EdgeEffect 提供了两种选择:
Spring:像 iOS 一样的弹簧回弹效果,滚动到边界时出现弹性阻尼None:硬边界,到顶/到底立即停止
Spring 效果更接近用户直觉,建议在绝大多数场景中使用。
11. 组件化设计:从 @Component 到 @BuilderParam
组件化是大型应用开发的基石。ArkTS 提供了完整的组件化机制,从基础 @Component 到高级的 @BuilderParam 插槽模式。
11.1 组件的层级结构
在我们的项目中,组件层级可以清晰地梳理为:
App (EntryAbility)
└── ColumnScrollPage (@Entry @Component)
├── AppHeader (可复用顶部标题栏)
├── ScrollControlBar (滚动控制面板)
│ └── 内置 Button × 4
├── Scroll
│ └── Column
│ └── ListItemCard × N (每一条江湖消息)
└── 底部状态栏 (内联)
11.2 子组件:ListItemCard
ListItemCard 是一个典型的数据驱动子组件,通过 @Prop 接收数据并渲染:
@Component
struct ListItemCard {
private item: ListEntry = {
id: 0, category: '', title: '', content: '',
author: '', likes: 0, time: '', tag: ''
};
build() {
Column() {
// ① 分类 + 标签(Row 横向分布)
Row() {
Text(this.item.category)
.fontSize(10)
.fontColor('#ffffff')
.backgroundColor(this.getCategoryColor(this.item.category))
.padding({ left: 8, right: 8, top: 2, bottom: 2 })
.borderRadius(4)
if (this.item.tag) {
Text(this.item.tag)
.fontSize(10)
.fontColor('#ff9800')
.border({ width: 1, color: '#ff9800' })
.borderRadius(4)
.margin({ left: 6 })
}
Text(this.item.time)
.layoutWeight(1) // 占满剩余,实现右对齐
.textAlign(TextAlign.End)
}
.alignItems(VerticalAlign.Center)
.width('100%')
// ② 标题
Text(this.item.title)
.fontSize(16).fontWeight(FontWeight.Bold)
.fontColor('#1a1a2e').lineHeight(24)
.margin({ top: 8 })
// ③ 正文(长度不一 → 卡片高度不同)
Text(this.item.content)
.fontSize(13).fontColor('#555555')
.lineHeight(22).margin({ top: 6 })
// ④ 底部信息栏
Row() {
Row() {
Text('✍').fontSize(12)
Text(this.item.author)
.fontSize(12).fontColor('#888888')
.margin({ left: 4 })
}
.layoutWeight(1)
Row() {
Text('👍').fontSize(12)
Text(`${this.item.likes}`)
.fontSize(12).fontColor('#888888')
.margin({ left: 4 })
}
.margin({ right: 16 })
Text('阅读全文 ›')
.fontSize(12).fontColor('#3a7bd5')
.onClick(() => {
promptAction.showToast({
message: `打开「${this.item.title}」`,
duration: 1500
});
})
}
.margin({ top: 10 })
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(14)
.backgroundColor('#ffffff')
.borderRadius(10)
.shadow({ radius: 4, color: '#18000000', offsetX: 0, offsetY: 2 })
}
}
组件设计要点:
- 约定优于配置:属性名避开与
CommonAttribute重名的关键字(如backgroundColor、borderRadius),改用带业务含义的名称 - 默认值兜底:
ListItemEntry的成员都给了空值,避免数据缺失时的渲染崩溃 - 内联方法封装:
getCategoryColor()将分类到颜色的映射逻辑封装在组件内部,调用者无需关心 - 响应式交互:使用
onClick配合promptAction.showToast提供即时反馈
11.3 通用组件:Card 容器
通用卡片容器使用 @BuilderParam 实现类似于 Vue slot 或 React children 的内容注入:
@Component
export struct Card {
@Prop cardPadding: number = 16;
@Prop cardMargin: number = 12;
@Prop cardColor: string = '#ffffff';
@Prop cardRadius: number = 16;
@BuilderParam content: () => void = this.defaultContent;
@Builder
defaultContent(): void {
Text('卡片内容')
.fontSize(14)
.fontColor('#888')
}
build() {
Column() {
this.content() // ★ 调用注入的内容
}
.width('100%')
.padding(this.cardPadding)
.backgroundColor(this.cardColor)
.borderRadius(this.cardRadius)
.margin({ bottom: this.cardMargin })
.shadow({ radius: 4, color: '#1a000000', offsetX: 0, offsetY: 2 })
}
}
使用方式:
Card({
cardPadding: 12,
cardColor: '#f8f9fc',
cardRadius: 10
}) {
// ★ 这里的内容会替换 this.content()
Text('自定义卡片内容')
Button('点击操作')
}
@BuilderParam 是 ArkTS 实现"插槽"模式的官方方式,它比通过 @Prop 传递整个组件实例更为灵活。
11.4 通用组件:AppHeader
应用通用的顶部标题栏,支持显示返回按钮:
@Component
export struct AppHeader {
@Prop headerTitle: string = '';
@Prop headerSubtitle: string = '';
@Prop showBack: boolean = false;
onBack: () => void = () => {};
build() {
Row() {
if (this.showBack) {
Text('←')
.fontSize(22).fontColor('#ffffff')
.onClick(() => { this.onBack(); })
.margin({ right: 8 })
}
Column() {
Text(this.headerTitle)
.fontSize(20).fontWeight(FontWeight.Bold)
.fontColor('#ffffff')
if (this.headerSubtitle.length > 0) {
Text(this.headerSubtitle)
.fontSize(12).fontColor('#cce0ff')
.margin({ top: 2 })
}
}
.alignItems(HorizontalAlign.Start)
Blank() // 占满剩余
}
.width('100%')
.padding({ top: 12, bottom: 12, left: 20, right: 20 })
.backgroundColor('#2d5f8a')
}
}
Blank() 是一个空心占位组件,在 Row 中会自动撑满剩余空间,实现"文字左对齐"的效果——这比在 CSS 中使用 justify-content: space-between 更简洁。
11.5 通用组件:ModuleEntryCard
模块快捷入口卡片,用于首页的功能导航矩阵:
@Component
export struct ModuleEntryCard {
@Prop entryIcon: string = '';
@Prop entryLabel: string = '';
@Prop entryColor: string = '#3a7bd5';
onClickAction: () => void = () => {};
build() {
Column() {
Text(this.entryIcon).fontSize(32).margin({ bottom: 8 })
Text(this.entryLabel)
.fontSize(13).fontColor('#333')
.fontWeight(FontWeight.Medium)
}
.width('30%') // 每行放 3 个
.aspectRatio(1.0) // 正方形
.justifyContent(FlexAlign.Center)
.backgroundColor((this.entryColor + '18')) // 18 = 10% 透明度
.borderRadius(16)
.onClick(() => { this.onClickAction(); })
.shadow({ radius: 2, color: '#10000000', offsetX: 0, offsetY: 1 })
}
}
aspectRatio(1.0) 是鸿蒙布局中非常实用的属性——设置宽高比后,组件会根据宽度自动计算高度,无需手动指定。这里实现的正方形入口卡片,无论屏幕宽度如何变化,始终是 1:1 的正方形。
12. 数据模型设计:英语学习 App 的数据层
除了江湖信息流页面,项目中还包含一个完整的英语学习应用的数据模型层,涵盖单词库、听力、阅读、语法、口语五大模块。
12.1 核心数据结构
/** 单词条目 */
export interface WordItem {
id: number;
word: string; // 英文单词
phonetic: string; // 音标
translation: string; // 中文释义
partOfSpeech: string; // 词性
exampleSentence: string; // 例句
exampleTranslation: string;// 例句翻译
difficulty: Difficulty; // 难度
category: string; // 分类
audioPath: string; // 音频路径
}
/** 学习记录 */
export interface StudyRecord {
wordId: number;
reviewCount: number; // 复习次数
correctCount: number; // 正确次数
lastReviewTime: string; // 上次复习时间
masteryLevel: number; // 掌握度 0.0-1.0
}
设计亮点:
StudyRecord与WordItem通过wordId关联,实现"数据"与"进度"的分离masteryLevel使用 0.0-1.0 的浮点数,比离散的等级(熟悉/不熟悉)更精细difficulty使用枚举而非字符串,类型安全且可排序
12.2 难度与词性枚举
export enum Difficulty {
EASY = 1,
MEDIUM = 2,
HARD = 3,
}
export enum PartOfSpeech {
NOUN = 'n.',
VERB = 'v.',
ADJ = 'adj.',
ADV = 'adv.',
PREP = 'prep.',
CONJ = 'conj.',
PRON = 'pron.',
INTERJ = 'interj.',
}
使用枚举而非字符串常量有两大好处:编译时类型检查和 IDE 自动补全。
12.3 样例数据:30 个核心单词
export const SAMPLE_WORDS: WordItem[] = [
{
id: 1,
word: 'abandon',
phonetic: '/əˈbændən/',
translation: '放弃;遗弃',
partOfSpeech: 'v.',
exampleSentence: 'They had to abandon the project due to lack of funds.',
exampleTranslation: '由于缺乏资金,他们不得不放弃这个项目。',
difficulty: Difficulty.MEDIUM,
category: '基础词汇',
audioPath: ''
},
// ... 29 个更多单词
];
数据的组织按难度分层:
| 难度 | 数量 | 代表词汇 |
|---|---|---|
| EASY(基础) | ~12 个 | benefit, challenge, journey, knowledge |
| MEDIUM(核心) | ~10 个 | determine, maintain, negotiate, adapt |
| HARD(进阶) | ~8 个 | elaborate, hypothesis, phenomenon, sustainable |
这种分层设计方便后续的"困难优先"复习策略——系统可以自动安排更多时间给高难度词汇。
12.4 阅读与语法模块
阅读文章包含完整的内容和配套问题:
export interface ReadingArticle {
id: number;
title: string;
level: string; // 初级 / 中级 / 高级
wordCount: number;
content: string; // 文章正文
questions: ReadingQuestion[];
}
export interface ReadingQuestion {
id: number;
type: 'main_idea' | 'detail' | 'inference' | 'vocabulary';
question: string;
options: string[];
correctIndex: number;
}
type 字段使用联合类型(Union Type),限定了四种问题类型,保证类型安全的同时,也让 UI 可以根据问题类型展示不同的交互样式。
语法练习题使用 explanation 字段提供详细的解析:
{
id: 1,
topic: '时态 - 现在完成时',
question: 'She ___ in London for five years.',
options: ['live', 'lives', 'has lived', 'is living'],
correctIndex: 2,
explanation: '现在完成时表示从过去持续到现在的动作:has/have + 过去分词。'
}
这种"题目 + 选项 + 答案 + 解析"的四要素结构,是教育类应用数据设计的标准模式。
13. 间隔重复引擎:SM-2 算法的鸿蒙实现
间隔重复(Spaced Repetition)是语言学习类应用的灵魂。我们基于 SuperMemo 的 SM-2 算法实现了复习调度引擎。
13.1 算法的核心参数
export interface ReviewResult {
nextInterval: number; // 下次复习间隔(天)
newRepetition: number; // 更新后的连续正确次数
newEf: number; // 更新后的易度系数
nextReview: Date; // 下次复习日期
}
三个核心参数构成一个状态三元组 (interval, repetition, ef):
interval:距离下次复习的天数,决定何时再次见到这个单词repetition:连续答对次数,决定当前学习阶段的进度ef(Easiness Factor):易度系数,反映单词对用户的难易程度
13.2 调度逻辑
export class SpacedRepetitionEngine {
private static readonly DEFAULT_EF = 2.5;
private static readonly MIN_EF = 1.3;
private static readonly MAX_INTERVAL = 180;
static schedule(
quality: number, // 0-5 的回答质量评分
previousInterval: number, // 上次间隔
repetition: number, // 连续正确次数
previousEf: number = SpacedRepetitionEngine.DEFAULT_EF,
): ReviewResult {
// 回答不合格 → 重置进度
if (quality < 3) {
return {
nextInterval: 1,
newRepetition: 0,
newEf: SpacedRepetitionEngine.updateEf(previousEf, quality),
nextReview: new Date(Date.now() + 86400000), // 明天
};
}
// 回答合格 → 正常排程
const newEf = SpacedRepetitionEngine.updateEf(previousEf, quality);
let nextInterval: number;
if (repetition === 0) {
nextInterval = 1;
} else if (repetition === 1) {
nextInterval = 3;
} else {
nextInterval = Math.round(previousInterval * newEf);
}
nextInterval = Math.min(nextInterval, SpacedRepetitionEngine.MAX_INTERVAL);
return {
nextInterval,
newRepetition: repetition + 1,
newEf,
nextReview: new Date(Date.now() + nextInterval * 86400000),
};
}
}
回答质量评分(quality)对照表:
| 评分 | 含义 | 处理逻辑 |
|---|---|---|
| 0 | 完全忘记 | 重置 → 明天复习 |
| 1 | 错误但能回忆部分 | 重置 → 明天复习 |
| 2 | 错误但感觉容易 | 重置 → 明天复习 |
| 3 | 正确但有困难 | 间隔增长 |
| 4 | 正确但稍有犹豫 | 间隔增长 |
| 5 | 完全正确且流利 | 间隔增长 |
间隔增长算法(repetition >= 2):
newInterval = previousInterval × EF
其中 EF 在每次复习后更新:
EF' = EF + (0.1 - (5 - q) × (0.08 + (5 - q) × 0.02))
这个公式是 SM-2 的核心:如果用户总是能正确回答(quality 高),EF 增长缓慢但稳定,间隔时间不断延长;如果用户经常答错,EF 下降,间隔缩短,复习更频繁。
13.3 掌握度可视化
static getMasteryColor(mastery: number): string {
if (mastery >= 0.8) return '#00b894'; // 绿色 - 已掌握
if (mastery >= 0.5) return '#ff9f43'; // 橙色 - 学习中
if (mastery >= 0.2) return '#e17055'; // 浅红 - 需加强
return '#d63031'; // 红色 - 新词/陌生
}
static getMasteryLabel(mastery: number): string {
if (mastery >= 0.8) return '已掌握';
if (mastery >= 0.5) return '学习中';
if (mastery >= 0.2) return '需加强';
return '新词';
}
四个等级对应四种颜色和标签,可以用在单词列表的左侧指示条或进度圆环中,一目了然。
14. @State 响应式状态管理
@State 是 ArkTS 响应式系统的核心。当 @State 变量发生变化时,框架会自动重新渲染依赖该变量的 UI 部分。
14.1 @State 的基本规则
@State private currentBarState: BarState = BarState.Auto;
@State private currentOffset: string = '0vp';
@State private itemCount: number = 30;
@State private contentHeight: string = '计算中...';
规则:
- 必须使用
private修饰:@State只在组件内部访问 - 必须初始化:不给初始值会编译报错
- 赋值即触发更新:
this.itemCount = newCount→ 框架立即重渲染 - 复杂类型也能响应:数组、对象等引用类型只要重新赋值就会触发
14.2 @State 与 UI 的绑定
@State 变量可以直接在 build() 中渲染,框架会自动追踪依赖关系:
text(`偏移:${this.currentOffset}`)
// 当 this.currentOffset 变化时,只有这行 Text 会重新渲染
Text(`共 ${this.itemCount} 条`)
// 当 this.itemCount 变化时,这行 Text 也会自动更新
.scrollBar(this.currentBarState)
// 当 this.currentBarState 变化时,Scroll 的滚动条模式切换
增量更新:框架不会重建整个页面,只会更新依赖了变化状态的那部分 UI。这是声明式框架相比传统命令式的核心性能优势。
14.3 状态与事件的交互模式
典型的"事件 → 状态 → UI"链路:
用户点击按钮
→ onClick 回调
→ 修改 @State 变量
→ 框架检测到变化
→ 重新执行 build() 中依赖该变量的部分
→ UI 更新
以我们的增加列表功能为例:
// Step 1: 用户点击 "+ 增 5 条" 按钮
Button('+ 增 5 条')
.onClick(() => {
// Step 2: 修改 @State 变量
this.addItems();
})
// Step 3: 状态更新方法
private addItems(): void {
const newCount = Math.min(this.itemCount + 5, this.fullData.length);
this.itemCount = newCount; // ← @State 变化
this.contentHeight = `${newCount * 150}vp+`;
// Step 4: 交互反馈
promptAction.showToast({
message: `增加至 ${newCount} 条内容`,
duration: 1500
});
}
14.4 状态与列表渲染
当 itemCount 变化时,getDisplayData() 返回的数组长度改变,ForEach 只增加/减少对应的列表项组件,已有项复用不变:
private getDisplayData(): ListEntry[] {
return this.fullData.slice(0, this.itemCount);
}
fullData 不是 @State,因为它从不变化(相当于常量池)。只有 itemCount 是响应式的,控制着"切片"的大小。
15. Canvas 绘图:圆形进度条的自绘实现
ProgressRing 组件使用 ArkUI 的 Canvas 组件实现了圆形进度条,展示了 ArkTS 绘图能力。
15.1 Canvas 基础
private ringContext: CanvasRenderingContext2D = new CanvasRenderingContext2D();
private progressContext: CanvasRenderingContext2D = new CanvasRenderingContext2D();
build() {
Stack() {
Canvas(this.ringContext) // 背景圆环
.width(this.ringSize)
.height(this.ringSize)
.onReady(() => {
this.drawRing(this.ringContext, 100, true);
})
Canvas(this.progressContext) // 进度弧
.width(this.ringSize)
.height(this.ringSize)
.onReady(() => {
this.drawRing(this.progressContext, this.ringProgress, false);
})
Column() { // 中心文字
Text(this.ringValue).fontSize(16).fontWeight(FontWeight.Bold)
Text(this.ringLabel).fontSize(10).fontColor('#888').margin({ top: 2 })
}
}
}
15.2 绘图逻辑
drawRing(ctx: CanvasRenderingContext2D, value: number, isBg: boolean): void {
const size = this.ringSize;
const stroke = this.ringStroke;
const cx = size / 2;
const cy = size / 2;
const r = (size - stroke) / 2;
const startAngle = -Math.PI / 2; // 从顶部开始(12 点钟方向)
const endAngle = startAngle + (value / 100) * 2 * Math.PI;
ctx.beginPath();
ctx.arc(cx, cy, r, startAngle, endAngle);
ctx.strokeStyle = isBg ? this.ringBgColor : this.ringColor;
ctx.lineWidth = stroke;
ctx.lineCap = 'round';
ctx.stroke();
}
绘图要点:
- 双 Canvas 分层绘制:背景圆环使用一个 Canvas,进度弧使用另一个 Canvas——这样进度变化时只需重新绘制进度弧,背景无需重绘
- 起始角度选择:
-Math.PI / 2(-90°)使弧线从顶部开始绘制,更符合进度条的习惯 lineCap: 'round':端点圆角,避免进度弧两端出现尖锐的直角onReady回调:Canvas 准备就绪后才会触发绘图,防止在 Canvas 未初始化时绘图导致错误
这种"两个 Canvas + Stack 叠加"的模式,在实现圆形进度条时比使用单个 Canvas 更高效,因为进度变化时只触发 progressContext 的 onReady,ringContext 保持不变。
16. 性能优化与最佳实践
16.1 ForEach 的键值策略
反例(性能差):
ForEach(
this.dataList,
(item) => { ItemCard({ item }) },
(item) => Math.random().toString() // 每次都不一样,所有组件都重建
)
正例(性能优):
ForEach(
this.getDisplayData(),
(item) => { ListItemCard({ item: item }) },
(item: ListEntry) => item.id.toString() // 稳定唯一标识
)
稳定键值让框架可以复用已有组件实例,避免销毁重建的开销。
16.2 避免不必要的状态更新
不必要的状态更新会导致额外的渲染周期。一个常见的优化是使用条件判断:
// 优化前:每次滑动都 setState
.onScroll((x, y) => {
this.currentOffset = `${Math.round(y)}vp`;
})
// 优化思路:只在变化幅度超过阈值时才更新
// 由于我们的应用需要实时位置显示,保留高频更新
对于不需要实时显示的场景,建议使用 onScrollStop 而不是 onScroll。
16.3 组件粒度控制
组件拆分越细,单次更新的范围越小。但也不是越细越好,需要在可维护性和性能之间平衡:
过粗:一个组件包含整个页面 → 任何小变化都导致全量重渲染
过细:每个 Text 都是一个独立组件 → 组件间通信成本高
适中:按功能模块拆分,每个模块 30-100 行
我们的应用中,ColumnScrollPage 约 600 行,内部通过 ListItemCard、ScrollControlBar 等子组件分解,粒度适中。
16.4 列表性能的 scale 策略
对于超长列表(1000+ 条),Scroll + Column 模式会一次性渲染所有子组件,性能较差。此时应该使用 LazyForEach 代替 ForEach,实现虚拟列表——只渲染可视区域内的组件:
// 适用于超长列表
LazyForEach(this.dataSource, (item: ListEntry) => {
ListItemCard({ item: item })
}, (item: ListEntry) => item.id.toString())
LazyForEach 需要一个实现了 IDataSource 接口的数据源对象,它会在列表滚动时按需创建和回收组件。
16.5 关于模块化路径的注意事项
在我们的项目中,使用 @State 时需要注意模块化路径。例如,组件属性的命名要避开 ArkUI 内部关键字:
// 正确:使用业务语义的名字
@Prop cardColor: string = '#ffffff';
@Prop cardRadius: number = 16;
// 错误:与 CommonAttribute 方法重名
// @Prop backgroundColor: string = '#ffffff';
// @Prop borderRadius: number = 16;
16.6 hilog 调试的最佳实践
const TAG = 'ScrollColumnDemo';
// 使用 TAG 区分模块
hilog.info(0x0000, TAG, 'Scroll stopped at: %s', this.currentOffset);
hilog.info(0x0000, TAG, 'Card clicked: %s', this.item.title);
// 错误日志带 JSON 格式化
hilog.error(0x0000, TAG, 'Failed: %{public}s', JSON.stringify(error));
hilog 是鸿蒙的日志系统,支持格式化输出和隐私标记(%{public}s / %{private}s)。建议为每个模块定义独立的 TAG,便于日志过滤。
17. 总结与展望
17.1 架构全景回顾
通过本文,我们从零到一拆解了一个鸿蒙 NEXT 应用的全貌:
┌──────────────────────────────────────────────────────┐
│ 应用架构全景 │
├──────────┬───────────────────────────────────────────┤
│ UI 层 │ @Component / @Entry 组件树 │
│ │ Column + Scroll + Stack 布局容器 │
│ │ ForEach 列表渲染 │
├──────────┼───────────────────────────────────────────┤
│ 交互层 │ onClick 事件处理 │
│ │ @State 响应式状态管理 │
│ │ Scroller 编程式滚动 │
├──────────┼───────────────────────────────────────────┤
│ 数据层 │ Interface / Enum 数据模型 │
│ │ SampleData 样例数据 │
│ │ SM-2 间隔重复算法 │
├──────────┼───────────────────────────────────────────┤
│ 能力层 │ Ability 生命周期 │
│ │ prompAction 交互反馈 │
│ │ hilog 日志系统 │
└──────────┴───────────────────────────────────────────┘
17.2 核心知识点速查表
| 知识点 | 关键方法/属性 | 使用场景 |
|---|---|---|
| Column 布局 | justifyContent() alignItems() |
垂直排列内容 |
| 弹性撑满 | layoutWeight(1) |
分段式布局中间撑满 |
| 滚动容器 | Scroll(scroller) { Column() {} } |
超屏内容展示 |
| 编程式滚动 | scrollTo() scrollEdge() |
回到顶部/底部 |
| 滚动条控制 | scrollBar(BarState) |
切换显示模式 |
| 边界效果 | edgeEffect(EdgeEffect.Spring) |
弹簧回弹 |
| 响应式数据 | @State |
状态驱动 UI 更新 |
| 组件属性 | @Prop |
父子组件传值 |
| 内容插槽 | @BuilderParam |
自定义卡片内容 |
| 列表渲染 | ForEach(array, fn, keyFn) |
数据列表展示 |
| Canvas 绘图 | CanvasRenderingContext2D |
自定义图形绘制 |
| 交互反馈 | promptAction.showToast() |
用户操作即时提示 |
17.3 展望
HarmonyOS NEXT 的 ArkTS / ArkUI 技术栈仍处于快速演进阶段,未来值得关注的方向包括:
-
状态管理进阶:随着应用规模增长,需要引入类似 Redux/Pinia 的全局状态管理方案。目前社区正在探索基于
@Provide/@Consume装饰器的跨组件状态传递模式。 -
自定义组件库建设:本文展示的
Card、ProgressRing、AppHeader等组件只是起点,企业级应用需要更完善的组件库体系。 -
动画与手势:ArkUI 提供了
animateTo、transition等动画 API,以及PanGesture、SwipeGesture等手势识别,可用于构建更丰富的交互体验。 -
端侧 AI 集成:鸿蒙 NEXT 提供了端侧 AI 能力,结合本文的学习数据,可以实现智能化的学习路径推荐。
-
多设备适配:鸿蒙的"一次开发,多端部署"理念要求代码能适配手机、平板、折叠屏等多种形态,ArkUI 的栅格系统 (
GridRow/GridCol) 是关键的适配手段。
附录:完整术语对照表
| 术语 | 说明 | 类似概念 |
|---|---|---|
| ArkTS | 鸿蒙原生应用开发语言,基于 TypeScript | Kotlin、Swift |
| ArkUI | 鸿蒙声明式 UI 框架 | SwiftUI、Jetpack Compose |
| Ability | 应用能力的封装,类似 Activity/Page | Android Activity |
| @Component | 组件装饰器 | @Composable、struct |
| @State | 组件内部响应式状态 | useState、MutableState |
| @Prop | 父组件传入的不可变属性 | React props |
| @BuilderParam | 内容插槽装饰器 | Vue slot、children |
| Column | 垂直布局容器 | flex-direction: column |
| Row | 水平布局容器 | flex-direction: row |
| Scroll | 滚动容器 | ScrollView |
| ForEach | 列表渲染函数 | Array.map()、v-for |
| layoutWeight | 弹性权重类似 flex | flex: 1 |
| Blank | 弹性占位组件 | Spacer、空 View |
| Stack | 层叠布局容器 | FrameLayout |
| hilog | 日志系统 | console.log、Logcat |
| prompAction | 原生交互反馈 | Toast、Dialog |
| vp | 虚拟像素,自适应单位 | dp、pt |
本文基于 HarmonyOS NEXT 6.1.1(API 24),SDK 版本
targetSdkVersion: "6.1.1(24)",开发环境为 DevEco Studio。文中所有代码均来源于实际可运行的项目,可在相应地目录下找到完整源码。
更多推荐




所有评论(0)