HarmonyOS ArkTS API 24ArkUI自定义组件生命周期全流程剖析,从aboutToAppear到aboutToDisappear逐阶段梳理执行时机,理清状态变更与生命周期回调的先后关
一、技术前言
在运动健身与健康管理数字化浪潮中,运动训练正经历从"健身房打卡"到"全场景智能陪练"的深刻变革。从晨间慢跑心率监控到 HIIT 爆发冲刺,从力量训练周期化编排到睡眠恢复量化评估,每一位运动者都需要匹配个性化的训练计划、精准的数据统计和及时的运动提醒。传统运动健康应用面临三大挑战:训练提醒铃声千篇一律导致用户疲劳、双语跟练场景字幕无法灵活切换导致课程割裂、训练数据可视化维度单一导致进步趋势模糊。
HarmonyOS ArkUI 框架为这些挑战提供了系统级解决方案。ArkUI 的声明式 UI 范式通过 @Component 封装可复用组件、@State 管理响应式状态、@Builder 拆分复杂 UI 结构,天然适合"训练-数据-提醒"三层架构。@Observed 装饰器让数据模型字段级变化被 UI 感知,实现"状态更新即视图刷新"的流畅体验。ForEach 列表渲染配合键值生成器,保证了长列表的高效更新与精准复用。
本平台深度融合 HarmonyOS 的三大前沿特性。Notification Kit 实现沙箱自定义铃声通知——通过 buildWavBytes 函数生成正弦波 PCM 音频(44 字节 WAV 头 + 16bit 单声道数据),写入 EL1 沙箱 filesDir 目录后,以 'uri::' + fileUri.getUriFromPath(沙箱路径) 拼接为 NotificationRequest.sound 字段值,突破传统 rawfile 资源限制,实现"铃声坊"式音频生成器体验。Speech Kit 的 AI 字幕组件引入了 sourceLanguage、targetLanguage、fontSize、fontColor 四个新字段,支持中英双向翻译、中英双语对照、四档字号调节和五色字体预设,配合 writeAudio 640 字节 PCM 块写入实现实时语音转字幕。Canvas 绘制 实现今日完成率进度环(drawRing 方法:背景环 + 进度弧 + 中心百分比,呼吸联动弧长波动)和近 12 周训练时长折线图(drawLine 方法:网格 + 渐变填充 + 折线 + 数据点,呼吸联动末点半径放大),让运动数据以可视化方式直观呈现。
二、整体架构流程图
架构以 Page1284 为根组件,使用 Stack 容器层叠:底层 Column 纵向排列头部品牌区、分割线、Scroll 内容区和底部 Tab 栏,顶层是三个独立弹窗(panelAdd/panelEdit/panelDel 各自条件渲染)。内容区通过 currentTab 状态变量在 6 个 @Builder 方法间切换,三大特性分散在训练(Canvas 进度环与折线图)、铃音(Notification Kit 沙箱铃声)和字幕(Speech Kit AI 字幕)三个 Tab 上,状态变量统一声明在组件顶层实现跨 Tab 共享。呼吸动画定时器每秒翻转 breath 布尔值,联动进度环弧长、折线图末点半径、柱状图高度、各处圆点透明度,形成全局统一的"呼吸"视觉节拍。
三、色彩体系设计
3.1 ColorPalette 接口定义
interface ColorPalette {
bg: string; // 页面底色·夜跑黑
card: string; // 卡片底色·炭咖
dark: string; // 次级容器底色
title: string; // 主标题·暖白
sub: string; // 次级文字·燕麦
text3: string; // 弱化文字·灰咖
orange: string; // 主题色·活力橙
orangeD: string; // 主题色深·深橙
green: string; // 辅色·青柠绿
blue: string; // 辅色·湖蓝
red: string; // 辅色·警示红
line: string; // 分割线
tabOn: string; // Tab 激活色
mask: string; // 弹窗遮罩
greenFade: string; // 青柠渐隐(折线图渐变终点)
gradA: string; // 渐变起点·深橙棕
gradB: string; // 渐变终点·活力橙
onMain: string; // 主色按钮上的深字
}
3.2 COLORS 常量逐色分析
const COLORS: ColorPalette = {
bg: '#12100E', // 夜跑黑,深夜操场般的沉浸暗光底色
card: '#1E1B18', // 炭咖卡片底色,比背景亮一档的暖调
dark: '#262220', // 次级容器底色,统计格与标签底
title: '#F5EFE6', // 暖白标题,暗光高对比可读
sub: '#C8BBA8', // 燕麦副标题,层次柔和不刺眼
text3: '#8A7F70', // 灰咖弱文本,辅助信息不抢视觉
orange: '#FF7A3C', // 活力橙主色,渐变横幅与按钮主色
orangeD: '#E05E20', // 深橙,渐变起点与强调元素
green: '#9CD04F', // 青柠绿辅色,已完成与通过状态
blue: '#4FA3E3', // 湖蓝,信息提示与字幕语言标识
red: '#E85B5B', // 警示红,未授权与删除操作
line: '#332E29', // 分割线,低对比不干扰
tabOn: '#FF7A3C', // Tab 选中色为活力橙(与主色统一)
mask: 'rgba(0,0,0,0.6)', // 半透黑遮罩
greenFade: 'rgba(156,208,79,0.06)', // 折线图渐变终点近乎透明
gradA: '#3B2210', // 深橙棕,身份大卡渐变起点
gradB: '#5A2E0C', // 深活力橙,身份大卡渐变终点
onMain: '#241305' // 深棕字,活力橙按钮上的高对比文字
};
色彩体系以"夜跑黑 + 活力橙 + 青柠绿"为核心三色。夜跑黑构建沉浸式的深色运动场景基底,活力橙代表运动能量与行动力,青柠绿象征健康达成与完成状态。值得注意的是 Tab 选中色使用 orange(活力橙)而非另设独立色,这是因为橙色在深黑背景上具有极高的视觉穿透力,用户定位当前 Tab 更迅速。身份渐变大卡使用 linearGradient 从 gradA(深橙棕)到 gradB(深活力橙)的 135° 渐变,模拟运动后皮肤充血的暖色调,搭配 onMain(深棕字)保证渐变背景上的文字可读性。折线图使用 green(青柠绿)作为线条主色,配合 greenFade(近乎透明的青柠)作为渐变填充终点,营造出数据从高空逐渐消散的视觉层次。
四、Tab 元数据与常量定义
4.1 底部导航 Tab 定义
interface TabMeta {
icon: string; // Tab 图标
label: string; // Tab 标签
}
const TAB_LIST: TabMeta[] = [
{ icon: '💪', label: '训练' },
{ icon: '🏋️', label: '课程' },
{ icon: '⏰', label: '提醒' },
{ icon: '🔔', label: '铃音' },
{ icon: '🗣', label: '字幕' },
{ icon: '👤', label: '我的' }
];
6 个 Tab 从训练执行到个人中心覆盖运动健康管理全流程:训练是数据概览与今日课表首页,课程提供精选课程入口,提醒管理训练节奏通知,铃音是 Notification Kit 沙箱铃声的核心演示区,字幕提供 Speech Kit AI 字幕设置面板,我的展示运动者身份与月度数据。
4.2 AI 字幕语言选项
const SRC_LANGS: LangOption[] = [
{ code: 'zh', name: '中文' },
{ code: 'en', name: '英文' }
];
const TGT_LANGS_EN: LangOption[] = [
{ code: 'zh', name: '中文' },
{ code: 'en', name: '英文' },
{ code: 'zh-en', name: '中英双语' }
];
源语言仅中文和英文两选。中文源时目标语言锁定 zh(原文直显不翻译),英文源时目标语言有三选:中文翻译、英文原文、中英双语对照。switchSourceLang 方法在切换源语言时自动联动目标语言——中文源锁定 zh,英文源默认切双语 zh-en。
4.3 字号与颜色预设
const SIZE_OPTIONS: SizeOption[] = [
{ size: AICaptionFontSize.SMALL, name: '小号' },
{ size: AICaptionFontSize.NORMAL, name: '标准' },
{ size: AICaptionFontSize.BIG, name: '大号' },
{ size: AICaptionFontSize.LARGE, name: '超大' }
];
const CAPTION_FONT_COLORS: string[] = ['#FFFFFF', '#FFE9B0', '#9CE8B5', '#9CD0FF', '#FFB3C1'];
字号使用 AICaptionFontSize 枚举而非数字,四档从小号到超大。颜色预设五色:经典白、暖黄、薄荷绿、天空蓝、樱花粉,覆盖不同运动场景的字幕视觉需求——晨跑用暖黄提神、冥想用樱花粉放松、跟练用经典白保证清晰度。
4.4 折线图与月度数据
const LINE_DATA: number[] = [180, 165, 210, 240, 195, 260, 230, 205, 250, 275, 220, 245];
const LINE_LABELS: string[] = ['W1', 'W2', 'W3', 'W4', 'W5', 'W6', 'W7', 'W8', 'W9', 'W10', 'W11', 'W12'];
const MONTH_CAL: number[] = [860, 920, 1100, 1020, 1240, 1180];
const MONTH_MAX: number = 1400;
近 12 周训练时长折线数据从 180 分钟波动上升到 245 分钟,整体呈增长趋势,反映运动者训练量稳步提升。月度卡路里消耗从 3 月的 860 千卡增长到 8 月的 1180 千卡,峰值出现在 7 月的 1240 千卡,MONTH_MAX 作为柱高归一化基准确保最高柱不超过 78px。
五、工具函数
5.1 正弦波 WAV 音频生成
function buildWavBytes(freq: number, durationMs: number): ArrayBuffer {
const sampleRate = 44100;
const numSamples = Math.floor(sampleRate * durationMs / 1000);
const dataSize = numSamples * 2;
const buf = new ArrayBuffer(44 + dataSize);
const view = new DataView(buf);
// RIFF/WAVE/fmt 头写入
// PCM 单声道 16bit 数据写入(含起音包络与自然衰减)
return buf;
}
这是 Notification Kit 沙箱铃声特性的核心前置函数。它生成标准 WAV 格式音频字节:44 字节 RIFF/WAVE 头(含 PCM 编码标记、采样率 44100Hz、单声道、16bit 位深)+ 正弦波 PCM 数据。值得注意的是音频数据加入了起音包络(前 20ms 渐入)和自然衰减(按时长线性衰减),避免纯正弦波的突兀感,让生成的铃声更悦耳。wavSizeText 辅助函数则将字节量转为 KB 展示文本,用于铃声库列表的文件大小字段。
5.2 训练状态与课程难度配色
function statusColor(status: string): string {
if (status === '已完成') { return COLORS.green; }
if (status === '进行中') { return COLORS.orange; }
return COLORS.text3;
}
function levelColor(level: string): string {
if (level === '入门') { return COLORS.green; }
if (level === '进阶') { return COLORS.orange; }
return COLORS.red;
}
训练状态三色区分:已完成青柠绿(达成)、进行中活力橙(正在运动)、待开始灰咖(尚未启动)。课程难度三色区分:入门青柠绿(友好)、进阶活力橙(过渡)、高级警示红(高难度),与整体色彩语义一致——绿色代表安全与完成,橙色代表活跃与中等,红色代表挑战与警示。
5.3 语言码与环比配色
function langName(code: string): string {
if (code === 'zh') { return '中文'; }
if (code === 'en') { return '英文'; }
if (code === 'zh-en') { return '中英双语'; }
return code;
}
function deltaColor(delta: string): string {
if (delta.indexOf('+') === 0) { return COLORS.green; }
if (delta.indexOf('-') === 0) { return COLORS.red; }
return COLORS.text3;
}
langName 将语言码转为中文展示名,用于字幕场景卡的语言组合标签。deltaColor 根据环比变化首字符判断配色:以 + 开头为青柠绿(进步)、以 - 开头为警示红(退步)、其他为灰咖(持平或非数值),让周报复盘的数据趋势一目了然。
5.4 四列宫格分片
function chunkGrid(list: CourseItem[], size: number): CourseItem[][] {
const rows: CourseItem[][] = [];
for (let i = 0; i < list.length; i += size) {
rows.push(list.slice(i, i + size));
}
return rows;
}
该函数将课程列表按每行 4 个切分为二维数组,常量期预计算结果存入 COURSE_ROWS,避免在 ForEach 渲染时重复切片,保证了列表渲染的高效性。8 门课程被切分为 2 行 4 列的宫格布局。
六、数据模型层
6.1 WorkoutItem 训练条目模型
@Observed
export class WorkoutItem {
name: string; // 训练项目名
duration: string; // 时长
cal: string; // 消耗卡路里
status: string; // 状态:已完成/进行中/待开始
}
7 条今日训练清单数据覆盖晨间慢跑、动态热身、核心稳定、哑铃上肢、深蹲爆发、全身拉伸、泡沫轴松解,从有氧到力量到恢复形成完整训练闭环。状态字段使用中文字符串,配合 statusColor 函数实现语义化配色。
6.2 CourseItem 课程条目模型
@Observed
export class CourseItem {
icon: string; // 课程图标
name: string; // 课程名
level: string; // 难度:入门/进阶/高级
duration: string; // 课程时长
}
8 条课程数据覆盖晨跑燃脂、撸铁全身、睡前冥想、拳击有氧、柔韧拉伸、动感单车、HIIT 爆发、游泳塑形,从有氧到力量到柔韧全面覆盖,难度从入门到高级分层。课程数据同时用于四列宫格入口和横滑大卡展示,一份数据驱动两套 UI。
6.3 RemindItem 训练提醒模型
@Observed
export class RemindItem {
time: string; // 提醒时间
title: string; // 提醒标题
repeat: string; // 重复类型:每天/工作日/周末/单次
on: boolean; // 是否开启
}
6 条提醒数据从 06:30 晨跑唤醒到 21:00 体测复盘,覆盖一天训练节奏。on 字段联动时间轴圆点的呼吸闪烁效果——开启的提醒圆点随 breath 透明度波动,关闭的提醒圆点保持暗淡静态。弹窗系统的增删改均绑定此实体。
6.4 RingItem 铃声库条目模型
@Observed
export class RingItem {
name: string; // 铃声名
file: string; // 沙箱文件名
freq: number; // 生成频率 Hz
duration: number; // 时长 ms
size: string; // 文件大小展示
inSandbox: boolean; // 是否已写入沙箱 EL1
}
5 条铃声库数据(破晓鸟鸣 880Hz、心跳鼓点 440Hz、冲刺号角 1320Hz、青柠滴答 660Hz、静夜风铃 520Hz),初始均未导入沙箱,需用户点"生成到沙箱"按钮触发 saveRingToSandbox 落盘。inSandbox 标记联动铃声库列表的"已入沙箱/未导入"状态徽章,size 字段在导入后更新为实际文件大小。
6.5 NoticeLog 与 CaptionScene 模型
@Observed
export class NoticeLog {
title: string; // 通知标题
text: string; // 通知正文
time: string; // 发布时间
}
@Observed
export class CaptionScene {
scene: string; // 场景名
desc: string; // 场景说明
src: string; // 推荐源语言
tgt: string; // 推荐目标语言
}
NoticeLog 记录通知发布历史,最新在前最多 6 条,time 由 nowTime() 自动生成。6 条字幕场景预设覆盖跟练口令双语(en→zh-en)、教练讲解转写(zh→zh)、晨跑英语听力(en→zh)、冥想引导双语(en→zh-en)、赛事解说转写(zh→zh)、康复指导跟读(en→zh),点击场景卡调用 applyScene 方法一键应用推荐语言组合。
七、组件主体结构
7.1 状态变量总览
组件 Page1284 的状态变量按功能分为七组,层次清晰、职责分明。
基础 UI 状态:currentTab(当前 Tab 索引)、breath(呼吸动画开关)、timer(定时器句柄)。breath 每秒翻转,联动进度环弧长、折线图末点半径、柱状图高度、各处圆点透明度,是全局呼吸节拍的核心驱动。
弹窗状态:addModal/editModal/delModal 三个布尔开关,editIdx/delIdx 两个操作索引,三态统一绑定 RemindItem 提醒实体。
训练业务状态:workoutList、remindList、todayTotal/todayDone(进度环分母分子)、streakDays、todayCal。
特性 A 状态(Notification Kit):granted(通知授权)、notifyId(自增通知 id)、currentRingIdx(当前铃声索引)、ringList(铃声库)、noticeLogs(发布历史)、sandboxCount(已导入沙箱数)、genFreq/genDuration(生成器参数)。
特性 B 状态(Speech Kit):captionController(字幕控制器,private 非 @State)、captionShown(字幕显示状态)、srcLang/tgtLang(源/目标语言)、captionSize/captionColor(字号/颜色)、captionReady/captionErrMsg/captionFed(就绪/错误/计数)。
特性 C 状态(Canvas):ringCtx/lineCtx(画布上下文,private 非 @State)、ringReady/lineReady(画布就绪标记)。
表单状态:formTime/formTitle/formRepeat(新建表单)、editTime/editTitle/editRepeat(编辑表单)。
7.2 生命周期
aboutToAppear() {
notificationManager.isNotificationEnabled().then((enabled: boolean) => {
this.granted = enabled;
}).catch(() => {
this.granted = false;
});
this.timer = setInterval(() => {
this.breath = !this.breath;
if (this.ringReady) { this.drawRing(); }
if (this.lineReady) { this.drawLine(); }
}, 1000);
}
aboutToDisappear() {
clearInterval(this.timer);
}
aboutToAppear 做两件事:一是调用 notificationManager.isNotificationEnabled() 异步查询通知授权状态,结果赋给 granted;二是启动 1 秒间隔的呼吸定时器,翻转 breath 后检查 ringReady/lineReady 标记,若画布已就绪则手动调用 Canvas 绘制方法实现无闪烁重绘。aboutToDisappear 清理定时器,防止内存泄漏。与静态绘制不同,这里采用"画布就绪后手动重绘"策略——因为 breath 是布尔翻转,ArkUI 不会自动触发 Canvas 重绘,必须手动调用 drawRing/drawLine 才能实现动画帧更新。
7.3 build() 根构建
build() {
Stack() {
Column() {
this.headerMain()
Divider().strokeWidth(1).color(COLORS.line)
Scroll() {
Column() {
if (this.currentTab === 0) { this.tabTrain() }
else if (this.currentTab === 1) { this.tabCourse() }
// ... 其余 Tab 分支 ...
}.padding({ left: 14, right: 14, top: 12, bottom: 12 })
}.layoutWeight(1).scrollBar(BarState.Off)
this.tabBar()
}.width('100%').height('100%')
if (this.addModal) {
this.panelAdd(() => { this.addModal = false; })
}
if (this.editModal) {
this.panelEdit(() => { this.editModal = false; })
}
if (this.delModal) {
this.panelDel(() => { this.delModal = false; })
}
}.width('100%').height('100%')
.alignContent(Alignment.Center)
.backgroundColor(COLORS.bg)
}
根构建使用 Stack 层叠:底层 Column 从上到下排列头部、分割线、Scroll 内容区(layoutWeight(1) 占满中间空间)和底部 Tab 栏;顶层是三个独立条件渲染的弹窗。内容区外层包 Scroll 保证超屏内容可滚动,scrollBar(BarState.Off) 隐藏滚动条保持视觉整洁。弹窗使用回调函数模式——panelAdd/panelEdit/panelDel 各自接收一个 onClose 回调,点击遮罩或取消按钮时执行回调关闭弹窗,这种设计让弹窗的关闭逻辑由调用方控制,弹窗自身只关注表单内容。
八、头部区域详解
@Builder
headerMain() {
Column({ space: 10 }) {
Row() {
Column({ space: 4 }) {
Text('动能空间').fontSize(20).fontWeight(FontWeight.Bold).fontColor(COLORS.title)
Text(this.headerSub()).fontSize(11).fontColor(COLORS.sub)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
Circle({ width: 10, height: 10 }).fill(COLORS.orange)
.opacity(this.breath ? 0.95 : 0.45)
}.width('100%')
Row({ space: 6 }) {
// 三特性状态胶囊:通知授权 / 当前铃声 / 字幕语言
}.width('100%')
}.width('100%')
.padding({ left: 14, right: 14, top: 12, bottom: 10 })
.backgroundColor(COLORS.bg)
}
头部包含两行。第一行是品牌标题"动能空间"和 headerSub() 返回的 Tab 联动副标题——当 currentTab 为 0 时显示"训练台 · 今日 7 项完成 5 项",为 1 时显示"课程库 · 8 门精品课任意练",以此类推,每个 Tab 切换时头部副标题同步更新。右侧是呼吸圆点,活力橙色的 10px 圆形随 breath 透明度 0.95/0.45 交替波动,模拟运动心跳节拍。
第二行是三特性状态胶囊行,集中展示三大特性的当前状态:
- 通知授权胶囊:
granted为 true 时绿点 + “通知已授权”,false 时红点 + “通知未授权”,让用户一眼判断 Notification Kit 是否可用。 - 当前铃声胶囊:橙色圆点 + 当前默认铃声名(从
ringList[currentRingIdx]取),未设定时显示"未设铃声"。 - 字幕语言胶囊:湖蓝圆点 + "字幕 zh→zh"格式的语言组合标签,实时反映
srcLang/tgtLang状态。
三个胶囊使用 dark 色背景配 10px 圆角,视觉上紧凑且信息密度高,让头部成为整个平台的"状态仪表盘"。
九、Tab0 训练 — 统计三格与 Canvas 双图
训练 Tab 是平台首页,纵向五段布局,集成了 Canvas 双图(进度环 + 折线图)和训练清单。
统计三格:Row + 三个 statCell Builder 等宽排列,分别展示今日消耗(826 千卡,活力橙)、连续打卡(26 天,青柠绿)、训练时长(52 分钟,湖蓝)。statCell 是可复用的统计格 Builder,接收图标、标签、数值、单位和颜色五参数,数值使用 20px 粗体大字突出展示。
今日完成率进度环卡(特性 C · Canvas drawRing):Row 布局,左侧 180×180 的 Canvas(ringCtx) 组件,onReady 回调置 ringReady = true 并首绘。右侧是进度环说明文字——标题"今日完成率"、描述"7 项计划已完成 5 项,剩余 2 项"、已完成/待开始图例和"呼吸联动:弧长 0.85~1.0 波动"技术说明。
drawRing 方法是进度环的核心绘制逻辑,分四步:
drawRing() {
const ctx = this.ringCtx;
const progress = this.todayDone / this.todayTotal;
const breathVal = this.breath ? 1.0 : 0.85;
// ① 背景环(深咖底环)
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.strokeStyle = COLORS.dark; ctx.lineWidth = 12; ctx.stroke();
// ② 进度弧(从12点方向起笔,青柠绿,弧长随breath波动)
ctx.arc(cx, cy, r, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * progress * breathVal);
ctx.strokeStyle = COLORS.green; ctx.lineCap = 'round'; ctx.stroke();
// ③ 中心百分比大字(26px粗体暖白)
ctx.fillText(Math.round(progress * 100) + '%', cx, cy + 2);
// ④ 中心副标签"今日完成率"(10px灰咖)
ctx.fillText('今日完成率', cx, cy + 24);
}
呼吸动画通过 breathVal 系数实现:breath 为 true 时弧长乘以 1.0(完整进度),false 时乘以 0.85(弧长缩至 85%),每秒交替形成进度弧"呼吸"效果。弧线从 12 点方向(-PI/2)起笔,使用 lineCap = 'round' 让弧线端点圆润。
近 12 周训练时长折线卡(特性 C · Canvas drawLine):全宽 Canvas(lineCtx) 组件,高 170px,onReady 置 lineReady = true 并首绘。drawLine 方法分五步:
drawLine() {
// ① 背景网格(3条横线,line色描边)
// ② 青柠渐变填充区域(green → greenFade 透明渐变)
// ③ 折线(青柠绿2px描边)
// ④ 数据点(末点半径随breath放大4.5/3.5,其余点固定3)
// ⑤ 横轴周标签(W1~W12隔点绘制避免拥挤)
}
渐变填充使用 createLinearGradient 从顶部 green 到底部 greenFade(近乎透明的青柠),营造数据从高空逐渐消散的视觉效果。末点(最新一周)的数据点半径随 breath 在 4.5/3.5 间波动,形成"当前周数据脉动"的视觉焦点。横轴标签隔点绘制(W1、W3、W5…),避免 12 个标签拥挤。
今日训练清单:ForEach(workoutList) 渲染 7 行训练条目,每行包含状态指示点(statusColor 配色,进行中时随 breath 透明度闪烁)、项目名、时长+卡路里、状态胶囊。列表底部信息显示"7 项"总量。键值生成器使用 'workout-' + idx 保证列表项精准复用。
十、Tab1 课程 — 四列宫格与横滑大卡
课程 Tab 提供课程浏览入口,纵向两段布局。
四列宫格课程入口:ForEach(COURSE_ROWS) 遍历预计算好的二维数组,外层 Row 渲染行,内层 ForEach(row) 渲染列。每个宫格卡片包含 24px 课程图标、10px 粗体课程名、8px 难度标签(levelColor 配色),layoutWeight(1) 等宽分配。8 门课程切分为 2 行 4 列的紧凑宫格,点击即可快速开练。
课程时长大卡横滑:Scroll + Row + ForEach(COURSE_LIST) 横向滑动排列 8 张大卡。每张卡宽 150px,包含 30px 大图标、13px 课程名、18px 粗体橙色时长、难度+教练带练说明。横滑使用 scrollable(ScrollDirection.Horizontal) 并隐藏滚动条,形成流畅的横向浏览体验。与宫格入口不同,大卡更突出时长信息和教练带练描述,适合用户根据时间预算选择课程。
值得注意的是课程数据复用——COURSE_LIST 同时驱动宫格入口和横滑大卡,一份数据驱动两套不同粒度的 UI,体现了数据与视图分离的设计思想。宫格用 chunkGrid 预切分为二维数组避免渲染时切片,大卡直接遍历一维数组,两种遍历方式各有适配。
十一、Tab2 提醒 — 时间轴与通知发布
提醒 Tab 是 Notification Kit 的业务场景入口,纵向四段布局。
通知授权状态卡:Row 布局,左侧状态圆点(granted 为 true 时青柠绿,呼吸闪烁;false 时警示红),中间标题和说明文字,右侧授权按钮。granted 为 true 时按钮显示"已开启"(灰底绿字),false 时显示"去授权"(橙底深字),点击调用 requestAuth() 方法。requestAuth 方法先获取宿主上下文 this.getUIContext().getHostContext(),判空兜底后调用 notificationManager.requestEnableNotification(hostCtx),首次调用弹系统授权框;曾被拒绝(返回 1600004)时回退调用 notificationManager.openNotificationSettings(hostCtx) 拉起通知设置页引导用户手动开启。
双按钮行:左侧"+ 新建提醒"(灰底白字)点击置 addModal = true 打开新建弹窗,右侧"发布训练提醒"(橙底深字)点击调用 publishRemindNotice() 发布携带沙箱铃声的通知。publishRemindNotice 方法遍历 remindList 找到第一条 on 为 true 的提醒,拼接通知文案后调用 publishNotice 方法。
训练提醒时间轴:ForEach(remindList) 渲染提醒时间轴,每行固定 height(72)。时间列(宽 50px)显示时间和重复类型;竖线轴列(宽 14px)包含呼吸圆点(开启时橙色呼吸闪烁,关闭时灰咖静态)和 layoutWeight(1) 填满行高的竖线;提醒卡片列占满剩余宽度,包含标题、状态描述、Toggle 开关和编辑/删除按钮。Toggle 的 onChange 调用 toggleRemind(idx, isOn) 翻转提醒开关。编辑按钮调用 openEditRemind(idx) 回填表单并打开编辑弹窗,删除按钮置 delIdx 并打开删除确认弹窗。
通知发布历史:noticeLogs 为空时显示提示文案"暂无发布记录,点上方「发布训练提醒」试听沙箱铃声";有数据时用 List + ForEach 渲染,每条记录包含蓝点、标题、正文(含发布结果)和时间戳。头部信息栏显示"通知 id 自 N",让用户感知 notifyId 自增机制。
十二、Tab3 铃音 — 沙箱铃声生成器
铃音 Tab 是 Notification Kit 沙箱自定义铃声特性的核心展示区,纵向四段布局。
当前默认铃声状态卡:显示当前默认铃声名称和沙箱导入进度(“已导入沙箱 N / M 个 · 发布通知时以其为 sound”),让用户清晰了解铃声库的整体导入状态。
正弦波生成器卡:这是"铃声坊"的核心交互区。Slider 控件范围 440~1320Hz、步进 20,onChange 实时更新 genFreq。三档时长预设(900/1200/1500ms)以 chips 形式排列,选中态橙底深字、未选中态灰底浅字。底部"生成 44 字节头 PCM 正弦波并写入沙箱"按钮调用 genCustomRing() 方法——创建新 RingItem 并追加到列表,调用 saveRingToSandbox 落盘,成功后更新 inSandbox 和 size。
铃声库列表:ForEach(ringList) 渲染每条铃声,包含铃声名(当前默认铃声前缀 ✓)、频率+时长+大小信息、沙箱状态徽章。底部双按钮:左侧"生成到沙箱/重新生成到沙箱"调用 importRing(idx),右侧"设为默认铃声/当前铃声"调用 setCurrentRing(idx)(未导入沙箱时先自动导入)。
saveRingToSandbox 方法是沙箱铃声链路的关键环节:
saveRingToSandbox(fileName: string, freq: number, durationMs: number): string {
const hostCtx = this.getUIContext().getHostContext() as common.UIAbilityContext;
const appCtx = hostCtx.getApplicationContext();
appCtx.area = contextConstant.AreaMode.EL1; // 必须在 EL1 沙箱下
const dir = appCtx.filesDir; // EL1 区域 files 目录
const path = dir + '/' + fileName;
const data = buildWavBytes(freq, durationMs); // 生成正弦波 WAV
const file = fs.openSync(path, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY | fs.OpenMode.TRUNC);
fs.writeSync(file.fd, data);
fs.closeSync(file);
return path;
}
该方法设置 appCtx.area = contextConstant.AreaMode.EL1 确保在 EL1 沙箱区域操作,通过 fs.openSync 以创建+只写+截断模式打开文件,写入 WAV 字节后 closeSync 关闭文件描述符。
publishNotice 方法完成从沙箱铃声到通知发布的完整链路:
publishNotice(title: string, text: string) {
const ring = this.ringList[this.currentRingIdx];
const appCtx = hostCtx.getApplicationContext();
appCtx.area = contextConstant.AreaMode.EL1;
const sandboxPath = appCtx.filesDir + '/' + ring.file;
const uri = fileUri.getUriFromPath(sandboxPath);
const soundVal = 'uri::' + uri; // ★ 沙箱路径 → uri → 'uri::' 前缀
const request: notificationManager.NotificationRequest = {
id: this.notifyId++,
notificationSlotType: notificationManager.SlotType.SOCIAL_COMMUNICATION,
content: { normal: { title, text, additionalText: '铃声:' + ring.name } },
sound: soundVal // ★ 原来只能传 rawfile 文件名,现在支持沙箱 uri
};
notificationManager.publish(request).then(() => {
this.addNoticeLog(title, text);
}).catch((err: BusinessError) => {
this.addNoticeLog(title, '发布失败 ' + err.code + ':' + err.message);
});
}
核心创新在于 sound 字段填值:传统方式只能传 rawfile 目录下的文件名,现在通过 'uri::' + fileUri.getUriFromPath(沙箱路径) 拼接,支持动态生成的沙箱音频文件作为通知铃声,实现了"铃声坊"式自定义铃声体验。通知类型设为 SOCIAL_COMMUNICATION(社交通信类),id 自增避免覆盖上一条通知,additionalText 附加铃声来源说明。
十三、Tab4 字幕 — AI 字幕五区块设置面板
字幕 Tab 是 Speech Kit AI 字幕特性的核心展示区,纵向五区块布局。
① AICaptionComponent 实时预览区:使用 AICaptionComponent 组件,传入 captionController(字幕控制器)、buildCaptionOptions() 返回的 AICaptionOptions 和 captionShown(@State 引用传参,实现双向绑定)。预览区下方三按钮行:"开启字幕/隐藏字幕"切换 captionShown、"写入演示音频"调用 feedAudioStream() 写入 640 字节 PCM 块、已写块计数显示。错误信息以红色文案展示。
buildCaptionOptions 方法组装完整配置:
buildCaptionOptions(): AICaptionOptions {
return {
initialOpacity: 1,
sourceLanguage: this.srcLang, // ★ 源语言('zh' | 'en')
targetLanguage: this.tgtLang, // ★ 目标语言('zh' | 'en' | 'zh-en')
fontSize: this.captionSize, // ★ 字体大小(AICaptionFontSize 枚举)
fontColor: this.captionColor, // ★ 字体颜色(ResourceColor)
onPrepared: () => { this.captionReady = true; },
onError: (error: BusinessError) => { this.captionErrMsg = '...'; }
};
}
四个新字段齐全:sourceLanguage、targetLanguage、fontSize、fontColor。onPrepared 回调置 captionReady = true 表示字幕服务就绪,onError 回调捕获 BusinessError 并设置错误信息。
feedAudioStream 方法演示音频写入:生成 640 字节 PCM 块(16kHz/16bit/单声道,约 20ms),通过 captionController.writeAudio({ data: block }) 写入音频流,captionFed 计数。每个 640 字节块对应 20ms 音频,连续写入形成实时语音流。
② 语言设置联动卡:源语言 chips 两选(中文/英文),点击调用 switchSourceLang(code) 联动目标语言。中文源时目标语言锁定 zh(显示"中文(锁定)"无选项),英文源时目标语言三选(中文/英文/中英双语)。这种联动逻辑确保了语言组合的有效性——中文源不支持翻译方向,英文源才支持翻译到其他语言。
③ 字号四档卡:ForEach(SIZE_OPTIONS) 渲染小号/标准/大号/超大四档,选中态橙底深字,点击设置 captionSize 为对应 AICaptionFontSize 枚举值。字号使用枚举而非数字,保证了类型安全。
④ 五色卡:ForEach(CAPTION_FONT_COLORS) 渲染五个色块(经典白/暖黄/薄荷绿/天空蓝/樱花粉),选中态叠加 ✓ 标记,点击设置 captionColor。
⑤ 字幕场景卡:ForEach(sceneList) 渲染 6 个场景预设,每行包含场景图标、名称、说明和推荐语言组合标签。当前语言组合匹配时标签变青柠绿,点击调用 applyScene(scene) 一键应用推荐语言组合,降低用户配置成本。
十四、Tab5 我的 — 渐变身份卡与柱状图
我的 Tab 展示运动者个人信息,纵向三段布局。
身份渐变大卡:linearGradient 从 gradA(深橙棕)到 gradB(深活力橙)的 135° 渐变,包含 34px 运动图标、"晨曦跑者 · Leo"昵称、"Lv.6 体能进阶 · 已坚持 26 周"等级信息,以及三列身份统计格(累计里程 1284km / 周均消耗 962 千卡 / 获得徽章 14 枚)。idStat Builder 渲染身份卡内的小统计格,数值用 13px 粗体暖白,标签用 9px 深棕字(onMain),保证渐变背景上的文字可读性。
近 6 个月柱状图卡:chartCard Builder 使用传统 Column + ForEach 实现柱状图(非 Canvas 绘制)。ForEach(MONTH_IDX) 渲染 6 根柱子,柱高由 barHeight 方法计算——v / MONTH_MAX * 78 归一化映射到 0~78px 范围,breath 翻转时柱高乘以 0.93 形成呼吸波动。最新月份(8 月)使用 orange(活力橙),其余月份使用 orangeD(深橙),形成"当前月突出"的视觉重点。柱状图整体 height(110) 配 alignItems(VerticalAlign.Bottom) 让柱子从底部对齐。
周报复盘清单:ForEach(WEEK_REVIEW) 渲染 6 行复盘数据,每行包含图标、指标名、本周数值和环比变化(deltaColor 配色:+ 开头青柠绿、- 开头警示红、其他灰咖)。6 项指标覆盖有氧总时长(+12%)、力量训练量(+8%)、周消耗热量(+6%)、深睡平均(-3%)、静息心率(-2)、腿部酸痛度(正常),全面反映运动训练的健康影响。
十五、底部 Tab 栏与弹窗系统
15.1 底部 Tab 栏
@Builder
tabBar() {
Row() {
ForEach(TAB_LIST, (tab: TabMeta, idx: number) => {
Column({ space: 3 }) {
Text(tab.icon).fontSize(20).opacity(this.currentTab === idx && this.breath ? 1 : 0.75)
Text(tab.label).fontSize(9).fontColor(this.currentTab === idx ? COLORS.tabOn : COLORS.text3)
}.layoutWeight(1).padding({ top: 8, bottom: 8 })
.onClick(() => { this.currentTab = idx; })
}, (tab: TabMeta) => 'tab-' + tab.label)
}.width('100%').backgroundColor(COLORS.card)
}
底部 6 Tab 单排等宽排列,选中态 tabOn(活力橙)标签字 + 图标不透明度随 breath 在 1/0.75 间波动(呼吸高亮),未选中态 text3(灰咖)标签字 + 图标固定 0.75 透明度。点击切换 currentTab,触发内容区 Builder 切换。键值生成器使用 'tab-' + tab.label 保证 Tab 项精准复用。
15.2 弹窗系统
弹窗系统采用"全屏遮罩 + 居中卡片"模式,三个独立弹窗各自条件渲染。
modalOverlay 遮罩层:全屏 Column 填充半透黑遮罩色,onClick 执行 onClose 回调关闭弹窗。所有弹窗都复用此遮罩 Builder,保证遮罩行为一致。
panelAdd 新建提醒弹窗:Stack 层叠遮罩和居中卡片(宽 78%),包含标题"新建训练提醒"、时间输入框、标题输入框、重复类型 chips(每天/工作日/周末/单次)、取消/创建双按钮。saveRemind 方法读取表单状态,空字段用默认值兜底(时间默认 07:00、标题默认"训练提醒"),push 新 RemindItem 到列表并 todayTotal + 1,最后关闭弹窗并重置表单。
panelEdit 编辑提醒弹窗:结构同新建弹窗,但表单预填当前提醒的值(openEditRemind 回填 editTime/editTitle/editRepeat)。updateRemind 方法实现字段级更新——空输入不覆盖原值(if (this.editTime !== '') 判空),保证用户只想修改部分字段时其他字段不被清空。更新后 remindList = this.remindList.slice() 触发数组引用变化,驱动 ForEach 重渲染。
panelDel 删除确认弹窗:简洁的确认对话框,标题"删除训练提醒"+ 说明文案 + 取消/确认删除双按钮。delRemind 方法 splice 移除指定索引的提醒项,todayTotal 同步减 1。删除按钮使用 COLORS.red 背景强化危险操作视觉提示。
三个弹窗的关闭模式统一——通过 onClose 回调由调用方控制,弹窗内部不直接修改 addModal/editModal/delModal 状态,实现了弹窗组件的关注点分离。
十六、功能模块对比表
| 维度 | 训练 Tab | 课程 Tab | 提醒 Tab | 铃音 Tab | 字幕 Tab | 我的 Tab |
|---|---|---|---|---|---|---|
| 布局方式 | 纵向五段+Canvas双图 | 纵向两段+宫格+横滑 | 纵向四段+时间轴 | 纵向四段+生成器 | 纵向五区块 | 渐变大卡+柱状+清单 |
| 数据模型 | WorkoutItem | CourseItem | RemindItem | RingItem | CaptionScene | WeekRow |
| 字段数 | 4 | 4 | 4 | 6 | 4 | 4 |
| 核心操作 | 进度环/折线图/清单浏览 | 宫格选课/横滑浏览 | 新增/编辑/删除/发布通知 | 生成/导入/设默认 | 语言/字号/颜色/场景 | 数据展示 |
| 动画效果 | 进度环弧长+折线末点呼吸 | — | 圆点呼吸闪烁 | — | — | 柱高呼吸波动 |
| 状态颜色 | 已完成绿/进行中橙/待开始灰 | 入门绿/进阶橙/高级红 | 授权绿红/开启橙/关闭灰 | 已入沙箱绿/未导入灰 | 匹配绿/默认灰 | 上升绿/下降红/持平灰 |
| 数据量 | 7 训练项+12周数据 | 8 课程 | 6 提醒 | 5 铃声 | 6 场景 | 6 月柱+6 复盘行 |
| 特殊组件 | Canvas进度环+Canvas折线 | Scroll横滑+宫格分片 | Toggle开关+List历史 | Slider+文件IO | AICaptionComponent | linearGradient+Column柱状 |
| 对应特性 | 特性C·Canvas绘制 | — | 特性A·通知授权 | 特性A·沙箱铃声 | 特性B·AI字幕 | — |
深化解析:从代码结构到业务闭环
布局方式与数据流
运动健康页面围绕计划、训练、提醒、趋势和复盘形成路径。课程模型描述可选择的训练内容,训练记录表达一次真实执行,进度环、折线图和柱状图把结果转为可比较指标。分析时要说明动画如何由 breath 状态驱动、图表数值如何从数组映射,以及提醒和铃声如何与训练计划协同。
页面根结构通常由头部、内容区和底部 Tab 栏组成。头部负责展示当前业务状态,内容区根据索引选择不同的 @Builder,底部导航负责修改索引。这样的结构把“当前显示什么”收敛为一个明确状态:用户点击 Tab 后先更新索引,ArkUI 再重新计算相关分支。各个 Builder 虽然共享主题色和页面级数据,却可以采用完全不同的布局方式;高密度列表适合纵向 Scroll,概览数据适合横向统计卡或双列 Flex,实时预览类组件需要独占有界高度,历史事件则适合时间轴或固定行高 List。
数据模型层承担界面与业务之间的契约。使用 @Observed 的实体保存可编辑字段,页面级 @State 数组负责驱动 ForEach。新增时创建新实体并插入数组,编辑时修改目标实体,删除时移除对应项。为了让列表差分稳定,key 应来自不会改变的唯一标识,不宜使用标题等可编辑字段。统计数字、完成比例和分类数量属于派生信息,可以从数组即时计算,避免同时维护两份状态后出现卡片已经更新、图表仍显示旧值的情况。
弹窗表单使用独立缓存是必要的。打开新增弹窗时清空缓存,打开编辑弹窗时复制目标字段,用户确认后才写回正式模型。这样点击取消不会污染列表数据。若直接把 TextInput 双向绑定到列表实体,用户尚未保存时卡片就可能跟着变化,破坏“确认提交”的交互语义。删除弹窗还需要保存目标索引或唯一标识,并在确认时再次校验目标存在,避免列表变化后误删其他项。
核心代码与状态驱动机制
@State 的价值不是简单替代普通变量,而是建立状态与界面之间的依赖关系。当前 Tab、筛选条件、动画开关、弹窗显隐、下载进度或能力状态发生变化时,只有读取这些变量的组件需要刷新。代码段中连续的修饰器调用分别控制尺寸、间距、背景、字体和事件,它们共同构成声明式描述;阅读时应从容器方向、子项分布、状态绑定和交互回调四个层面理解,而不是逐个孤立翻译属性名称。
ForEach 负责把数组映射为重复 UI。回调中的 item 提供业务字段,index 适合显示顺序,但不适合作为长期身份。列表发生新增或删除时,稳定 key 可以让框架复用未变化节点,减少重建。若直接修改对象属性后界面没有按预期刷新,可在保持实体身份的前提下替换数组引用;但不应为了刷新把所有元素都重新构造,否则会增加无意义渲染并丢失局部状态。
条件渲染体现了页面状态机。空闲时展示引导,准备中展示进度,成功时展示结果,失败时展示原因和重试入口。相比一个布尔值,四态文案更能覆盖异步能力。系统接口调用前先检查权限、设备支持和会话状态,调用后再读取结果校验。异常处理除了记录错误码,还要把可理解的反馈写入响应式状态,让用户知道失败发生在哪一步。
动画效果与颜色使用策略
呼吸动画通常由定时器周期翻转 breath,再把该状态映射为透明度、柱高或圆点半径的小幅变化。它适合表达“正在运行”或让统计图保持生命感,但幅度应克制,不能改变核心数据含义。柱状图的基础高度仍由真实数值计算,动画只能在很小范围内偏移;进度环的角度仍由完成比例决定,不能为了视觉效果显示超过真实进度的结果。页面离开时必须清理定时器,避免后台继续刷新。
颜色常量应按语义使用。主色承担选中态和主要操作,辅助色突出数据或次级动作,绿色表达完成与可用,橙色表达进行中或需要注意,红色只用于失败、逾期和删除等高风险场景。弱文本与分割线降低视觉权重,遮罩色用于聚焦弹窗。颜色不能成为唯一的状态信息,还要配合文字、图标或进度值,保证色觉差异用户也能理解。
渐变更适合头部大卡、核心指标或柱状图,不宜在每个小元素上重复使用。深色主题要检查正文与卡片背景的对比度,浅色主题则要避免辅助文字过淡。选中和未选中 Tab 除颜色差异外,还可以通过字重、图标透明度或底部指示器区分。这样既保持主题统一,又能建立清晰的信息层级。
各 Tab 之间的交互联动
各 Tab 不应只共享一个导航索引,还应围绕业务对象建立必要联动。列表页新增或编辑数据后,头部计数、图表和个人统计要同步更新;网页或地图产生的结果应写入记录模型,供下载、日志或我的页面继续展示;通知、字幕、相机等系统能力的状态应在头部胶囊或对应 Tab 中保持一致。跨 Tab 跳转时先更新必要参数,再修改当前索引,可以避免目标页面读取到旧条件。
切换离开重型组件时需要处理资源边界。相机输入、地图监听、字幕控制器、Web 下载代理和定时器都不能只创建不释放。可以在统一的 switchTab 方法中判断来源与目标,离开能力页时解除监听或停止会话;页面销毁时再执行兜底释放。释放方法应允许重复调用,并对每个资源独立判空,确保一次异常不会阻止后续清理。
交互反馈要覆盖成功与失败。按钮点击后先进入处理中状态并防止重复提交;成功后更新模型、关闭弹窗并显示结果;失败后保留用户输入,展示错误原因和重试入口。权限拒绝、能力不支持、网络失败、文件不存在和输入非法都属于正常业务分支。通过状态卡或行内提示展示这些分支,比只在控制台打印更符合完整产品体验。
边界场景与验证思路
空列表时应显示占位说明和新增入口,不能只留下空白。长标题需要限制行数并使用省略号,数字字段需要限定上下界,文本提交前要去除首尾空格。筛选后无结果应保留清除条件的入口。删除最后一项后,当前选择索引要回退到有效范围。异步搜索连续触发时,应防止较早请求晚返回后覆盖新结果。
验证数据链路时,可以依次检查新增、编辑、删除和筛选:新增后列表条数、统计数字和图表是否同时变化;编辑取消后正式数据是否保持不变;删除后 ForEach key 是否稳定;切换 Tab 再返回时必要数据是否仍在。验证系统能力时分别模拟支持、拒绝和异常,确认界面都有明确状态。验证动画时检查页面离开后是否停止,低性能设备上是否仍保持流畅。
视觉验收需要检查不同屏幕宽度、系统字体放大、深浅背景对比和长文本换行。表格中的布局方式、模型、字段数、核心操作、动画、状态颜色、数据量和特殊组件应与正文一致。Mermaid 图则需要对应真实的数据流和能力链路,节点文字加引号以避免中文或特殊字符导致解析失败。
组件化设计的进一步理解
参数化 Builder 适合抽取重复的统计格、状态行、标签和按钮组。参数只传入渲染所需数据和事件,不让子构建器直接依赖过多页面变量,可以降低耦合。业务复杂后,可把模型与系统能力封装为独立控制器,页面只负责组合 UI 和响应状态。这样既保留声明式代码的直观性,也能让权限、错误码翻译和资源释放得到集中管理。
当前单页面集中展示完整源码,便于博文逐段讲解。若演进为正式项目,可以按领域拆分组件:导航和页面框架位于容器层,列表、图表和弹窗位于展示层,数据读写和 Kit 接入位于服务层。组件之间通过参数、回调、@Link 或 @ObjectLink 传递状态,不使用全局变量代替清晰的数据流。
性能优化首先来自减少不必要刷新。派生数据不要重复存储,动画状态不要进入列表 key,长列表使用稳定标识,Canvas 只在数据或尺寸变化时重绘。其次是控制资源生命周期,页面不可见时停止高成本任务。最后才是微调阴影、渐变和绘制细节。这样的优先级能保证页面在功能增加后仍然可维护。
通过以上补充,可以看到 ArkUI 的声明式模式并非只让布局语法更简洁,它更重要的价值是把数据变化、界面刷新和交互反馈连接为可追踪链路。理解每个代码段读取什么状态、写入什么状态、影响哪些组件,才能真正掌握文章中多个 Tab、图表、弹窗和系统能力协同工作的原理。
十七、总结与展望
本平台以"夜跑黑 + 活力橙 + 青柠绿"的深色视觉体系为基底,将 HarmonyOS 的三大前沿特性——Notification Kit 沙箱自定义铃声、Speech Kit AI 字幕四新字段、Canvas 进度环与折线图——有机融合进运动健康管理的 6 个业务场景中。
在技术架构上,平台采用"状态集中声明 + Builder 分散渲染"的模式。Notification Kit 的沙箱铃声链路是一大创新——通过 buildWavBytes 函数动态生成正弦波 PCM 音频,写入 EL1 沙箱 filesDir 目录后,以 'uri::' + fileUri.getUriFromPath(沙箱路径) 拼接为 NotificationRequest.sound 字段值,突破了传统 rawfile 静态资源的限制,实现了"铃声坊"式音频生成器体验。用户可通过 Slider 调节频率(440~1320Hz)、选择时长档位(900/1200/1500ms),实时生成个性化运动提醒铃声。Speech Kit 的 AICaptionOptions 通过 buildCaptionOptions 方法组装,四个新字段(sourceLanguage/targetLanguage/fontSize/fontColor)齐全,onPrepared 和 onError 回调兜底,配合 feedAudioStream 的 640 字节 PCM 块写入演示了完整的实时语音转字幕链路。Canvas 绘制通过 drawRing(四步:背景环→进度弧→中心百分比→副标签)和 drawLine(五步:网格→渐变填充→折线→数据点→横轴标签)两个方法实现双图可视化,breath 状态联动进度环弧长(0.85~1.0 波动)和折线末点半径(3.5/4.5 波动),形成统一的呼吸动画节拍。
在交互设计上,平台有几个值得关注的设计决策。其一,呼吸动画采用"画布就绪后手动重绘"策略——breath 是布尔翻转不会自动触发 Canvas 重绘,需在 setInterval 回调中手动调用 drawRing/drawLine,这保证了动画帧的确定性更新。其二,提醒时间轴采用固定行高 72px + layoutWeight(1) 竖线填满的设计,保证了竖线轴的视觉连贯性。其三,字幕语言联动逻辑确保了组合有效性——中文源锁定 zh(不支持翻译方向),英文源才开放三选目标语言。其四,弹窗系统采用回调模式,panelAdd/panelEdit/panelDel 接收 onClose 回调,弹窗内部不直接修改状态,实现了关注点分离。其五,编辑弹窗的字段级更新策略——空输入不覆盖原值,让用户可以只修改部分字段而不影响其他字段。
展望未来,本平台可在以下方向深化:接入运动传感器数据实现实时心率/步频/配速的 Canvas 动态图表;引入 AI 运动姿态识别实现动作质量评估与实时纠错;通过分布式能力实现手机+手表+大屏多设备协同训练场景;利用穿戴设备采集睡眠/血氧/体温数据丰富健康评估维度;扩展 Notification Kit 的通知渠道分级(训练提醒/休息提醒/赛事提醒各有独立铃声和优先级);深化 Speech Kit 的实时翻译能力支持多语种运动课程跟练。HarmonyOS 的 AI 能力、分布式架构和传感器生态为这些扩展提供了坚实的技术底座,"动能空间"有望从训练记录工具进化为全场景运动健康智能伴侣。
附录:DevEco Studio 创建新项目与查看 SDK 版本
本章节演示如何使用 DevEco Studio 创建一个 HarmonyOS 新项目,并查看当前 IDE 已安装的 SDK 版本,适合作为其他技术博文的补充操作指南。
一、创建新项目
1.1 进入欢迎界面
启动 DevEco Studio 后,首先看到的是欢迎界面。左侧导航栏默认选中 “项目”,右侧提供三个主要入口:
- 新建项目:从头创建新项目
- 打开项目:打开本地已有项目
- 克隆仓库:从 Git 等版本控制拉取代码
点击 “新建项目” 按钮,进入项目创建向导。

1.2 选择项目模板
在弹出的"新建项目"对话框中,左侧分类标签提供了两种项目类型:
| 类型 | 说明 |
|---|---|
| 应用(Application) | 开发标准的 HarmonyOS 应用,具备完整的 Ability 生命周期 |
| 元服务(Atomic Service) | 开发轻量级的原子化服务,无需安装即可使用 |
选择 “应用” 标签后,右侧展示多种模板。对于大多数场景,推荐选择 “Empty Ability” —— 这是一个最基础的入门模板,仅包含 Hello World 功能,适合从零开始构建应用。

1.3 配置项目信息
点击 “下一步” 后,进入项目配置界面,需要填写以下核心参数:
| 配置项 | 示例值 | 说明 |
|---|---|---|
| 项目名称(Project name) | rollboat |
应用的项目名称,建议使用英文命名 |
| 包名(Bundle name) | com.rollboat.myapplication |
应用唯一标识,采用反向域名格式 |
| 保存路径(Save location) | D:\CodeFactory\rollboat |
项目本地存储路径,避免使用中文和空格 |
| 兼容 SDK(Compatible SDK) | 6.1.1(24) |
目标 HarmonyOS API 版本,点击"查看参考"可了解各版本差异 |
| 模块名称(Module name) | entry |
主模块名称,默认 entry 为应用入口模块 |
| 设备类型(Device types) | ☑ Phone | 勾选目标设备:Phone / Tablet / 2in1 / Car / Wearable / TV |
右侧预览区会实时展示当前模板的默认效果 —— 一个居中显示的 “Hello World” 文本。

1.4 完成创建
确认配置无误后,点击右下角 “完成” 按钮,IDE 将自动执行以下操作:
- 生成项目骨架(Stage 模型目录结构)
- 执行
ohpm install安装依赖 - 运行 Hvigor 构建初始化(
Build Init)
构建日志中显示 “退出代码为 0” 表示项目初始化成功。

1.5 项目结构概览
创建完成后,左侧项目面板展示的是标准的 Stage 模型 目录结构:
rollboat/
├── .hvigor/ # Hvigor 构建工具缓存
├── .idea/ # IDE 配置文件
├── AppScope/ # 应用级全局配置
│ └── app.json5
├── entry/ # 主模块(入口模块)
│ ├── src/main/ets/
│ │ ├── entryability/ # Ability 生命周期管理
│ │ │ └── EntryAbility.ets
│ │ └── pages/ # UI 页面
│ │ └── Index.ets # 首页(默认 Hello World)
│ ├── src/main/resources/ # 资源文件
│ ├── module.json5 # 模块配置
│ └── build-profile.json5 # 构建配置
├── oh_modules/ # OHPM 依赖包
├── build-profile.json5 # 工程构建配置
├── hvigorfile.ts # Hvigor 构建脚本
└── oh-package.json5 # 包管理配置
核心文件 Index.ets 的默认代码如下,采用 ArkTS 声明式 UI 语法:
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
build() {
RelativeContainer() {
Text(this.message)
.id('HelloWorld')
.fontSize($r('app.float.page_text_font_size'))
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
.onClick(() => {
this.message = 'Welcome';
})
}
.height('100%')
.width('100%')
}
}
| 关键语法 | 作用 |
|---|---|
@Entry |
标记为页面入口,可用于路由跳转 |
@Component |
声明为自定义组件 |
@State |
状态变量,数据变更时自动触发 UI 刷新 |
RelativeContainer |
相对布局容器,替代传统线性布局 |
.onClick() |
点击事件,此处点击后文本变为 “Welcome” |
打开右侧 Previewer(预览器),选择 Phone 设备,即可实时预览 Hello World 效果,无需连接真机或启动模拟器。

