鸿蒙原生 ArkTS「行均分布栏」布局实战——基于 HarmonyOS NEXT 6.1.1(API 24)的完整应用开发全解析


一、引言
鸿蒙操作系统(HarmonyOS)自诞生以来,始终致力于构建面向全场景的分布式操作系统。随着 HarmonyOS NEXT 的发布,系统去除了 AOSP 兼容层,实现了完全的鸿蒙内核底座,这意味着开发者需要以纯正的 ArkTS 语言和 ArkUI 框架进行原生应用开发。在此背景下,深入理解 ArkTS 的声明式 UI 布局体系,成为每一位鸿蒙开发者必须掌握的核心技能。
本文将以一个真实的英语学习辅助应用项目为例,从项目架构、布局体系、组件设计、游戏交互到数据算法,全方位剖析 HarmonyOS NEXT 6.1.1(API 24)环境下的 ArkTS 开发实践。项目代码可在 DevEco Studio 中直接编译运行,所有示例均采用 Stage 模型与 ArkTS 语言,帮助开发者从零掌握鸿蒙原生应用的核心开发范式。
本文涵盖以下核心内容:
- 项目整体架构与目录规划
- ArkUI 布局体系深度解析(Column、Row、Stack、FlexAlign、layoutWeight)
- 可复用组件设计模式(Card、ProgressRing、ModuleEntryCard)
- Canvas 2D 游戏引擎实现(跑酷与光点追逐)
- SM-2 间隔重复算法在鸿蒙中的实现
- 工程化最佳实践与调试技巧
二、项目概览与架构设计
2.1 技术栈与开发环境
| 项目 | 版本/规格 |
|---|---|
| 操作系统 | HarmonyOS NEXT |
| SDK 版本 | 6.1.1(API 24) |
| 开发工具 | DevEco Studio |
| 编程语言 | ArkTS(声明式 UI) |
| 应用模型 | Stage 模型 |
| 目标设备 | Phone |
| 编译模式 | Stage Mode |
2.2 项目目录结构
项目采用了模块化的目录组织结构,将所有源代码放置于 entry/src/main/ets/ 目录下,按照功能分目录管理:
entry/src/main/ets/
├── entryability/ # Ability 入口
│ └── EntryAbility.ets
├── pages/ # 页面级组件
│ ├── Index.ets # Column + Start 演示
│ ├── RowSpaceEvenlyDemo.ets # Row + SpaceEvenly 演示
│ ├── RunnerPage.ets # 跑酷游戏 + layoutWeight
│ └── LightChaseGame.ets # 光点追逐游戏
├── components/ # 可复用组件
│ └── CommonComponents.ets
└── model/ # 数据模型与算法
├── AppModel.ets
├── SampleData.ets
└── SpacedRepetition.ets
这种分层架构体现了良好的关注点分离原则:
- entryability:应用入口层,管理 Ability 生命周期与页面路由
- pages:页面层,每个页面是一个独立的
@Entry @Component,负责完整的页面 UI 与交互 - components:组件层,封装可复用的 UI 组件,供页面或其他组件调用
- model:数据层,定义数据结构、枚举、接口以及业务算法(如间隔重复引擎)
2.3 应用入口设计
应用的入口文件 EntryAbility.ets 采用 Stage 模型的生命周期管理方式。Stage 模型是 HarmonyOS NEXT 推荐的 Ability 框架,它将 Ability 分为 EntryAbility 和 ExtensionAbility,每个 Ability 拥有独立的窗口和生命周期。
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
const DOMAIN = 0x0000;
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(DOMAIN, 'EnglishApp', 'Ability onCreate');
}
onDestroy(): void {
hilog.info(DOMAIN, 'EnglishApp', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
hilog.info(DOMAIN, 'EnglishApp', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/RowSpaceEvenlyDemo', (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.');
});
}
onWindowStageDestroy(): void {
hilog.info(DOMAIN, 'EnglishApp', 'Ability onWindowStageDestroy');
}
onForeground(): void {
hilog.info(DOMAIN, 'EnglishApp', 'Ability onForeground');
}
onBackground(): void {
hilog.info(DOMAIN, 'EnglishApp', 'Ability onBackground');
}
}
其中最关键的便是 onWindowStageCreate 方法中的 windowStage.loadContent() 调用,它决定了应用启动后首屏加载的页面。通过更换参数 'pages/RowSpaceEvenlyDemo' 即可在同一应用内切换不同的演示页。
hilog 日志系统在此处扮演了关键角色:hilog.info(0x0000, TAG, 'message') 是鸿蒙原生的日志输出 API,相比 console.log,hilog 支持按域(domain)和标签(tag)过滤日志,在生产环境下的调试效率更高。
三、ArkUI 布局体系深度解析
ArkUI 的布局体系基于 Flexbox 弹性盒模型,核心容器为 Column(垂直排列)和 Row(水平排列)。理解主轴(Main Axis)与交叉轴(Cross Axis)是掌握布局的关键前提。
3.1 主轴与交叉轴
在 ArkUI 中,每个容器都有一个主轴和一个交叉轴:
| 容器 | 主轴方向 | 交叉轴方向 | justifyContent 作用 | alignItems 作用 |
|---|---|---|---|---|
| Column | 垂直(从上到下) | 水平(从左到右) | 垂直方向排列方式 | 水平方向对齐方式 |
| Row | 水平(从左到右) | 垂直(从上到下) | 水平方向排列方式 | 垂直方向对齐方式 |
项目的 build-profile.json5 确认了目标 SDK 版本为 6.1.1(API 24),这是 HarmonyOS NEXT 的重要版本,Stage 模型下的布局 API 成熟稳定。
3.2 Row + SpaceEvenly 均分布局(核心示例)
RowSpaceEvenlyDemo.ets 是本项目最新创建的页面,专门演示 Row + justifyContent(FlexAlign.SpaceEvenly) 的均分布局效果。这一布局技巧在实际项目中应用广泛,尤其适合顶部操作工具栏、底部导航栏等需要按钮均匀排列的场景。
3.2.1 布局效果说明
SpaceEvenly(均匀分布)的核心特点在于:第一个子组件前侧的间距、相邻子组件之间的间距、最后一个子组件后侧的间距完全相等。这与 SpaceBetween(首尾贴边)和 SpaceAround(两侧半间距)形成鲜明对比。
假设 Row 容器总宽度为 W,容器内有 N 个子组件,每个子组件的自然宽度为它的内容宽度,则:
- SpaceBetween:首尾子组件紧贴容器边缘,中间 N-1 个间距相等。每个间距 = (W - 所有子组件宽度之和) / (N - 1)
- SpaceAround:每个子组件两侧的间距相等,但首尾子组件的外侧间距仅为中间间距的一半。每个子组件两侧间距 = (W - 所有子组件宽度之和) / N,首尾外侧间距 = 此值的一半
- SpaceEvenly:所有间距完全相等。间距数 = N + 1(首、中间、尾一共 N+1 个间距),每个间距 = (W - 所有子组件宽度之和) / (N + 1)
3.2.2 完整代码实现
/**
* 鸿蒙原生 ArkTS 布局示例 — Row + justifyContent(SpaceEvenly)
* 功能:演示 Row 主轴(水平方向)均分布局
* 子组件在容器中均匀排列,两端的间距与中间间距相等
* 场景:顶部操作栏 / 底部导航栏 / 工具栏 / 菜单栏
* 核心技术:
* - Row 容器(主轴:水平方向)
* - justifyContent(FlexAlign.SpaceEvenly) — 子组件在主轴均匀分布
* - alignItems(VerticalAlign.Center) — 子组件在交叉轴(垂直)居中
*/
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = 'RowSpaceEvenlyDemo';
首先导入 hilog 日志工具。在鸿蒙开发中,@ohos.hilog 已被整合为 @kit.PerformanceAnalysisKit 下的 hilog,这是 API 24 版本的变化——所有 Kit 化的 API 都按照领域进行分组管理。
接下来定义三个子组件:
工具栏图标按钮(ToolbarItem):
@Component
struct ToolbarItem {
private icon: string = '';
private label: string = '';
private onClickAction: () => void = () => {};
build() {
Column() {
// 图标区:圆形背景 + Emoji
Text(this.icon)
.fontSize(28)
.width(52)
.height(52)
.textAlign(TextAlign.Center)
.backgroundColor('#f5f7fa')
.borderRadius(26)
.shadow({ radius: 3, color: '#15000000', offsetX: 0, offsetY: 2 })
// 文字标签
Text(this.label)
.fontSize(12)
.fontColor('#555555')
.margin({ top: 6 })
.lineHeight(16)
}
.alignItems(HorizontalAlign.Center)
.onClick(() => {
hilog.info(0x0000, TAG, `ToolbarItem clicked: ${this.label}`);
this.onClickAction();
})
}
}
设计要点:
- 使用
borderRadius(26)配合固定的width(52)/height(52)实现圆形背景,这是 ArkUI 中制作圆形的标准做法——圆角半径 = 宽度一半 shadow属性为图标添加阴影增强立体感,#15000000表示 RGBA 颜色模式下的半透明黑色阴影- 每个 ToolbarItem 内部使用 Column 纵向排列图标和文字,外部 Row 容器负责水平均分布局
竖直分隔线(ToolbarDivider):
@Component
struct ToolbarDivider {
build() {
Column()
.width(1)
.height(36)
.backgroundColor('#e0e0e0')
}
}
这里使用了一个宽度为 1px 的 Column 模拟竖线。ArkUI 的 Divider 组件默认是水平方向的,如果需要竖直分割线,用这种窄 Column 实现是最简洁的方案。
功能面板(FeaturePanel):
@Component
struct FeaturePanel {
private activeFeature: string = '';
private onClose: () => void = () => {};
build() {
Column() {
Row() {
Text(this.activeFeature)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#2d5f8a')
Blank() // 弹性占位
Text('✕')
.fontSize(18)
.fontColor('#999999')
.onClick(() => { this.onClose(); })
}
.width('100%')
.alignItems(VerticalAlign.Center)
Divider()
.height(1)
.width('100%')
.color('#e8e8e8')
.margin({ top: 10, bottom: 14 })
Text(this.getFeatureDescription())
.fontSize(14)
.fontColor('#666666')
.lineHeight(22)
}
.width('100%')
.padding(16)
.backgroundColor('#ffffff')
.borderRadius(12)
.shadow({ radius: 8, color: '#1a000000', offsetX: 0, offsetY: 4 })
}
private getFeatureDescription(): string {
const descriptions: Record<string, string> = {
'📁 文件': '点击此按钮可打开文件管理器...',
'✏️ 编辑': '进入编辑模式...',
'📤 分享': '将当前内容通过系统分享面板...',
'⭐ 收藏': '将当前内容添加到收藏夹...',
'⚙️ 设置': '打开应用设置页面...',
};
return descriptions[this.activeFeature] || '此功能待开发,敬请期待。';
}
}
设计要点:
Blank()组件是 ArkUI 中的弹性空白占位符,它会自动占据 Row 容器中的剩余空间,从而将关闭按钮推到右侧Record<string, string>是 ArkTS 中的索引签名类型,用于存储键值对映射- FeaturePanel 本身是可交互的,点击关闭按钮触发 onClose 回调,体现了 ArkTS 组件化设计中的回调传递模式
3.2.3 主页面核心实现
@Entry
@Component
struct RowSpaceEvenlyPage {
@State activeFeature: string = '';
@State isPanelVisible: boolean = false;
private readonly toolbarItems: ToolbarItemData[] = [
{ icon: '📁', label: '文件' },
{ icon: '✏️', label: '编辑' },
{ icon: '📤', label: '分享' },
{ icon: '⭐', label: '收藏' },
{ icon: '⚙️', label: '设置' },
];
build() {
Column() {
// ===== 区域1:页面标题区 =====
Column() {
Text('📐 Row + justifyContent(SpaceEvenly)')
.fontSize(20).fontWeight(FontWeight.Bold).fontColor('#ffffff')
Text('水平均分布局 · 工具栏按钮均匀排列(首尾间距 = 中间间距)')
.fontSize(12).fontColor('#cce0ff').margin({ top: 4 })
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding({ top: 20, bottom: 16, left: 20, right: 20 })
.backgroundColor('#2d5f8a')
// ===== 区域2:核心演示区 =====
Column() {
Text('📋 顶部操作工具栏(均分布局)')
.fontSize(15).fontWeight(FontWeight.Bold).fontColor('#1a1a2e')
.margin({ bottom: 14 })
// ★★★ 核心:Row + SpaceEvenly ★★★
Row() {
ForEach(this.toolbarItems, (item: ToolbarItemData, idx: number) => {
ToolbarItem({
icon: item.icon,
label: item.label,
onClickAction: () => { this.onToolbarItemClick(item.label); }
})
if (idx < this.toolbarItems.length - 1) {
ToolbarDivider()
}
})
}
.justifyContent(FlexAlign.SpaceEvenly) // ← 核心属性
.alignItems(VerticalAlign.Center) // ← 垂直居中
.width('100%')
.height(80)
.padding({ left: 4, right: 4 })
.backgroundColor('#ffffff')
.borderRadius(14)
.shadow({ radius: 6, color: '#1a000000', offsetX: 0, offsetY: 2 })
}
// ... 面板和说明区域
}
.width('100%')
.height('100%')
.backgroundColor('#eef2f7')
}
private onToolbarItemClick(label: string): void {
this.activeFeature = label;
this.isPanelVisible = true;
}
private closePanel(): void {
this.isPanelVisible = false;
this.activeFeature = '';
}
}
interface ToolbarItemData {
icon: string;
label: string;
}
SpaceEvenly 核心代码仅需两行链式调用:
.justifyContent(FlexAlign.SpaceEvenly) // 水平方向均匀分布
.alignItems(VerticalAlign.Center) // 垂直方向居中
justifyContent 控制主轴(Row 的主轴是水平方向)的排列方式,alignItems 控制交叉轴(Row 的交叉轴是垂直方向)的对齐方式。这两个属性是 ArkUI 弹性盒模型的核心,理解它们的区别是正确使用布局的关键。
ArkTS 链式调用说明:在 ArkUI 的声明式 UI 中,组件的属性设置通过链式调用
.属性名(参数)的方式完成。这种语法源自 TypeScript 的 Builder 模式,每个属性方法返回组件实例本身,因此可以无限串联。注意与 HTML/CSS 的属性写法的区别——在 ArkUI 中,所有属性都是方法调用,而非键值对赋值。
3.3 Column + FlexAlign.Start 顶部起始布局
Index.ets 页面演示了 Column 容器的 justifyContent(FlexAlign.Start) 布局。这一布局模式适用于信息流列表、消息中心、设置页等需要内容从顶部开始紧凑排列的场景。
3.3.1 核心实现逻辑
@Entry
@Component
struct ColumnStartPage {
private readonly infoList: InfoItem[] = [
{ title: '📌 系统通知', desc: '您的鸿蒙应用已通过安全检测...' },
{ title: '📊 数据报告', desc: '本周活跃用户较上周增长 12%...' },
{ title: '⚙️ 版本更新', desc: 'v3.2.0 发布:新增 ColumnStart 布局组件...' },
{ title: '🎯 优化建议', desc: '检测到 3 处可优化项...' },
];
build() {
Column() {
// 标题区(固定高度)
// 核心演示区
Column() {
// 信息卡片通过 ForEach 渲染
ForEach(this.infoList, (item: InfoItem, idx: number) => {
InfoCard({ item: item, index: idx })
}, (item: InfoItem) => item.title)
}
.alignItems(HorizontalAlign.Start)
.justifyContent(FlexAlign.Start) // ★ 核心:从顶部开始排列
.width('100%')
.height(0)
.layoutWeight(1) // 占满剩余空间
.backgroundColor('#ffffff')
}
.width('100%')
.height('100%')
}
}
3.3.2 layoutWeight 与 justifyContent 的协同
此处值得关注的是 layoutWeight(1) 与 justifyContent(FlexAlign.Start) 的组合效果:
layoutWeight(1)使 Column 占满父容器中未被固定高度组件占据的剩余空间justifyContent(FlexAlign.Start)确保柱内所有子组件从顶部开始排列,底部留空
这种组合在需要"内容紧凑于顶部,底部留空"的场景中非常实用,例如空白状态页、信息流页面加载更多时的底部留白。
layoutWeight 的底层逻辑:当 Column 设置了 height('100%') 且包含多个子组件时,固定高度的子组件(通过 .height() 指定)优先占据空间,剩余空间按照各子组件的 layoutWeight 值的比例分配。如果某个子组件未设置 layoutWeight,它的大小由内容撑开。如果一个 Column 中的所有子组件都未设置 layoutWeight,则每个子组件按内容自然高度排列,justifyContent 决定它们在主轴上的分布方式。
3.4 JustifyContent 五种取值对比
ArkUI 的 FlexAlign 枚举提供了五种主轴排列方式,它们在 Row 和 Column 中的表现对应如下:
| FlexAlign 取值 | Row(水平方向)效果 | Column(垂直方向)效果 |
|---|---|---|
| Start | 子组件靠左排列 | 子组件靠顶排列 |
| Center | 子组件水平居中 | 子组件垂直居中 |
| End | 子组件靠右排列 | 子组件靠底排列 |
| SpaceBetween | 首尾贴边,中间等距 | 首尾贴顶底,中间等距 |
| SpaceAround | 两侧半间距,中间等距 | 上下半间距,中间等距 |
| SpaceEvenly | 所有间距完全相等 | 所有间距完全相等 |
这些取值的间距计算方式可以用下面的公式来记忆(N 为子组件数量):
- SpaceBetween:间距数 = N - 1,首尾无外侧间距
- SpaceAround:间距数 = 2N,每个子组件两侧间距相等但首尾外侧 = 内侧的一半
- SpaceEvenly:间距数 = N + 1,包含首尾外侧
四、弹性布局与游戏开发实战
RunnerPage.ets 是项目中布局技术最为复杂的页面,它是 Column + layoutWeight 弹性布局的经典案例,同时也融合了 Canvas 2D 图形绘制、物理引擎模拟、帧循环动画等技术。
4.1 弹性布局架构
Column() { // 全屏容器
┌──────────────────────────────┐
│ 固定顶部(height: 50) │ ← 固定高度
│ 🏃 单键跑酷 得分: 12 │
├──────────────────────────────┤
│ ★ layoutWeight(1.0) │ ← 弹性区 A(50%)
│ 游戏主场景(Canvas) │
├──────────────────────────────┤
│ ★ layoutWeight(0.3) │ ← 弹性区 B(15%)
│ 游戏状态信息区 │
├──────────────────────────────┤
│ ★ layoutWeight(0.7) │ ← 弹性区 C(35%)
│ 【🦘 跳跃!】按钮 │
└──────────────────────────────┘
}
弹性区的高度计算公式为:
弹性区高度 = (Column总高度 - 固定区高度之和) × 该区layoutWeight / 所有弹性区layoutWeight之和
在此示例中:
- Column 总高度 = 屏幕高度(100%)
- 固定区高度 = 50px(顶部状态栏)+ 底部说明面板(内容撑高)
- 三个弹性区 layoutWeight 之和 = 1.0 + 0.3 + 0.7 = 2.0
- 弹性区 A 的高度占比 = 1.0 / 2.0 = 50%
- 弹性区 B 的高度占比 = 0.3 / 2.0 = 15%
- 弹性区 C 的高度占比 = 0.7 / 2.0 = 35%
使用 layoutWeight 时必须注意:父容器(Column)必须设置 .height('100%') 或固定的高度值,否则 layoutWeight 无法计算剩余空间,会导致布局异常。这是 ArkTS 开发中常见的踩坑点。
4.2 Canvas 游戏场景实现
RunnerPage 使用 Canvas 组件 + CanvasRenderingContext2D 实现游戏画面的实时绘制。这是一种轻量级的游戏渲染方案,不需要引入额外的游戏引擎。
4.2.1 Canvas 初始化与帧循环
private ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D();
// 在 build 中使用 Canvas
Canvas(this.ctx)
.width('100%')
.height('100%')
.onReady(() => {
this.canvasW = this.ctx.width;
this.canvasH = this.ctx.height;
this.drawScene(); // 绘制首帧
})
// 游戏循环驱动
this.timerId = setInterval(() => {
this.gameLoop();
}, 24); // 约 42 FPS
关键要点:
CanvasRenderingContext2D必须通过构造函数传入 Canvas 组件,不能通过内部getContext获取onReady回调是 Canvas 组件就绪的信号,此时才能获取实际的 canvas 宽高- 用
setInterval实现固定帧率的游戏循环,每帧约 24ms ≈ 42 FPS
4.2.2 物理引擎模拟
游戏的物理系统模拟了重力、跳跃和地面碰撞:
// 物理常量
const GRAVITY: number = 0.55; // 重力加速度
const JUMP_VEL: number = -9.0; // 跳跃初速度(负值=向上)
// 物理更新(每帧执行)
private gameLoop(): void {
// 1. 重力作用
this.playerVY = this.playerVY + GRAVITY;
this.playerY = this.playerY + this.playerVY;
// 2. 地面碰撞检测
if (this.playerY >= 0) {
this.playerY = 0;
this.playerVY = 0;
}
// 3. 障碍物移动(速度随分数递增)
this.curSpeed = BASE_SPEED + this.score * SPEED_STEP;
// ...
}
物理运动公式:
- 速度:
v(t) = v(0) + g × t - 位移:
y(t) = y(0) + v(0) × t + 0.5 × g × t² - 在离散的帧循环中,近似为:
v += g; y += v;
4.2.3 Canvas 绘制管线
drawScene 方法实现了完整的游戏画面绘制管线,按照从后到前的顺序绘制各层:
private drawScene(): void {
// 1. 清空画布
ctx.clearRect(0, 0, w, h);
// 2. 绘制天空渐变背景
const grad = ctx.createLinearGradient(0, 0, 0, h);
grad.addColorStop(0, '#87CEEB');
grad.addColorStop(0.7, '#E0F7FA');
grad.addColorStop(1, '#A5D6A7');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, w, h);
// 3. 绘制地面
ctx.fillStyle = '#5D4037';
ctx.fillRect(0, groundY, w, h - groundY);
// 4. 绘制障碍物
for (let i = 0; i < this.obstacles.length; i++) {
// 障碍物主体、边框、X装饰...
}
// 5. 绘制角色
// 身体(圆角矩形)、眼睛、瞳孔、微笑...
// 6. 绘制 UI 叠加层(游戏结束/READY状态)
}
绘制层级顺序是 Canvas 渲染的核心概念——后绘制的图形覆盖在先绘制的图形之上。Pipeline 的绘制顺序(从底到顶)为:天空背景 → 地面 → 草地边沿 → 地面纹理 → 障碍物 → 角色 → UI 覆盖层。
4.3 游戏交互设计
RunnerPage 采用"单键操作"设计理念,整个游戏仅需一个按钮即可完成所有交互:
private doJump(): void {
if (this.gameState === GameState.READY || this.gameState === GameState.OVER) {
this.startGame(); // 开始/重新开始
return;
}
if (this.gameState === GameState.PLAYING) {
if (this.playerY >= -2) { // 只有在接近地面时才能跳跃
this.playerVY = JUMP_VEL;
}
}
}
跳跃限制条件 this.playerY >= -2 防止了"空中二段跳",这是平台跳跃游戏中的常见设计,避免玩家重复跳跃影响游戏体验。
4.4 光点追逐游戏(LightChaseGame)
LightChaseGame.ets 是本项目中的第二个完整游戏,与 RunnerPage 不同,它采用了 Stack 布局 + @State 响应式状态管理的方式,完全不使用 Canvas,完全依靠 ArkUI 原生组件实现游戏逻辑。
游戏核心机制:
- 光点随机出现在屏幕上,玩家需要在限定时间内点击光点得分
- 难度动态递增:得分越高,光点越小、移动越快
- 连击系统:连续点击光点获得连击奖励提示
- 30 秒限时,结束后根据分数评级
// 难度递增逻辑
const difficulty = Math.min(this.score / 20, 1);
this.dotSize = 48 - difficulty * 20; // 48 → 28 逐渐缩小
const hue = (this.score * 30) % 360;
this.dotColor = `hsl(${hue}, 100%, 65%)`; // 颜色随分数变化
const interval = Math.max(MIN_INTERVAL, BASE_INTERVAL - this.score * 30);
// 移动间隔从 1200ms 逐渐缩短至 300ms
hsl() 颜色函数是此处的亮点——通过改变色相(Hue)值,光点颜色随着分数增加而循环变化,从黄色渐变到红、蓝、绿等各种颜色。这是一种无需额外资源即可实现视觉多样性的技巧。
@State 的响应式原理:在 LightChase 中,所有需要触发 UI 重新渲染的变量都标注了 @State,例如 score、timeLeft、dotX、dotY。当这些变量的值发生改变时,ArkUI 框架会自动重新调用 build() 方法生成新的 UI 树,并通过 Diff 算法高效地更新实际的渲染节点。而对于不需要触发 UI 重绘的内部状态(如 timerId、moveTimerId),则不使用 @State,避免不必要的性能开销。
五、通用组件设计模式
CommonComponents.ets 是项目的组件库,包含了四个可复用的 UI 组件。在 ArkTS 中,组件的复用主要有两种方式:@Component 装饰的结构体组件和 @Builder 装饰的构建函数。
5.1 卡片容器组件(Card)
@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 })
}
}
@BuilderParam 插槽机制:ArkTS 的自定义组件不支持 Vue 或 React 中的默认 children / slot 机制,因此要实现"容器组件"模式,必须使用 @BuilderParam 装饰器。父组件在使用 Card 时,通过 content 属性传入一个 @Builder 构建函数:
Card({ cardPadding: 20 }) {
Column() {
Text('自定义卡片内容')
// 任意子组件...
}
}
@Prop 装饰器:用于从父组件接收单向数据。当父组件的属性值发生变化时,子组件会自动更新。@Prop 装饰的变量必须有默认值,且是单向绑定(父 → 子)。
5.2 Canvas 圆形进度条(ProgressRing)
ProgressRing 组件使用两个 Canvas 叠加在同一位置,分别绘制背景圆环和进度圆环:
@Component
export struct ProgressRing {
@Prop ringProgress: number = 0; // 进度值(0-100)
@Prop ringSize: number = 80;
@Prop ringColor: string = '#3a7bd5';
@Prop ringBgColor: string = '#e8ecf0';
build() {
Column() {
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)
if (this.ringLabel.length > 0) {
Text(this.ringLabel).fontSize(10).fontColor('#888')
}
}
}
}
}
drawRing(ctx: CanvasRenderingContext2D, value: number, isBg: boolean): void {
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();
}
}
Stack 布局的层叠特性:Stack 容器允许子组件在 Z 轴上重叠排列,后声明的子组件在前。这里利用 Stack 将背景圆环、进度圆环、中央文字三层叠在同一位置,实现了复合视觉组件。
两个独立的 Canvas 上下文:由于 ArkUI 的 Canvas 不支持在同一个上下文中同时维护两个独立的绘制状态(drawRing 会覆盖之前的绘制结果),因此需要创建两个 CanvasRenderingContext2D 实例,分别用于背景和进度。
5.3 模块入口卡片(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')
}
.width('30%')
.aspectRatio(1.0) // 1:1 正方形比例
.justifyContent(FlexAlign.Center)
.backgroundColor((this.entryColor + '18')) // 添加 18 十六进制透明度
.borderRadius(16)
.onClick(() => { this.onClickAction(); })
}
}
aspectRatio(1.0):这是一个非常实用的属性,强制组件的宽高比为 1:1。在网格布局中,结合 .width('30%') 可以保证所有卡片无论内容多少都保持相同的正方形比例,实现整齐的网格排列。
颜色透明度技巧:this.entryColor + '18' 通过字符串拼接的方式在颜色十六进制值后追加透明度通道(18 十六进制 ≈ 9% 透明度),这是一种轻量级实现半透明背景的方式,无需调用 Color.hexToRgba 等转换方法。
5.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')
}
}
showBack 条件渲染:通过 @Prop showBack 控制返回按钮的显示与隐藏,这是一个典型的组件配置模式。父组件通过设置 showBack: true 开启返回功能,并传入 onBack 回调处理返回事件。
六、数据模型与算法实现
6.1 数据结构定义
AppModel.ets 使用 ArkTS 的 interface 和 enum 定义了完整的数据模型体系:
/** 难度等级 */
export enum Difficulty {
EASY = 1,
MEDIUM = 2,
HARD = 3,
}
/** 单词条目 */
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
}
此外还包括听力材料(ListeningMaterial)、阅读文章(ReadingArticle)、语法练习(GrammarExercise)等数据模型,覆盖了英语学习应用所需的全部数据结构。
6.2 间隔重复算法(SM-2)
SpacedRepetition.ets 实现了经典的 SM-2 间隔重复算法,这是 SuperMemo 2 的核心算法,广泛应用于 Anki、Mnemosyne 等记忆类应用。
算法核心参数:
- 易度系数(EF, Ease Factor):初始值 2.5,范围 [1.3, +∞),反映单词的"易学程度"
- 连续正确次数(Repetition):连续正确回答的次数
- 回答质量(Quality):0~5 的评分,0 = 完全忘记,5 = 完全正确
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,
previousInterval: number,
repetition: number,
previousEf: number = SpacedRepetitionEngine.DEFAULT_EF,
): ReviewResult {
// 质量 < 3:回答不合格,重置进度
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);
// 计算间隔:首次1天,二次3天,之后按 EF 指数增长
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),
};
}
/** EF 更新公式:EF' = EF + (0.1 - (5-Q) × (0.08 + (5-Q) × 0.02)) */
private static updateEf(oldEf: number, quality: number): number {
const newEf = oldEf + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02));
return Math.max(newEf, SpacedRepetitionEngine.MIN_EF);
}
}
SM-2 算法的关键特点:
-
针对单个单词灵活调整:每个单词独立维护自己的 EF 和 Repetition 值,算法根据每次复习的实际表现动态调整。
-
指数增长的复习间隔:当用户连续正确回答时,复习间隔会从 1 天 → 3 天 → 按 EF 倍数增长。例如 EF=2.5 时,间隔为 1→3→8→19→48→120… 天,呈指数级增长,这正是"间隔重复"名称的由来。
-
失分即重置:一旦用户答错(quality < 3),间隔立即重置为 1 天,repetition 归零,这体现了"大脑遗忘曲线"的客观规律。
-
EF 双向调整:EF 值不是固定的,它会根据每次回答的质量进行调整。答得好 EF 增加(更快进入长间隔),答得差 EF 降低(缩短间隔,增加复习频率)。
掌握度映射:
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'; // 红色 - 新词/陌生
}
这种颜色映射让学习进度一目了然,无需阅读文字说明即可快速识别当前掌握状态。
6.3 示例数据
SampleData.ets 提供了 30 个基础词汇的完整数据以及阅读文章和语法练习题。每个单词条目包含 9 个字段,覆盖了英语学习所需的信息维度:
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 个更多单词
];
单词分类体系:30 个词汇覆盖了"基础词汇"、“核心词汇”、“进阶词汇”、"学术词汇"四个类别,难度从 EASY 到 HARD 三个等级,为后续的排序学习算法提供了丰富的维度。
七、工程化最佳实践
7.1 项目构建设置
项目根目录的 build-profile.json5 定义了关键 SDK 版本和目标设备配置:
{
"app": {
"products": [
{
"name": "default",
"targetSdkVersion": "6.1.1(24)",
"compatibleSdkVersion": "6.1.1(24)",
"runtimeOS": "HarmonyOS",
"buildOption": {
"strictMode": {
"caseSensitiveCheck": true,
"useNormalizedOHMUrl": true
}
}
}
]
}
}
targetSdkVersion 与 compatibleSdkVersion:前者表示应用开发时依赖的最新 SDK 特性,后者表示应用可以兼容的最低 SDK 版本。两者都设为 6.1.1(24) 意味着应用仅在 HarmonyOS NEXT(API 24)上运行。
7.2 hilog 日志规范
项目中统一使用 hilog 进行日志输出,而非 console.log。hilog 是鸿蒙系统的日志基础设施,支持日志分级和按域过滤:
const TAG = 'RowSpaceEvenlyDemo'; // 标签常量,按模块命名
// 日志输出格式:hilog.level(domain, tag, message)
hilog.info(0x0000, TAG, 'Feature selected: ' + label);
// 或使用格式化字符串:
hilog.info(0x0000, TAG, 'canvas ready: %d x %d', this.canvasW, this.canvasH);
日志级别:hilog 支持 DEBUG、INFO、WARN、ERROR 四个级别,strictMode 开启时,WARN 及以上级别的日志会触发编译警告。
7.3 页面路由管理
module.json5 配置了应用的页面路由:
{
"module": {
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["action.system.home"]
}
]
}
]
}
}
"pages": "$profile:main_pages" 引用了一个资源配置文件,该文件位于 resources/base/profile/main_pages.json 中,定义了应用中所有页面的路径映射。每个页面路径对应 @Entry 装饰的组件,例如 'pages/RowSpaceEvenlyDemo' 对应 RowSpaceEvenlyDemo.ets 中的 RowSpaceEvenlyPage 结构体。
7.4 生命周期管理
ArkTS 组件提供了完整的生命周期回调:
aboutToAppear(): void {
// 组件即将显示时调用
// 适合初始化数据、启动定时器
}
aboutToDisappear(): void {
// 组件即将销毁时调用
// 必须在此清理定时器、释放资源
if (this.timerId >= 0) {
clearInterval(this.timerId);
this.timerId = -1;
}
}
资源清理的重要性:在 LightChaseGame.ets 中,aboutToAppear 中创建了背景色渐变动画定时器(setInterval),aboutToDisappear 中必须清理 timerId 和 moveTimerId 两个定时器。否则页面跳转后,定时器仍在后台运行,会导致内存泄漏和逻辑异常。
RunnerPage 直接将 setInterval 的返回值(timerId)存储在普通成员变量中(非 @State),因为定时器句柄不需要驱动 UI 重绘。这是一个重要的性能优化策略——尽量减少 @State 变量的数量,避免不必要的 UI 更新。
7.5 @State 与性能优化
在 ArkTS 中,@State 装饰器的使用需要遵循以下原则:
- 最小化 @State 范围:只有直接影响 UI 展示的变量才标记为
@State - 避免大对象作为 @State:如果一个大对象只有个别字段影响 UI,考虑将其拆分为多个独立的
@State - 不变的属性用
private:仅用于子组件传入的数据,使用private而非@State
// 正确做法
@State private score: number = 0; // 驱动显示
@State private timeLeft: number = 30; // 驱动显示
private timerId: number = -1; // 不驱动 UI,不用 @State
private obstacles: Obstacle[] = []; // 内部状态,由 Canvas 重绘
// 错误做法
@State private timerId: number = -1; // 不需要,会导致多余的 build() 调用
八、ArkTS 声明式 UI 与传统命令式 UI 的对比
8.1 数据流方向
传统命令式(Java/XML):业务逻辑中通过类似 findViewById(id) 获取控件实例,然后调用 setText()、setOnClickListener() 等方法手动更新 UI。
ArkTS 声明式:数据与 UI 通过 @State / @Prop / @Link 自动绑定。开发者只需修改数据,框架自动完成 UI 更新。数据流是单向的:Model → View。
8.2 代码组织方式
传统方式:布局文件(XML)与逻辑代码(Java)分属不同文件,布局与逻辑的关联通过 ID 隐式绑定,大型项目中维护困难。
ArkTS 方式:UI 结构与业务逻辑在同一个文件中,通过 build() 方法声明 UI 结构,通过装饰器定义数据绑定关系。组件的"UI + 逻辑"完全内聚,不存在跨文件隐式依赖。
8.3 样式管理
传统方式:通常使用独立的样式文件(如 Android 的 styles.xml)或在布局文件中直接写内联样式,缺乏类型检查。
ArkTS 方式:所有样式属性通过链式方法调用设置,具有完整的类型安全。IDE 可以提供自动补全、参数类型校验和重构支持。
九、多页面切换与导航
当前项目中,EntryAbility.ets 的 windowStage.loadContent() 决定了首页的加载。要实现多页面间的导航,可以通过以下方式扩展:
// 方式一:使用页面路由
import { router } from '@kit.ArkUI';
router.pushUrl({
url: 'pages/LightChaseGame',
params: { }
});
// 方式二:条件渲染(适用于页面较少的场景)
build() {
Column() {
if (this.currentPage === 'home') {
ColumnStartPage()
} else if (this.currentPage === 'toolbar') {
RowSpaceEvenlyPage()
}
// ...
}
}
router.pushUrl 是鸿蒙推荐的页面路由方式,支持 URL 跳转、参数传递和页面栈管理(router.back() 返回上一页)。条件渲染方式适合页面数量较少的场景,无需配置路由表,但所有页面的代码都会被打包到同一个文件中。
十、总结与展望
本文以 HarmonyOS NEXT 6.1.1(API 24)为开发平台,通过一个完整的英语学习应用实例,深入剖析了鸿蒙 ArkTS 开发的五大核心领域:
10.1 布局体系
ArkUI 的弹性盒模型(Flexbox)通过 Column 和 Row 两个容器,配合 justifyContent 和 alignItems 属性,能够实现几乎所有常见的 UI 布局。layoutWeight 弹性权重机制为自适应屏幕尺寸提供了优雅的解决方案。SpaceEvenly 均分布局在工具栏场景中的表现尤为突出——仅需一行代码即可实现精确等距排列,相比传统布局方案(手动计算间距或使用嵌套 LinearLayout)大幅降低了复杂度。
10.2 组件化开发
通过 @Component 装饰器定义可复用组件,配合 @Prop、@BuilderParam 等装饰器实现灵活的数据传递和内容注入。这种组件化模式天然支持单一职责原则,每个组件只关注自己的功能和视图,通过清晰的接口与外部交互。
10.3 Canvas 游戏编程
ArkUI 的 Canvas 组件和 CanvasRenderingContext2D 提供了 2D 图形绘制能力,足以支撑轻量级游戏开发。从物理引擎模拟到帧循环动画,从碰撞检测到图形绘制管线,全部使用纯 ArkTS 代码实现,无需引入任何第三方游戏引擎。
10.4 数据模型与算法
从 interface 数据模型定义到 SM-2 间隔重复算法的实现,展示了在 ArkTS 中处理复杂业务逻辑的能力。数据类型安全、模块化设计、可测试性等工程原则在这里得到了充分的体现。
10.5 工程化实践
hilog 日志管理、生命周期资源清理、@State 最小化、Stage 模型配置等,构成了鸿蒙应用开发的工程化基础。这些看似细节的实践,恰恰是保障应用长期可维护性的关键。
随着 HarmonyOS 生态的持续完善,ArkTS 作为鸿蒙原生开发的首选语言,其类型系统、声明式 UI 和丰富的 API 能力将越来越强大。希望本文能够帮助开发者快速上手鸿蒙原生开发,掌握 ArkUI 布局体系的核心原理,在实际项目中构建出优雅、高效、可维护的鸿蒙应用。
从 Column + Start 的顶部排列,到 Row + SpaceEvenly 的均分布局,再到 layoutWeight 的弹性自适应——ArkUI 的布局哲学始终围绕着"用最简单的代码,实现最灵活的界面"。这也是鸿蒙原生开发一直追求的核心理念:让开发者专注于创造价值,而不是与布局框架做斗争。
更多推荐



所有评论(0)