读懂 HarmonyOS ArkUI 体能星球运动健康管理平台中的夜跑黑与活力橙的运动节拍
一、技术前言
在全民健身与数字化健康管理深度融合的浪潮中,运动健身应用正经历从"记录工具"到"智能陪练"的深刻变革。从晨间慢跑的卡路里精准统计到HIIT爆发训练的实时心率监控,从训练提醒的自定义铃声推送到跨语言教练口令的AI字幕转写,每一个运动场景都需要匹配不同的数据维度、交互模式和反馈机制。传统运动健康管理应用面临三大挑战:通知提醒铃声千篇一律导致用户忽略训练计划、跨语言训练课程字幕无法灵活配置导致跟练体验割裂、训练数据可视化维度单一导致进步趋势模糊。
HarmonyOS ArkUI 框架为这些挑战提供了系统级解决方案。ArkUI 的声明式 UI 范式通过 @Component 封装可复用组件、@State 管理响应式状态、@Builder 拆分复杂 UI 结构,天然适合"训练-课程-提醒"多 Tab 架构。@Observed 装饰器让数据模型字段级变化被 UI 感知,实现"状态更新即视图刷新"的流畅体验。ForEach 配合 layoutWeight 弹性布局使列表渲染与自适应排版一气呵成。
本平台深度融合 HarmonyOS 6.1.1 的三大前沿特性。Notification Kit(API 24)实现了沙箱自定义铃声通知能力——通过 buildWavBytes 生成正弦波 44 字节头 PCM 音频数据,saveRingToSandbox 将音频文件写入 EL1 区域 filesDir 沙箱目录,最终通过 NotificationRequest.sound = 'uri::' + fileUri.getUriFromPath(沙箱路径) 实现通知铃声的完全自定义。Speech Kit(API 24)的 AI 字幕组件引入了 sourceLanguage、targetLanguage、fontSize、fontColor 四个新字段,支持中英双向翻译、中英双语对照、四档字号调节和五色字体预设,配合 writeAudio 640 字节 PCM 块写入实现实时语音转字幕。Canvas 绘制 实现今日完成率进度环(drawRing)与近 12 周训练时长折线图(drawLine),通过 breath 状态联动数据值微波动模拟实时刷新效果。
二、整体架构流程图
架构以主组件为根,使用 Stack 容器层叠:底层 Column 纵向排列头部品牌区、分割线、Scroll 内容区和底部 Tab 栏,顶层是三个独立弹窗(panelAdd/panelEdit/panelDel 各自条件渲染)。内容区通过 currentTab 状态变量在 6 个 @Builder 方法间切换,三大特性分散在训练(Canvas 进度环与折线图)、提醒(沙箱铃声通知)、铃音(WAV 生成器与沙箱写入)和字幕(AI 字幕四字段)四个 Tab 上。状态变量统一声明在组件顶层实现跨 Tab 共享,breath 呼吸动画定时器每秒翻转一次,联动 Canvas 重绘与 UI 元素透明度波动。
从布局层次来看,根构建方法 build() 内部的 Stack 容器是所有视觉元素的根节点。Stack 的第一层子节点是 Column,它纵向排列三大区域:headerMain() 头部品牌区占据顶部固定高度,Divider 分割线以 1px 宽度 line 色绘制,Scroll 内容区通过 layoutWeight(1) 填充剩余高度并设置 scrollBar(BarState.Off) 隐藏滚动条,tabBar() 底部导航栏占据底部固定高度。Stack 的第二、三、四层子节点是三个条件渲染的弹窗 @Builder,它们在 addModal/editModal/delModal 为 true 时才挂载到视图树,通过 alignContent(Alignment.Center) 使弹窗内容卡居中显示。这种分层架构确保了内容区与弹窗系统的视觉隔离——弹窗始终覆盖在内容区之上,且点击遮罩区域可关闭弹窗而不影响底层内容。
三、色彩体系设计
3.1 ColorPalette 接口定义
interface ColorPalette {
bg: string; // 页面底色·夜跑黑
card: 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(活力橙)而非 green(青柠绿),这是因为橙色在深色背景上对比度更高,用户视觉定位更迅速。身份渐变大卡使用 linearGradient 从 gradA(深橙棕)到 gradB(活力橙)的 135 度渐变,模拟从黎明到日出的色彩过渡,呼应"晨曦跑者"的身份意象。折线图渐变填充从 green(青柠绿)到 greenFade(青柠渐隐 0.06 透明度),形成数据峰值的高亮衰减效果。
从色彩工程化角度分析,ColorPalette 接口与 COLORS 常量的分离设计实现了"类型约束"与"值定义"的解耦。接口层通过 string 类型约束确保所有颜色值都是合法的 CSS 颜色字符串,常量层集中管理所有色值,避免了硬编码散落在各 @Builder 方法中。当需要切换主题(如从深色切换到浅色)时,只需替换 COLORS 常量对象即可,所有引用 COLORS.xxx 的组件自动更新。onMain 字段(#241305)专为活力橙底色按钮上的深色文字设计,保证 WCAG 对比度标准。mask 使用 rgba 格式而非十六进制,因为半透明遮罩需要 alpha 通道控制,0.6 的透明度既保证遮盖效果又允许底层内容隐约可见。dark 色(#262220)介于 bg 和 card 之间,用于统计格底色、标签底色和次级容器,形成三级深色层次。
四、Tab 元数据与常量定义
4.1 底部导航 Tab 定义
const TAB_LIST: TabMeta[] = [
{ icon: '💪', label: '训练' },
{ icon: '🏋️', label: '课程' },
{ icon: '⏰', label: '提醒' },
{ icon: '🔔', label: '铃音' },
{ icon: '🗣', label: '字幕' },
{ icon: '👤', label: '我的' }
];
底部导航采用 6 Tab 单排布局,每个 Tab 对应完全不同的页面布局结构。训练 Tab 聚焦今日运动数据与清单管理;课程 Tab 提供精品课程宫格与横滑大卡入口;提醒 Tab 管理训练节奏时间轴与通知发布;铃音 Tab 是沙箱自定义铃声的生成与管理中心;字幕 Tab 配置 AI 字幕的语言字号色;我的 Tab 展示用户身份与月度趋势。
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: '中英双语' }
];
const SIZE_OPTIONS: SizeOption[] = [
{ size: AICaptionFontSize.SMALL, name: '小号' },
{ size: AICaptionFontSize.NORMAL, name: '标准' },
{ size: AICaptionFontSize.BIG, name: '大号' },
{ size: AICaptionFontSize.LARGE, name: '超大' }
];
源语言支持中文与英文两种,目标语言在英文源时支持中文、英文、中英双语三种,但中文源时目标语言锁定为中文(因为中文源无翻译方向)。字号四档对应 AICaptionFontSize 枚举值,注意 fontSize 字段接收的是枚举类型而非数字,这是 6.1.1 新特性的类型约束。
4.3 训练数据常量
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_NAME: string[] = ['03', '04', '05', '06', '07', '08'];
const WEEK_REVIEW: WeekRow[] = [
{ icon: '🏃', label: '有氧总时长', value: '186 分钟', delta: '+12%' },
{ icon: '🏋️', label: '力量训练量', value: '9.4 吨', delta: '+8%' },
// ...共 6 行
];
近 12 周训练时长数据用于折线图 Canvas 绘制,数据点从 W1 到 W12 呈现整体上升趋势,体现训练坚持的进步轨迹。月度卡路里消耗数据映射柱状图高度,最大值基准 1400 千卡用于归一化。周报复盘清单包含有氧总时长、力量训练量、周消耗热量、深睡平均、静息心率、腿部酸痛度六项运动训练行业语义化指标。
五、工具函数层
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);
// ... 写入 44 字节 WAV 头
writeStr(0, 'RIFF');
view.setUint32(4, 36 + dataSize, true);
writeStr(8, 'WAVE');
// ... 写入 PCM 数据(正弦波 + 起音包络 + 自然衰减)
for (let i = 0; i < numSamples; i++) {
const t = i / sampleRate;
const env = Math.min(1, i / (sampleRate * 0.02));
const decay = Math.max(0, 1 - t / (durationMs / 1000));
const v = Math.sin(2 * Math.PI * freq * t) * 0.5 * env * decay;
view.setInt16(44 + i * 2, Math.round(v * 32767), true);
}
return buf;
}
这是 Notification Kit 沙箱铃声特性的核心函数。它手工构造标准 WAV 文件:44 字节文件头包含 RIFF 标识、WAVE 格式、PCM 编码(format code 1)、单声道、44100Hz 采样率、16bit 位深。音频数据部分使用正弦波公式 sin(2πft) 生成纯净音调,叠加起音包络(前 20ms 线性渐入)和自然衰减(按时长线性衰减至零),模拟真实铃声的音色特征。DataView 的 setUint16/setUint32/setInt16 方法第三参数 true 表示小端字节序,符合 WAV 规范。最终返回的 ArrayBuffer 直接写入沙箱文件,供通知 sound 字段引用。
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;
}
function deltaColor(delta: string): string {
if (delta.indexOf('+') === 0) { return COLORS.green; }
if (delta.indexOf('-') === 0) { return COLORS.red; }
return COLORS.text3;
}
三个配色函数将业务语义映射为颜色值:训练状态(已完成/进行中/待开始)分别对应青柠绿/活力橙/灰咖;课程难度(入门/进阶/高级)同理;周报环比变化(正增长/负下降/持平)通过前缀字符判断。这种集中式配色管理避免了硬编码散落在 ForEach 渲染逻辑中。
5.3 四列宫格分片函数
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;
}
该函数在常量期将课程列表预切分为二维数组(8 门课按每行 4 个切成 2 行),避免在 ForEach 渲染时反复切片。这是一种性能优化策略:常量预计算替代运行时切片,使 ForEach 直接遍历二维数组即可渲染宫格布局。从算法复杂度来看,chunkGrid 的时间复杂度为 O(n/size),空间复杂度为 O(n),在常量期执行一次后结果被 COURSE_ROWS 常量缓存,后续每次 Tab 切换渲染课程宫格时直接读取缓存数组,零运行时开销。
5.4 时间戳与文件大小辅助函数
function nowTime(): string {
const d = new Date();
const h = String(d.getHours()).padStart(2, '0');
const m = String(d.getMinutes()).padStart(2, '0');
const s = String(d.getSeconds()).padStart(2, '0');
return h + ':' + m + ':' + s;
}
function wavSizeText(durationMs: number): string {
const bytes = 44 + Math.floor(44100 * durationMs / 1000) * 2;
return Math.round(bytes / 1024).toString() + ' KB';
}
nowTime 为通知发布历史提供 HH:mm:ss 格式时间戳,使用 padStart(2, '0') 确保个位数补零。wavSizeText 根据 WAV 文件格式计算文件大小:44 字节头加上 PCM 数据量(采样率 × 时长秒数 × 2 字节/样本),结果四舍五入到 KB 单位。这两个函数都是纯函数——无副作用、确定性输出,适合在常量初始化和运行时计算中反复调用。
六、数据模型层
6.1 @Observed 训练条目模型
@Observed
export class WorkoutItem {
name: string; // 训练项目名
duration: string; // 时长
cal: string; // 消耗卡路里
status: string; // 状态:已完成/进行中/待开始
constructor(name: string, duration: string, cal: string, status: string) {
this.name = name;
this.duration = duration;
this.cal = cal;
this.status = status;
}
}
@Observed 装饰器使 WorkoutItem 的字段变化能被 ArkUI 框架感知。当训练状态从"待开始"变为"进行中"或"已完成"时,绑定该数据的 UI 组件(状态指示点、状态胶囊)会自动刷新。Mock 数据包含 7 条晨训日课表,从晨间慢跑到泡沫轴筋膜松解,覆盖有氧、力量、核心、柔韧等多种训练类型。
从 ArkUI 的响应式原理来看,@Observed 装饰器在类级别注入了代理拦截:当 @State 修饰的 WorkoutItem[] 数组中某个元素的字段被修改时(如 item.status = '已完成'),框架会检测到变化并触发依赖该字段的 @Builder 片段重新渲染。注意直接修改数组元素属性不会触发数组本身引用变化,因此 ForEach 的列表项更新依赖于 @Observed 的字段级追踪能力。如果类未加 @Observed,则必须通过 this.workoutList = this.workoutList.slice() 创建新数组引用才能触发重绘,这会增加不必要的内存开销。
6.2 @Observed 提醒与铃声模型
@Observed
export class RemindItem {
time: string; // 提醒时间
title: string; // 提醒标题
repeat: string; // 重复类型
on: boolean; // 是否开启
}
@Observed
export class RingItem {
name: string; // 铃声名
file: string; // 沙箱文件名
freq: number; // 生成频率 Hz
duration: number; // 时长 ms
size: string; // 文件大小展示
inSandbox: boolean; // 是否已写入沙箱 EL1
}
RemindItem 是提醒 Tab 时间轴与弹窗系统共同绑定的业务实体,on 字段的 Toggle 开关变化直接驱动时间轴节点圆点的呼吸闪烁。RingItem 记录铃声的元数据,inSandbox 标记是否已通过 saveRingToSandbox 写入 EL1 沙箱目录,size 字段在导入沙箱后由 wavSizeText 计算更新。铃声库 Mock 数据包含 5 条预设铃声——从破晓鸟鸣(880Hz)到静夜风铃(520Hz),频率覆盖低中高三个频段,初始均未导入沙箱(inSandbox: false),需用户主动点击"生成到沙箱"按钮触发 EL1 文件写入。NoticeLog 模型的构造函数在实例化时自动调用 nowTime() 填充时间戳,保证每条发布历史都有精确到秒的记录时间。noticeLogs 数组使用 unshift 将最新记录插入头部,并在超过 6 条时 pop 移除尾部,实现固定容量的 FIFO+LIFO 混合队列。
6.3 @Observed 字幕场景模型
@Observed
export class CaptionScene {
scene: string; // 场景名
desc: string; // 场景说明
src: string; // 推荐源语言
tgt: string; // 推荐目标语言
}
字幕场景模型将运动训练行业的典型字幕使用场景预定义为数据条目,如"跟练口令双语"推荐英文源中英双语目标,"教练讲解转写"推荐中文源中文目标。点击场景卡时调用 applyScene 方法将推荐语言组合应用到当前配置。6 个场景覆盖了跟练口令、教练讲解、晨跑听力、冥想引导、赛事解说和康复指导六大运动训练行业典型场景,每个场景预绑定最优的源语言与目标语言组合,用户一键即可完成语言配置。当当前配置与场景推荐一致时,场景卡右侧的语言标签变为青柠绿高亮,提供视觉反馈确认当前匹配状态。
七、组件主体与状态架构
7.1 状态变量分层管理
组件主体声明了四大类状态变量,按职责清晰分层:
基础 UI 状态:currentTab 控制 Tab 切换路由,breath 布尔值每秒翻转驱动呼吸动画,timer 持有定时器句柄用于销毁清理。
弹窗状态(三态统一):addModal/editModal/delModal 三个布尔值分别控制新建/编辑/删除弹窗的显隐,editIdx/delIdx 记录当前操作的提醒索引。
特性 A 状态(Notification Kit):granted 记录通知授权状态,notifyId 自增避免通知覆盖,currentRingIdx 指向当前默认铃声,ringList 管理铃声库数据,noticeLogs 维护发布历史(最多 6 条),sandboxCount 统计已导入沙箱的铃声数量,genFreq/genDuration 绑定生成器参数。
特性 B 状态(Speech Kit):captionController 是 AICaptionController 实例,captionShown 通过 @Link 双向绑定到 AICaptionComponent.isShown,srcLang/tgtLang/captionSize/captionColor 对应四新字段,captionReady/captionErrMsg/captionFed 跟踪字幕服务状态。
特性 C 状态(Canvas 绘制):ringCtx/lineCtx 是 CanvasRenderingContext2D 实例(private 非 @State),ringReady/lineReady 标记画布就绪后才允许 setInterval 重绘。
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 中异步查询通知授权状态(isNotificationEnabled 返回 Promise),同时启动 1 秒间隔的呼吸动画定时器。定时器回调中先翻转 breath 状态触发声明式 UI 刷新(圆点透明度、柱高波动等),再手动调用 drawRing/drawLine 实现 Canvas 无闪烁重绘——因为 Canvas 绘制不依赖 @State 驱动,需手动调用才能刷新画布内容。aboutToDisappear 清理定时器防止内存泄漏。
这里有一个关键的 ArkUI 绘制机制需要说明:Canvas 组件的 onReady 回调是首次绘制的时机,但后续重绘不能依赖 @State 变化自动触发——Canvas 的 CanvasRenderingContext2D 是 private 非 @State 变量,其内部状态变化不会被框架追踪。因此呼吸动画的重绘策略是:setInterval 每秒翻转 breath 布尔值(这一步触发声明式 UI 刷新),然后在同一回调中手动调用 drawRing()/drawLine() 传入最新的 breath 值重绘 Canvas 画布。ringReady/lineReady 标记确保在 onReady 回调执行前不会过早调用绘制方法(此时画布上下文尚未初始化)。这种"声明式刷新 + 命令式重绘"的混合模式是 ArkUI 中 Canvas 动画的标准实现范式。
八、头部品牌区详解
@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 }) {
Row({ space: 4 }) {
Circle({ width: 5, height: 5 }).fill(this.granted ? COLORS.green : COLORS.red)
Text(this.granted ? '通知已授权' : '通知未授权').fontSize(9).fontColor(COLORS.sub)
}.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.backgroundColor(COLORS.dark).borderRadius(10)
// ... 当前铃声胶囊、字幕语言胶囊
}.width('100%')
}.width('100%').padding({ left: 14, right: 14, top: 12, bottom: 10 })
}
头部品牌区由两层构成。第一层是品牌标题行:左侧"体能星球"大标题配合 headerSub() 返回的 Tab 联动副标题(如"训练台 · 今日 7 项完成 5 项"),右侧是呼吸圆点——橙色圆点透明度随 breath 在 0.95 到 0.45 之间波动,模拟心跳脉搏效果。第二层是三特性状态胶囊行:通知授权状态胶囊(绿色已授权/红色未授权)、当前铃声名称胶囊(橙色圆点+铃声名)、字幕语言胶囊(蓝色圆点+源语言到目标语言)。三个胶囊使用 dark 底色 + borderRadius(10) 圆角,紧凑排列在同一行,让用户一眼掌握三大特性的当前配置状态。
headerSub() 方法通过 currentTab 索引返回对应的副标题文案,实现头部信息与内容区的联动——当用户切换 Tab 时,头部副标题即时更新为该 Tab 的概要描述。训练 Tab 返回"训练台 · 今日 7 项完成 5 项",课程 Tab 返回"课程库 · 8 门精品课任意练",提醒 Tab 返回"提醒台 · 训练节奏不错过",铃音 Tab 返回"铃声坊 · 沙箱自定义通知音",字幕 Tab 返回"AI 字幕 · 语言字号随心配",我的 Tab 返回"我的 · 体能进阶 Lv.6"。每个副标题都浓缩了该 Tab 的核心数据摘要,使用户在头部即可获取当前页面的关键信息。
九、各 Tab 页面深度分析
9.1 Tab0 训练:统计三格 + 进度环 + 折线图 + 清单
训练 Tab 是信息密度最高的页面,纵向排列四个区块:
统计三格:使用 statCell 公共 Builder 渲染三个等宽统计卡——今日消耗(826 千卡,活力橙)、连续打卡(26 天,青柠绿)、训练时长(52 分钟,湖蓝)。每格包含图标、标签、数值大字和单位,通过 layoutWeight(1) 等分宽度。
今日完成率进度环:这是特性 C 的 Canvas 绘制核心。Canvas(this.ringCtx) 宽高 180px,onReady 回调中设置 ringReady = true 并首次调用 drawRing()。drawRing 方法分四步绘制:背景环(dark 色深咖底环)→ 进度弧(从 12 点方向 -Math.PI/2 起笔,青柠绿,弧长 = 2π × 完成率 × breath波动系数 0.85~1.0)→ 中心百分比大字(26px 加粗暖白)→ 中心副标签"今日完成率"。breath 翻转时弧长在 85% 到 100% 之间微波动,形成呼吸般的动态效果。
近 12 周折线图:Canvas(this.lineCtx) 宽度 100%、高度 170px,drawLine 方法分五步绘制:背景网格(3 条横线)→ 青柠渐变填充区域(createLinearGradient 从 green 到 greenFade)→ 折线(青柠绿 2px 粗)→ 数据点(末点半径随 breath 在 3.5~4.5 波动)→ 横轴周标签(隔点绘制 W1/W3/…/W11 避免拥挤)。
今日训练清单:ForEach 遍历 workoutList 渲染 7 条训练条目,每条包含状态指示点(呼吸联动闪烁,进行中状态 breath 时透明度为 1 否则 0.55)、训练名称、时长与卡路里、状态胶囊。清单顶部的特性标注卡以活力橙文字"特性 C · Canvas 进度环核心调用"和 drawRing() 方法名标注,让开发者快速定位 Canvas 绘制的调用入口。ForEach 的第三个参数(键值生成器)使用 'workout-' + idx.toString() 确保列表项有稳定的唯一标识,避免 ArkUI 框架在数据变化时进行不必要的全量重渲染。
9.2 Tab1 课程:四列宫格 + 时长大卡横滑
课程 Tab 采用双区块布局:
四列宫格:使用常量期预计算的 COURSE_ROWS 二维数组,外层 ForEach 遍历行、内层 ForEach 遍历列,每格显示课程图标(24px emoji)、课程名(加粗)、难度级别(levelColor 配色)。8 门课程被切成 2 行 × 4 列,每格 layoutWeight(1) 等分宽度,borderRadius(12) 圆角卡片。
时长大卡横滑:Scroll 容器设置 scrollable(ScrollDirection.Horizontal) 实现横向滚动,内部 Row 排列 8 个宽 150px 的大卡,每卡显示图标(30px)、课程名、时长(18px 加粗活力橙)、难度+教练信息。横滑使用 scrollBar(BarState.Off) 隐藏滚动条,保持视觉简洁。注意横滑容器的 scrollable 是方法调用而非构造参数——ArkUI 中 Scroll 的滚动方向通过 scrollable(ScrollDirection.Horizontal) 方法链式设置,这是与 List 组件不同的 API 设计。大卡的 width(150) 固定宽度确保卡片在横滑时不会因弹性布局而压缩,padding(14) 和 borderRadius(12) 保持与宫格卡一致的视觉风格。
9.3 Tab2 提醒:时间轴 + 授权卡 + 发布历史
提醒 Tab 是 Notification Kit 特性的主要操作入口:
通知授权状态卡:根据 granted 状态显示不同文案与按钮。已授权时显示绿色圆点+“通知已授权”+"已开启"灰底按钮;未授权时显示红色呼吸圆点+“通知未授权”+"去授权"活力橙按钮,点击调用 requestAuth() 请求权限。
双操作按钮:左侧"新建提醒"灰底按钮触发 addModal = true 打开新建弹窗;右侧"发布训练提醒"活力橙按钮调用 publishRemindNotice() 发布携带沙箱铃声的通知。
训练提醒时间轴:核心是 ForEach 渲染 remindList 的固定行高布局。每行 height(72) 固定高度,分三列:时间列(50px 宽,显示时间和重复类型)、竖线轴列(14px 宽,节点圆点 + layoutWeight(1) 竖线)、提醒卡片列(layoutWeight(1) 填满剩余,含标题、状态文案、Toggle 开关、编辑和删除按钮)。竖线轴的 Column().width(2).layoutWeight(1) 在固定行高内自动填满,形成连贯的时间轴视觉。
通知发布历史:noticeLogs 为空时显示空状态提示,有数据时使用 List + ForEach + ListItem 渲染发布记录,每条含蓝色圆点、标题、正文和时间戳。List 组件的 space: 8 参数设置列表项间距,比 Column 的 space 更适合长列表场景——List 内部实现了虚拟化渲染,即使发布历史条目增多也不会影响滚动性能。发布历史顶部的"通知 id 自 N"标注让用户了解通知 id 的自增进度,因为相同 id 会覆盖上一条通知,自增策略确保每条通知独立展示。
9.4 Tab3 铃音:生成器 + 铃声库 + 沙箱预览
铃音 Tab 是沙箱自定义铃声的全功能管理中心:
当前默认铃声状态卡:显示当前选中铃声名称、已导入沙箱数量统计(如"已导入沙箱 3 / 5 个"),让用户了解铃声配置全貌。
正弦波生成器:Slider 滑块控制频率(440~1320Hz,步进 20),三档时长预设(900/1200/1500ms)以 chips 形式展示,选中态活力橙底色。点击"生成"按钮调用 genCustomRing() 将参数传入 buildWavBytes 生成 WAV 数据并写入沙箱。
铃声库列表:ForEach 遍历 ringList,每条铃声显示名称(当前默认铃声前加 ✓ 并用活力橙高亮)、频率/时长/大小信息、沙箱导入状态。每条提供两个操作按钮:"生成到沙箱"调用 importRing(idx) 生成并写入 EL1,"设为默认铃声"调用 setCurrentRing(idx)(未导入时自动先导入再设为默认)。铃声库底部的特性标注卡以"特性 A · NotificationRequest.sound"和 publishNotice() 方法名标注核心调用入口。importRing 方法在写入成功后更新 ring.inSandbox = true、sandboxCount++ 并调用 this.ringList = this.ringList.slice() 触发数组引用变化使 ForEach 重新渲染。setCurrentRing 方法的智能之处在于:如果目标铃声尚未导入沙箱,会先自动调用 importRing 完成导入再设为默认,确保发布通知时 sound 字段指向的沙箱文件一定存在。
9.5 Tab4 字幕:AI 字幕五区块设置
字幕 Tab 是 Speech Kit 四新字段的完整配置面板:
实时预览卡:AICaptionComponent 组件通过 isShown: this.captionShown(@Link 双向绑定)控制显隐,controller 传入 captionController 实例,options 传入 buildCaptionOptions() 动态组装的配置对象。下方提供"开启/隐藏字幕"按钮和"写入演示音频"按钮——后者调用 feedAudioStream() 生成 640 字节 PCM 块(16kHz/16bit/单声道 ≈ 20ms)并通过 writeAudio 写入字幕控制器。isShown 使用 @Link 双向绑定而非 @Prop 单向传递,是因为 AICaptionComponent 内部也可能修改字幕显示状态(如用户点击字幕组件自带关闭按钮),双向绑定确保父子组件状态同步。预览卡右上角根据 captionReady 显示"已就绪"(青柠绿)或"初始化中"(灰咖)状态标签,若 captionErrMsg 非空则在卡片底部以警示红显示错误信息。
语言设置联动卡:源语言 chips(中文/英文)点击调用 switchSourceLang,该方法实现联动逻辑——中文源时目标语言锁定为 zh 并显示"中文(锁定)"灰底标签;英文源时目标语言 chips 展示中文/英文/中英双语三选项,默认切换为中英双语。
字号四档卡:ForEach 遍历 SIZE_OPTIONS 渲染四档字号 chips,选中态活力橙底色 + onMain 深字。点击设置 captionSize 为对应的 AICaptionFontSize 枚举值。
五色预设卡:ForEach 遍历 CAPTION_FONT_COLORS 五色常量,每色用 Circle 渲染,选中色叠加 ✓ 标记。Stack 容器实现圆形与对勾的层叠居中。
字幕场景卡列表:ForEach 遍历 sceneList 渲染 6 个场景卡,每卡显示场景图标、名称、说明和推荐语言组合。点击调用 applyScene(scene) 将推荐源语言和目标语言应用到当前配置。
9.6 Tab5 我的:身份大卡 + 柱状图 + 周报复盘
我的 Tab 聚焦用户身份与长期趋势:
身份渐变大卡:使用 linearGradient({ angle: 135, colors: [[COLORS.gradA, 0.0], [COLORS.gradB, 1.0]] }) 实现从深橙棕到活力橙的 135 度渐变背景。卡片内含跑步图标、用户名"晨曦跑者 · Leo"、等级与坚持周数,以及三格统计(累计里程/周均消耗/获得徽章),文字使用 onMain 深色保证渐变背景上的可读性。
近 6 个月柱状图:chartCard Builder 使用传统 Column + ForEach 渲染柱状图(非 Canvas)。每根柱子高度由 barHeight(v) 方法计算——v / MONTH_MAX * 78 归一化映射到 78px 最大高度,breath 翻转时柱高乘以 0.93 实现呼吸波动。最新月份(第 6 根)使用 orange 活力橙,其余使用 orangeD 深橙,形成"当前月份高亮"的视觉焦点。Row 容器设置 alignItems(VerticalAlign.Bottom) 使柱子底部对齐。
周报复盘清单:ForEach 遍历 WEEK_REVIEW 常量渲染 6 行运动指标,每行包含图标、指标名、本周结果值和环比变化(deltaColor 配色:上升青柠绿/下降警示红/持平灰咖)。环比变化列使用固定 width(52) 和 textAlign(TextAlign.Center) 保证所有变化值右对齐,视觉上形成数据列的整齐排列。6 项指标涵盖了有氧总时长、力量训练量、周消耗热量、深睡平均、静息心率、腿部酸痛度,从训练量到恢复质量全方位覆盖运动健康管理的关键维度。
十、图表卡片与 Canvas 绘制
10.1 进度环 drawRing 详解
drawRing() {
const ctx = this.ringCtx;
ctx.antialias = true;
const cx = 90, cy = 90, r = 66;
const progress = this.todayDone / this.todayTotal;
const breathVal = this.breath ? 1.0 : 0.85;
// 背景环
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.strokeStyle = COLORS.dark;
ctx.lineWidth = 12;
ctx.stroke();
// 进度弧(从12点方向起笔)
ctx.beginPath();
ctx.arc(cx, cy, r, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * progress * breathVal);
ctx.strokeStyle = COLORS.green;
ctx.lineWidth = 12;
ctx.lineCap = 'round';
ctx.stroke();
// 中心百分比大字 + 副标签
ctx.fillStyle = COLORS.title;
ctx.font = 'bold 26px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(Math.round(progress * 100).toString() + '%', cx, cy + 2);
ctx.font = '10px sans-serif';
ctx.fillStyle = COLORS.text3;
ctx.fillText('今日完成率', cx, cy + 24);
}
进度环的呼吸动画通过 breathVal 系数实现:breath 为 true 时弧长乘以 1.0(完整弧长),为 false 时乘以 0.85(弧长缩短 15%),每秒翻转一次形成"呼吸"般的弧长伸缩效果。lineCap = 'round' 使弧线两端呈圆头,视觉更柔和。弧线起笔角度 -Math.PI / 2 对应 12 点钟方向,顺时针绘制到 -Math.PI / 2 + 2π × progress × breathVal。
10.2 折线图 drawLine 详解
折线图绘制分五步完成:背景网格用 3 条横线划分数据区域;渐变填充区域通过 createLinearGradient 创建从青柠绿到几乎透明的渐变,用 closePath 闭合路径后 fill 填充;折线通过 moveTo + lineTo 连接 12 个数据点;数据点用 arc 绘制圆形,末点半径随 breath 在 3.5~4.5px 间波动;横轴标签隔点绘制(i += 2)避免文字拥挤。数据点的 Y 坐标计算公式为 baseY - (LINE_DATA[i] / max) * plotH,将分钟值映射到画布像素坐标。
从 Canvas 坐标系角度分析,画布原点 (0,0) 在左上角,X 轴向右递增、Y 轴向下递增。pad = 26 是四周留白,baseY = h - pad - 14 是基线 Y 坐标(减去额外的 14px 为横轴标签预留空间)。数据点的 X 坐标按等间距分布:stepX = (w - pad * 2) / (n - 1),12 个数据点之间有 11 个间距。渐变填充区域的路径从左下角 pad, baseY 开始,依次连接各数据点,最后回到右下角 w-pad, baseY 闭合,形成折线下方的填充区域。数据点用 fillStyle = COLORS.card 填充内圆(与卡片底色一致形成"空心"效果),再用 strokeStyle = COLORS.green 描边青柠绿外环,lineWidth = 1.5 使数据点在折线上清晰可见。
十一、底部 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)
}
底部导航栏使用 Row 容器配合 ForEach 渲染 6 个 Tab,每个 Column 通过 layoutWeight(1) 等分宽度。选中 Tab 的图标透明度随 breath 在 1.0 到 0.75 间波动(呼吸闪烁),标签颜色切换为 tabOn 活力橙;未选中 Tab 的标签使用 text3 灰咖。点击事件直接设置 currentTab 索引,触发内容区 if-else 分支重新渲染对应 Tab 的 Builder。backgroundColor(COLORS.card) 使底部栏与内容区有视觉分隔。
从交互体验角度分析,底部 6 Tab 单排设计在窄屏设备上每个 Tab 约占 16.6% 屏宽,图标 20px 加标签 9px 的紧凑布局确保 6 个 Tab 在标准手机宽度下不被压缩。ForEach 的键值生成器 'tab-' + tab.label 使用 Tab 标签作为唯一标识,由于 Tab 列表是静态常量不会变化,键值始终稳定。Tab 切换时不使用动画过渡——currentTab 的变化直接触发 if-else 分支的条件渲染,ArkUI 框架会在同一渲染周期内完成旧 Tab Builder 的卸载和新 Tab Builder 的挂载,实现即时切换无白屏。
十二、弹窗系统
12.1 全屏遮罩层
@Builder
modalOverlay(onClose: () => void) {
Column().width('100%').height('100%').backgroundColor(COLORS.mask)
.onClick(() => { onClose(); })
}
modalOverlay 是所有弹窗的公共遮罩层,使用 rgba(0,0,0,0.6) 半透明黑色覆盖全屏,点击遮罩区域触发 onClose 回调关闭弹窗。这种"遮罩+内容"的 Stack 层叠模式使弹窗居中显示且背景内容半隐可见。
12.2 新建提醒弹窗 panelAdd
新建弹窗包含三个表单字段:提醒时间(TextInput 绑定 formTime)、提醒标题(TextInput 绑定 formTitle)、重复类型(四档 chips:每天/工作日/周末/单次,绑定 formRepeat)。底部"取消"按钮调用 onClose 关闭弹窗,“创建"按钮调用 saveRemind()——该方法用空字段默认值兜底(时间为空用"07:00”,标题为空用"训练提醒"),向 remindList 推入新 RemindItem 并将 todayTotal 加一,最后清空表单并关闭弹窗。
12.3 编辑提醒弹窗 panelEdit
编辑弹窗的结构与新建弹窗一致,但表单字段绑定 editTime/editTitle/editRepeat,且在 openEditRemind(idx) 时回填当前提醒的值。updateRemind() 方法对空输入做保护(不覆盖原值),更新后调用 this.remindList = this.remindList.slice() 触发数组引用变化使 ForEach 重新渲染。
12.4 删除确认弹窗 panelDel
删除弹窗最简洁,仅显示确认文案和取消/确认按钮。确认按钮使用 red 警示红底色,点击调用 delRemind() 从 remindList 中 splice 移除指定索引项,并将 todayTotal 减一(不低于 0)。
三个弹窗均使用 Stack 容器,底层是 modalOverlay(onClose),上层是 width('78%') 的居中内容卡,通过 alignContent(Alignment.Center) 实现内容居中。弹窗显隐由对应的布尔状态变量(addModal/editModal/delModal)在 build() 根构建中条件渲染。
从弹窗架构设计角度分析,三个弹窗共享 modalOverlay 遮罩层 Builder 和 Stack + Column 层叠结构,但各自绑定的表单状态变量独立分离——新建弹窗绑定 formTime/formTitle/formRepeat,编辑弹窗绑定 editTime/editTitle/editRepeat,删除弹窗无需表单仅传递确认回调。这种"共享结构、独立状态"的设计模式既保证了视觉一致性又避免了状态混淆。onClose 回调参数是 () => void 类型的箭头函数,在根构建 build() 中传入时绑定各自的状态变量(如 () => { this.addModal = false }),确保关闭操作精确作用于对应弹窗。弹窗内容卡使用 width('78%') 而非固定像素,使其在不同屏幕宽度下自适应居中,borderRadius(14) 比普通卡片(12px)更大的圆角强化弹窗的层级感。
十三、Notification Kit 沙箱铃声通知链路
13.1 沙箱音频写入
saveRingToSandbox(fileName: string, freq: number, durationMs: number): string {
const hostCtx = this.getUIContext().getHostContext() as common.UIAbilityContext;
if (!hostCtx) { return ''; }
const appCtx = hostCtx.getApplicationContext();
appCtx.area = contextConstant.AreaMode.EL1; // 必须在 EL1 沙箱下
const dir = appCtx.filesDir;
const path = dir + '/' + fileName;
try {
const data = buildWavBytes(freq, durationMs);
const file = fs.openSync(path, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY | fs.OpenMode.TRUNC);
fs.writeSync(file.fd, data);
fs.closeSync(file);
} catch (e) { /* 沙箱写入失败时忽略 */ }
return path;
}
该方法是 Notification Kit 特性的核心链路起点。首先通过 getUIContext().getHostContext() 获取宿主上下文(getContext(this) 已废弃必须用此方式),判空兜底。然后设置 appCtx.area = contextConstant.AreaMode.EL1 确保在 EL1 沙箱区域操作,获取 filesDir 目录路径。调用 buildWavBytes 生成 WAV ArrayBuffer 后,用 fs.openSync 以 CREATE|WRITE_ONLY|TRUNC 模式打开文件,writeSync 写入数据,closeSync 关闭文件句柄。返回的沙箱路径供后续 fileUri.getUriFromPath 转换为 URI。
13.2 发布携带沙箱铃声的通知
publishNotice(title: string, text: string) {
const ring = this.ringList[this.currentRingIdx];
if (!ring.inSandbox) { this.importRing(this.currentRingIdx); }
const hostCtx = this.getUIContext().getHostContext() as common.UIAbilityContext;
const appCtx = hostCtx.getApplicationContext();
appCtx.area = contextConstant.AreaMode.EL1;
const sandboxPath = appCtx.filesDir + '/' + ring.file;
const uri = fileUri.getUriFromPath(sandboxPath);
const soundVal = 'uri::' + uri;
const request: notificationManager.NotificationRequest = {
id: this.notifyId++,
notificationSlotType: notificationManager.SlotType.SOCIAL_COMMUNICATION,
content: { /* ... */ },
sound: soundVal // 沙箱铃声 URI
};
notificationManager.publish(request).then(() => {
this.addNoticeLog(title, text);
}).catch((err: BusinessError) => {
this.addNoticeLog(title, '发布失败 ' + err.code + ':' + err.message);
});
}
通知发布的完整链路:取当前默认铃声(未导入沙箱时先自动导入)→ 获取 EL1 沙箱路径 → fileUri.getUriFromPath 将沙箱路径转为 URI → 拼接 'uri::' 前缀作为 sound 字段值 → 构造 NotificationRequest(id 自增避免覆盖、SlotType.SOCIAL_COMMUNICATION 社交通知类型、additionalText 标注"沙箱自定义")→ notificationManager.publish 发布并处理成功/失败回调。这是 6.1.1 的新特性——原来 sound 字段只能传 rawfile 文件名,现在支持沙箱 URI,使应用可以动态生成铃声而无需预置资源文件。
13.3 通知授权流程
requestAuth() {
const hostCtx = this.getUIContext().getHostContext() as common.UIAbilityContext;
if (!hostCtx) { return; }
notificationManager.requestEnableNotification(hostCtx).then(() => {
this.granted = true;
}).catch((err: BusinessError) => {
// 曾拒绝时返回 1600004,拉起通知设置页引导用户手动开启
notificationManager.openNotificationSettings(hostCtx).then(() => {
}).catch(() => { this.granted = false; });
});
}
requestEnableNotification 必须传入 context 参数(无参版本已废弃),首次调用弹出系统授权框。若用户曾拒绝授权,返回错误码 1600004,此时调用 openNotificationSettings 拉起系统通知设置页引导用户手动开启,实现二次授权的优雅降级。
从授权流程的状态转换来看,granted 状态有三个转换路径:aboutToAppear 中 isNotificationEnabled 异步查询初始化为 true/false;用户点击"去授权"按钮调用 requestAuth 成功后设为 true;requestEnableNotification 失败后 openNotificationSettings 成功拉起设置页但无法感知用户是否在设置页中实际开启了通知(该回调无参数),因此 granted 不在此处更新——用户需返回应用后 aboutToAppear 或手动操作时才会重新查询。这种设计虽然存在状态同步延迟,但保证了授权流程的非阻塞性——不会因为用户在设置页停留而卡死应用。publishNotice 的 catch 回调捕获 BusinessError 后通过 addNoticeLog 记录失败信息(含错误码和消息),使用户在发布历史中看到失败原因而非无响应。
十四、Speech Kit AI 字幕配置链路
14.1 AICaptionOptions 四新字段组装
buildCaptionOptions(): AICaptionOptions {
const opts: AICaptionOptions = {
initialOpacity: 1,
sourceLanguage: this.srcLang, // 源语言
targetLanguage: this.tgtLang, // 目标语言
fontSize: this.captionSize, // 字号枚举
fontColor: this.captionColor, // 字体颜色
onPrepared: () => {
this.captionReady = true;
this.captionErrMsg = '';
},
onError: (error: BusinessError) => {
this.captionErrMsg = '字幕服务异常 ' + error.code + ':' + error.message;
}
};
return opts;
}
buildCaptionOptions 将四个 @State 变量动态组装为 AICaptionOptions 对象,每次 AICaptionComponent 渲染时调用。onPrepared 回调在字幕服务就绪时置 captionReady = true,onError 回调在出错时记录错误信息。四个新字段中 fontSize 是 AICaptionFontSize 枚举类型(非 number),fontColor 接收 ResourceColor 类型(此处传字符串色值)。
14.2 音频流写入演示
feedAudioStream() {
const block = new Uint8Array(640);
for (let i = 0; i < 640; i += 2) {
const t = (i / 2) / 16000;
const v = Math.round(Math.sin(2 * Math.PI * 440 * t) * 6000);
block[i] = v & 0xFF;
block[i + 1] = (v >> 8) & 0xFF;
}
try {
const audioData: AudioData = { data: block };
this.captionController.writeAudio(audioData);
this.captionFed++;
} catch (e) {
this.captionErrMsg = '音频写入失败';
}
}
该方法演示了 writeAudio 的调用方式:生成 640 字节 PCM 块(16kHz/16bit/单声道 ≈ 20ms 音频),正弦波 440Hz 为 A4 音高。每个 16bit 采样值按小端字节序写入两个字(低字节 v & 0xFF,高字节 (v >> 8) & 0xFF)。AudioData 对象包裹 Uint8Array 传给 captionController.writeAudio,计数器 captionFed 递增显示已写入块数。实际应用中应从麦克风采集音频流持续写入。
从音频流处理的工程角度分析,640 字节块的大小选择并非随意——16kHz 采样率下 640 字节等于 320 个采样点,即 20ms 的音频数据。这是语音识别引擎常用的最小处理单元,太小的块会增加 API 调用开销,太大的块会增加延迟。writeAudio 的调用在 try-catch 中执行,捕获可能的序列化异常或控制器状态异常(如字幕服务未就绪时写入)。captionFed 计数器为用户提供"已写入 N 块"的视觉反馈,按每块 20ms 计算,50 块即 1 秒音频,用户可据此判断演示音频的写入进度。switchSourceLang 方法的联动逻辑体现了 AI 字幕的语言约束:中文作为源语言时,目标语言只能是中文(无翻译方向),因为 AI 字幕的翻译能力仅支持以英文为源语言的目标语言切换。
十五、功能模块对比表
| 模块 | 核心技术 | 关键接口/方法 | 数据模型 | 交互特色 |
|---|---|---|---|---|
| 训练 Tab | Canvas 绘制 | drawRing / drawLine | WorkoutItem | 进度环呼吸联动 + 折线图渐变填充 |
| 课程 Tab | 弹性布局 | chunkGrid 预切分 | CourseItem | 四列宫格 + 横滑大卡双视图 |
| 提醒 Tab | Notification Kit | isNotificationEnabled / requestEnableNotification | RemindItem | 固定行高时间轴 + Toggle 开关 |
| 铃音 Tab | Notification Kit | buildWavBytes / saveRingToSandbox / fileUri.getUriFromPath | RingItem | 正弦波生成器 + Slider 频率调节 |
| 字幕 Tab | Speech Kit | AICaptionComponent / writeAudio | CaptionScene | 四新字段配置 + 五区块面板 |
| 我的 Tab | Canvas + 渐变 | barHeight / linearGradient | WeekRow | 身份渐变大卡 + 柱状图呼吸波动 |
| 弹窗系统 | 条件渲染 | panelAdd / panelEdit / panelDel | RemindItem | Stack 遮罩 + 三态统一 |
| 头部品牌 | @Builder | headerMain / headerSub | — | Tab 联动副标题 + 三特性胶囊 |
十六、总结与展望
本平台以"运动健康管理"为业务场景,通过 HarmonyOS ArkUI 框架的系统级能力深度融合,构建了一个功能完整、视觉统一的运动健身应用。在技术架构上,三大前沿特性各司其职:Notification Kit 的沙箱自定义铃声能力突破了传统通知铃声只能使用预置资源的限制,使运动提醒铃声可动态生成、个性化定制;Speech Kit 的 AI 字幕四新字段实现了跨语言训练课程的实时字幕转写与翻译,支持中英双语对照的复合场景;Canvas 绘制通过 drawRing 进度环和 drawLine 折线图实现了训练数据的可视化呈现,配合 breath 呼吸动画使数据展示更具生命力。
在工程实现上,本平台展现了几个值得借鉴的设计模式。其一是状态分层管理——将基础 UI 状态、弹窗状态、三大特性状态、Canvas 状态按职责分组声明,避免了状态变量的混乱堆叠。其二是常量预计算策略——COURSE_ROWS 二维数组在常量期完成切片,避免 ForEach 渲染时的运行时开销。其三是 breath 呼吸动画的统一驱动——一个 1 秒定时器同时驱动 Canvas 重绘、圆点透明度波动、柱高微波动和图标闪烁,用最小的状态变量实现最多的动画效果。其四是弹窗三态统一架构——新建/编辑/删除三个弹窗共享 modalOverlay 遮罩层和 Stack 层叠结构,通过独立的布尔状态变量条件渲染,代码结构清晰且易于维护。
展望未来,本平台可在以下方向持续演进:第一,接入 Sensor 服务实现实时心率、步频、配速等运动传感器数据,替代当前的 Mock 常量数据,使训练清单和统计图表反映真实运动状态。第二,引入分布式能力实现手机与手表的跨设备训练数据同步,利用 ArkUI 的分布式 UI 框架在手表端展示精简版训练面板。第三,扩展 AI 字幕的场景识别能力,结合 writeAudio 持续音频流写入实现训练过程中的实时语音教练反馈。第四,将 Canvas 绘制升级为更丰富的数据可视化体系,引入雷达图、热力图等多维度运动表现分析视图。第五,深化通知能力,利用 Notification Kit 的通知分组、快捷回复等高级特性,构建更智能的训练提醒体系。随着 HarmonyOS 的持续演进和 ArkUI 框架能力的不断扩展,运动健康管理应用将拥有更广阔的创新空间。
附录: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 应用的功能开发。
本文基于 DevEco Studio 6.1.1 Release 版本编写,不同版本界面可能存在细微差异。
更多推荐



所有评论(0)