二、查看 SDK 版本
2.1 查看 HarmonyOS SDK
DevEco Studio 安装时已内置 HarmonyOS SDK,无需单独下载。通过以下路径查看:
文件 → 设置 → HarmonyOS SDK(或快捷键
Ctrl + Alt + S搜索 “HarmonyOS SDK”)
在设置面板中,可以看到当前已安装的 SDK 版本信息:
| 名称 | 阶段 | 状态 |
|---|---|---|
| HarmonyOS 6.1.1 | Release | ✅ 已安装 |
界面顶部提示:“HarmonyOS SDK 已经包含在 IDE,无需单独安装”,省去了手动配置 SDK 的繁琐步骤。

2.2 查看 ArkUI-X SDK(跨平台扩展)
如果项目需要将 ArkUI 框架扩展到多个 OS 平台(Android / iOS / OpenHarmony),还需要配置 ArkUI-X SDK。路径如下:
文件 → 设置 → 语言和框架 → ArkUI-X
在这里可以查看已安装和可选的 ArkUI-X SDK 版本:
| 版本 | SDK 版本号 | 阶段 | 状态 |
|---|---|---|---|
| API Version 24 | 6.1.1.100 | Release | ✅ 已安装 |
| API Version 23 | 6.1.0.28 | Beta1 | 未安装 |
| API Version 22 | 6.0.2.112 | Release | 未安装 |
安装路径示例:D:\DevTools\ArkUI-X\sdk
说明:ArkUI-X 允许开发者使用一套 ArkTS 主代码,同时构建多平台应用。如果仅开发 HarmonyOS 原生应用,无需额外安装 ArkUI-X SDK。

三、小结
| 步骤 | 操作 | 关键点 |
|---|---|---|
| 创建项目 | 欢迎页 → 新建项目 → 选择 Empty Ability 模板 → 配置项目信息 → 完成 | 使用 Stage 模型 + ArkTS 语言 |
| 查看 SDK | 设置 → HarmonyOS SDK | SDK 已内置,无需手动安装 |
| 跨平台扩展 | 设置 → ArkUI-X | 根据需要安装对应 API 版本 |
至此,DevEco Studio 的项目创建与 SDK 环境确认全部完成,可以开始 HarmonyOS 应用的功能开发。
更多推荐



所有评论(0)