一、技术前言

在全民健康意识觉醒的当下,运动健身已从"偶尔打卡"进化为"数据驱动的持续生活方式"。从晨间慢跑的配速记录到力量训练的负荷追踪,从训练提醒的精准触达到 AI 字幕的跨语言跟练辅助,一款优秀的运动健康管理应用需要在"数据可视化、通知触达、语音交互"三大维度同时发力。传统运动类应用往往存在三大短板:训练数据图表静态僵化缺乏实时反馈、通知铃声千篇一律无法区分运动场景、跟练字幕语言配置僵化无法适配双语场景。

HarmonyOS ArkUI 框架为这些问题提供了从底层到表现层的系统性解决方案。ArkUI 基于 TypeScript 扩展的 ArkTS 语言,通过 @Entry@Component 装饰器声明页面入口与可复用组件,通过 @State@Observed 等状态管理装饰器实现数据驱动的响应式渲染,通过 @Builder 方法将复杂的 UI 结构拆分为可组合、可维护的构建块。ArkUI 的声明式范式意味着开发者只需描述"界面在当前状态下的样子",框架自动处理状态变化到界面更新的差量渲染,无需手动操作 DOM 节点。这种架构天然适合运动场景中"实时数据流—视觉反馈—用户交互"紧密耦合的需求——心率变化即刻反映到进度环弧长、训练完成即刻触发通知铃声、音频流写入即刻更新字幕显示。

本平台深度融合了 HarmonyOS 6.1.1 的三大前沿特性。Notification Kit 实现了沙箱自定义铃声链路——通过 buildWavBytes 函数生成正弦波 PCM 音频数据(44 字节 WAV 头 + 16bit 单声道采样),写入 EL1 沙箱 filesDir 目录,再以 'uri::' + fileUri.getUriFromPath(沙箱路径) 拼接填入 NotificationRequest.sound 字段,让训练提醒拥有从破晓鸟鸣到冲刺号角的差异化铃声。Speech Kit 提供了 AI 字幕四新字段能力——AICaptionComponent 组件的 AICaptionOptions 配置对象新增 sourceLanguage(源语言)、targetLanguage(目标语言)、fontSize(字号枚举)、fontColor(字体颜色)四个配置维度,配合 AICaptionController.writeAudio 以 640 字节 PCM 块写入音频流,实现跟练口令的实时字幕转写与跨语言翻译。Canvas 绘制 通过 CanvasRenderingContext2DarclineTocreateLinearGradient 等 API,结合 setInterval 呼吸动画驱动,绘制今日完成率进度环与近 12 周训练时长折线图,让运动数据"活"起来。

二、整体架构流程图

生命周期管理

劲动日历主组件

headerMain 头部品牌区

内容区 6 Tab 切换

tabBar 底部导航

弹窗遮罩系统

训练 Tab
统计三格+进度环+折线图+训练清单

课程 Tab
四列宫格+横滑大卡

提醒 Tab
时间轴+授权卡+发布历史

铃音 Tab
正弦波生成器+铃声库+沙箱导入

字幕 Tab
实时预览+语言联动+字号五色+场景卡

我的 Tab
身份渐变卡+月度柱状+周报复盘

Notification Kit
沙箱自定义铃声

Speech Kit
AI字幕四新字段

Canvas 绘制
进度环+折线图

panelAdd 新建训练提醒

panelEdit 编辑训练提醒

panelDel 删除确认弹窗

aboutToAppear
查询通知授权+启动呼吸定时器

aboutToDisappear
清理定时器

整体架构以主组件为根节点,采用 Stack 容器实现页面层叠:底层是 Column 纵向布局的头部品牌区 + 内容滚动区 + 底部 Tab 栏,顶层是全屏弹窗遮罩。内容区通过 currentTab 状态索引在 6 个 @Builder 方法间切换,每个 Tab 拥有完全独立的布局结构。三大特性(Notification Kit 沙箱铃声、Speech Kit AI 字幕、Canvas 绘制)分别挂载在铃音/提醒、字幕、训练三个 Tab 上,但它们的状态变量统一声明在组件顶层,实现跨 Tab 数据共享。aboutToAppear 生命周期中查询通知授权状态并启动呼吸动画定时器,aboutToDisappear 中清理定时器,形成完整的生命周期管理闭环。

三、色彩体系设计

3.1 ColorPalette 接口定义

平台采用深色"夜跑黑"主题,通过 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'  // 橙底深字,保证按钮文字对比度
};

色彩设计遵循"运动能量"原则:橙绿蓝红四色分别对应"运动中活力/已达成合格/信息参考/危险警告"四种语义状态,使用户在深色环境下凭颜色即可快速识别运动进度与状态。身份渐变大卡的 linearGradientgradA 深橙棕到 gradB 实现沉稳到活力的渐变,折线图使用 greenFade 作为渐变终点色,让青柠绿数据区域从饱满到自然消融,底部 6 Tab 栏选中态使用 orange 活力橙高亮,未选中态使用 text3 灰咖弱化,形成强烈的视觉层次。

四、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 单排排列,从训练管理到个人中心覆盖运动健康全流程。每个 Tab 图标与功能语义紧密对应:💪 代表力量训练,🏋️ 代表课程库,⏰ 代表时间提醒,🔔 代表铃声管理,🗣 代表字幕语音,👤 代表个人中心。底部导航通过 ForEach 渲染,选中索引 currentTab 决定图标透明度与文字配色,点击即切换内容区布局。

4.2 AI 字幕语言与字号预设

interface LangOption {
  code: string;   // 语言码('zh' | 'en' | 'zh-en')
  name: string;   // 展示名
}

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: '中英双语' }
];

interface SizeOption {
  size: AICaptionFontSize;  // 字号枚举(非 number)
  name: string;             // 展示名
}

const SIZE_OPTIONS: SizeOption[] = [
  { size: AICaptionFontSize.SMALL, name: '小号' },
  { size: AICaptionFontSize.NORMAL, name: '标准' },
  { size: AICaptionFontSize.BIG, name: '大号' },
  { size: AICaptionFontSize.LARGE, name: '超大' }
];

源语言仅支持中文与英文两种取值。当源语言为中文时,目标语言锁定为 zh(中文源不支持翻译方向切换);当源语言为英文时,目标语言可选中文、英文或中英双语三种模式。字号使用 AICaptionFontSize 枚举而非数字类型,通过 SizeOption 接口封装枚举值与中文展示名的映射关系。字幕字体颜色预设包含经典白、暖黄、薄荷绿、天蓝、樱花粉五色,通过常量数组统一管理避免硬编码散落。

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_IDX: number[] = [0, 1, 2, 3, 4, 5];
const MONTH_NAME: string[] = ['03', '04', '05', '06', '07', '08'];
const MONTH_CAL: number[] = [860, 920, 1100, 1020, 1240, 1180];
const MONTH_MAX: number = 1400;

近 12 周训练时长折线数据呈整体上升趋势(180→245 分钟),反映用户运动习惯逐步养成。折线图横轴标签 W1~W12 在 Canvas 绘制时采用隔点绘制策略(i += 2),避免 12 个标签拥挤。月度卡路里柱状图展示近 6 个月消耗趋势,MONTH_MAX 为 1400 千卡,用作柱高归一化基准,最新月份(08 月)使用 orange 活力橙突出,其余月份使用 orangeD 深橙,形成时序对比。

五、工具函数

5.1 时间戳与 WAV 音频生成

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;
}

nowTime 函数为通知发布历史记录提供统一的时间戳格式化,使用 padStart(2, '0') 保证时分秒始终两位数,格式为 HH:mm:ss

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 头写入(RIFF/WAVE/fmt /data 四段)
  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;
}

buildWavBytes 是沙箱自定义铃声特性的核心函数,它从零构造完整的 WAV 音频字节流。44 字节头部严格遵循 RIFF/WAVE 格式规范:RIFF 标识、文件大小、WAVE 类型、fmt 子块、PCM 编码(format=1)、单声道(channels=1)、44100Hz 采样率、16bit 位深。音频数据段通过正弦波公式 Math.sin(2 * Math.PI * freq * t) 生成,叠加起音包络(前 20ms 线性渐入)和自然衰减包络,模拟真实乐器的 ADSR 包络特征,使生成的铃声听感自然不刺耳。

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;
}

statusColor 将训练状态映射为颜色:已完成用青柠绿表示达成,进行中用活力橙表示运动中,待开始用灰咖弱化。levelColor 将课程难度映射为颜色:入门用青柠绿降低心理门槛,进阶用活力橙暗示挑战,高级用警示红传达强度警示。两个函数共同构建了运动状态的"色彩语义系统"。

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;
}

chunkGrid 将一维课程列表按指定列数切分为二维数组。8 门课程按每行 4 个切分为 2 行 4 列的宫格布局。该函数在常量声明期预计算(const COURSE_ROWS = chunkGrid(COURSE_LIST, 4)),而非在 ForEach 渲染时动态切片,避免每次重绘都执行数组分割操作。

六、数据模型层

6.1 训练条目模型 WorkoutItem

@Observed
export class WorkoutItem {
  name: string;     // 训练项目名
  duration: string; // 时长
  cal: string;      // 消耗卡路里
  status: string;   // 状态:已完成/进行中/待开始
}

WorkoutItem 使用 @Observed 装饰器标记为可观察对象。当训练清单中某项的状态从"待开始"变为"进行中"再到"已完成"时,@Observed 确保依赖该属性的 UI 节点自动刷新。Mock 数据包含 7 条晨训日课表,从晨间慢跑到泡沫轴筋膜松解,覆盖有氧、核心、力量、拉伸全品类。

6.2 课程条目模型 CourseItem

@Observed
export class CourseItem {
  icon: string;     // 课程图标
  name: string;     // 课程名
  level: string;    // 难度:入门/进阶/高级
  duration: string; // 课程时长
}

CourseItem 同时服务于课程宫格和横滑大卡两种布局。8 门课程通过 chunkGrid 预切分为 2 行 4 列宫格,同时以原始一维数组形式驱动横滑大卡列表,一份数据两种视图。难度配色通过 levelColor 函数统一管理。

6.3 训练提醒模型 RemindItem

@Observed
export class RemindItem {
  time: string;    // 提醒时间
  title: string;   // 提醒标题
  repeat: string;  // 重复类型:每天/工作日/周末/单次
  on: boolean;     // 是否开启
}

RemindItem 是弹窗系统的核心业务实体。新建弹窗的表单字段(formTime/formTitle/formRepeat)绑定到该模型,编辑弹窗回填该模型的当前值,删除弹窗确认后从数组移除该实例。on 布尔字段驱动时间轴卡片的 Toggle 开关状态与节点圆点配色。6 条 Mock 数据从 06:30 晨跑唤醒到 21:00 体测复盘,构成一天完整的训练节奏。

6.4 铃声库与通知历史模型

@Observed
export class RingItem {
  name: string;       // 铃声名
  file: string;       // 沙箱文件名
  freq: number;       // 生成频率 Hz
  duration: number;   // 时长 ms
  size: string;       // 文件大小展示
  inSandbox: boolean; // 是否已写入沙箱 EL1
}

@Observed
export class NoticeLog {
  title: string;   // 通知标题
  text: string;    // 通知正文
  time: string;    // 发布时间
}

RingItem 模型记录每条铃声的频率、时长、沙箱导入状态。初始 5 条铃声均未导入沙箱(inSandbox: false),需用户点击"生成到沙箱"按钮触发 buildWavBytes 生成与 EL1 落盘。NoticeLog 在构造时自动调用 nowTime() 填充时间戳,发布历史采用 unshift 最新在前、最多保留 6 条的策略。

6.5 字幕场景模型 CaptionScene

@Observed
export class CaptionScene {
  scene: string;   // 场景名
  desc: string;    // 场景说明
  src: string;     // 推荐源语言
  tgt: string;     // 推荐目标语言
}

CaptionScene 将运动训练场景与推荐的语言组合绑定。6 条场景覆盖跟练口令双语、教练讲解转写、晨跑英语听力、冥想引导双语、赛事解说转写、康复指导跟读,点击场景卡即调用 applyScene 一键应用推荐的源语言与目标语言组合。

七、组件主体与状态管理

7.1 组件声明与状态分层

主组件通过 @Entry @Component 声明为页面入口,内部状态按功能域分为五层:

基础 UI 状态层currentTab 索引控制 6 Tab 切换,breath 布尔值每秒翻转驱动呼吸动画,timer 句柄管理定时器生命周期。

弹窗状态层addModaleditModaldelModal 三个布尔值分别控制三种弹窗的显隐,editIdxdelIdx 记录当前操作的提醒索引,实现"三态统一"的弹窗管理。

训练业务状态层workoutListremindList 承载训练清单与提醒时间轴数据,todayTotal/todayDone 驱动进度环分子分母,streakDays 连续打卡天数与 todayCal 今日消耗卡路里驱动统计三格。

特性 A 状态层(Notification Kit)granted 通知授权状态、notifyId 自增通知 ID、currentRingIdx 当前默认铃声索引、ringList 铃声库数据、noticeLogs 发布历史、sandboxCount 已导入沙箱计数、genFreq/genDuration 自定义生成器参数。

特性 B 状态层(Speech Kit)captionController 字幕控制器实例、captionShown 字幕显示状态(@Link 双向绑定)、srcLang/tgtLang 源/目标语言、captionSize 字号枚举、captionColor 字体颜色、captionReady 就绪标记、captionErrMsg 错误信息、captionFed 已写入音频块计数。

特性 C 状态层(Canvas)ringCtxlineCtx 两个 CanvasRenderingContext2D 上下文(private@State),ringReadylineReady 画布就绪标记,确保 onReady 回调后才允许 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),启动每秒翻转的呼吸动画定时器。定时器内部检查画布就绪标记后才调用绘制方法,避免画布未初始化时的空指针异常。aboutToDisappear 清理定时器,防止页面销毁后定时器继续执行导致内存泄漏。

八、头部详解

@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)
}

头部区域由两部分组成。上半行为品牌标题行:左侧 Column 承载"劲动日历"品牌标题与 headerSub() 返回的 Tab 联动副标题,右侧呼吸圆点随 breath 状态在 0.45~0.95 透明度间波动,象征运动脉搏。headerSub() 方法根据 currentTab 索引返回对应的一句话描述,如训练 Tab 返回"训练台 · 今日 7 项完成 5 项",课程 Tab 返回"课程库 · 8 门精品课任意练",实现头部文案随 Tab 切换动态变化。

下半行为三特性状态胶囊行,分别展示:通知授权状态(绿色已授权/红色未授权 + 文案)、当前默认铃声名称(活力橙圆点 + 铃声名)、字幕语言方向(湖蓝圆点 + srcLang→tgtLang 格式)。三个胶囊均使用 dark 次级底色与 10px 圆角,高度紧凑,让用户在任意 Tab 都能一眼掌握三大特性的运行状态。

九、各 Tab 分析

9.1 训练 Tab:统计三格 + 进度环 + 折线图 + 训练清单

训练 Tab 是数据可视化最密集的页面,自上而下分为四个区块:

统计三格:通过 statCell Builder 方法渲染三个等宽统计卡片,分别展示今日消耗(826 千卡·活力橙)、连续打卡(26 天·青柠绿)、训练时长(52 分钟·湖蓝)。每格采用"图标+标签"顶行、"数值大字+单位"底行的双层结构,数值使用 20px 粗体强化视觉冲击。

今日完成率进度环卡:左侧 180×180 的 Canvas 组件绑定 ringCtxonReady 回调置 ringReady=true 并首绘 drawRing()。右侧 Column 展示完成率标题、进度说明、已完成/待开始图例,以及"呼吸联动:弧长 0.85~1.0 波动"技术说明。进度环每秒由定时器重绘,breath 翻转时弧长系数在 0.85~1.0 间波动,模拟心跳脉冲效果。

近 12 周训练时长折线卡Canvas 绑定 lineCtx,高度 170px,onReady 后首绘 drawLine()。折线图绘制背景网格(3 条横线)、青柠渐变填充区域、折线本体、12 个数据点(末点半径随 breath 放大)和隔点横轴标签,构成完整的数据可视化呈现。

今日训练清单ForEach 遍历 workoutList,每行为一个训练项目卡片:左侧状态指示点(进行中时随 breath 闪烁),中部训练名+时长+卡路里,右侧状态胶囊。通过 statusColor 函数统一配色,已完成青柠绿、进行中活力橙、待开始灰咖弱化。

9.2 课程 Tab:四列宫格 + 横滑大卡

课程 Tab 采用双视图布局展示 8 门课程:

四列宫格:通过预计算的 COURSE_ROWS 二维数组驱动双层 ForEach(外层行、内列),每格展示课程图标(24px)、课程名(10px 粗体)、难度标签(8px,配色由 levelColor 决定)。宫格紧凑排列,适合快速浏览选择。

横滑大卡:使用 Scroll().scrollable(ScrollDirection.Horizontal) 实现横向滚动,每张大卡宽 150px,展示课程图标(30px)、课程名(13px 粗体)、时长大字(18px 活力橙粗体)、难度+教练带练说明。横滑大卡提供沉浸式的课程详情预览,与宫格形成"概览-详情"的浏览层次。

9.3 提醒 Tab:时间轴 + 授权卡 + 发布历史

提醒 Tab 是 Notification Kit 特性的主要操作界面,包含四个区块:

通知授权状态卡:左侧状态圆点(绿/红 + breath 闪烁),中部标题+说明文案,右侧"去授权"或"已开启"按钮。点击未授权状态的"去授权"按钮调用 requestAuth() 方法,该方法通过 getUIContext().getHostContext() 获取宿主上下文(替代已废弃的 getContext(this)),调用 requestEnableNotification 弹系统授权框,曾被拒绝(返回 1600004)时拉起 openNotificationSettings 通知设置页引导二次授权。

双按钮行:左侧"新建提醒"(dark 底色)打开 addModal 弹窗,右侧"发布训练提醒"(orange 底色)调用 publishRemindNotice() 发布携带沙箱铃声的系统通知。publishRemindNotice 方法遍历 remindList 找到第一条开启的提醒,拼装通知标题"劲动日历 · 训练提醒"与正文文案后调用 publishNotice 核心方法。publishNotice 方法是整个沙箱铃声链路的终点:首先取当前默认铃声并在未导入时自动导入沙箱,然后获取宿主上下文并设置 EL1 区域,通过 fileUri.getUriFromPath(沙箱路径) 将文件系统路径转换为 URI,再以 'uri::' + uri 格式拼接为 sound 字段值,最终构造 NotificationRequest 对象——包含自增 id(避免覆盖上一条通知)、notificationSlotType 设为 SOCIAL_COMMUNICATIONcontent 使用 NOTIFICATION_CONTENT_BASIC_TEXT 基础文本类型,以及最关键的 sound: soundVal 字段。发布成功后通过 addNoticeLog 记录历史,失败时(如未授权返回 1600004)记录错误码与消息。

训练提醒时间轴:每行固定高度 72px,由三列组成:时间列(50px 宽,居中对齐)、竖线轴列(14px 宽,圆点+layoutWeight 填满行高的竖线)、提醒卡片列(layoutWeight 占满剩余宽度)。圆点颜色随 on 状态变化,开启时随 breath 闪烁。卡片内含标题、状态说明、Toggle 开关、编辑和删除按钮。固定行高设计确保竖线轴与圆点在每行严格对齐,形成视觉连贯的时间轴效果。Toggle 组件使用 ToggleType.Switch 样式和 selectedColor(COLORS.orange) 活力橙选中色,onChange 回调调用 toggleRemind(idx, isOn) 更新对应提醒的开关状态,编辑按钮调用 openEditRemind(idx) 打开编辑弹窗并回填表单,删除按钮设置 delIdx 后打开删除确认弹窗。整个时间轴通过 ForEachidx 参数实现精确索引操作,键值生成器使用 'remind-' + idx.toString() 确保列表项稳定标识。

通知发布历史List + ForEach + ListItem 渲染 NoticeLog 数组,空列表时显示引导文案,有记录时每条展示蓝色圆点、标题、正文(最多 2 行省略)和时间戳。

9.4 铃音 Tab:生成器 + 铃声库 + 沙箱导入

铃音 Tab 是沙箱自定义铃声特性的核心操作界面:

当前默认铃声状态卡:展示当前选中铃声名称与沙箱导入进度(“已导入沙箱 N/M 个 · 发布通知时以其为 sound”)。

正弦波生成器卡Slider 控件调节频率(440~1320 Hz,步进 20),三档时长预设 chips(900/1200/1500ms)切换时长,底部"生成"按钮调用 genCustomRing() 创建自定义铃声并写入沙箱。生成器让用户可以像调音台一样自由创建个性化运动提醒铃声。

铃声库列表ForEach 遍历 ringList,每条展示铃声名(当前默认铃声前加 ✓ 活力橙高亮)、频率·时长·大小信息、沙箱导入状态胶囊。底部双按钮:“生成到沙箱”(或"重新生成到沙箱")调用 importRing(idx) 生成 WAV 并写入 EL1,"设为默认铃声"调用 setCurrentRing(idx) 更新 currentRingIdximportRing 方法内部调用 saveRingToSandbox,该方法通过 getUIContext().getHostContext() 获取宿主上下文后,设置 appCtx.area = contextConstant.AreaMode.EL1 确保在 EL1 沙箱区域操作,再通过 appCtx.filesDir 获取沙箱目录路径,以 fs.openSync 配合 CREATE | WRITE_ONLY | TRUNC 三个 OpenMode 枚举创建或覆盖文件,写入 buildWavBytes 生成的完整 WAV 字节流后必须 closeSync 关闭文件描述符。整个链路体现了 HarmonyOS 文件系统沙箱隔离的安全模型——应用只能在自己的 EL1 区域读写文件,外部应用无法访问,保证了用户自定义铃声数据的隐私安全。setCurrentRing 方法在设定默认铃声前会检查该铃声是否已导入沙箱,未导入时自动调用 importRing 先完成落盘,确保发布通知时 sound 字段指向的沙箱文件一定存在。

特性标识卡:底部 dark 底色小卡标注"特性 A · NotificationRequest.sound"与 publishNotice() 方法名,明确标识该 Tab 的技术归属。

9.5 字幕 Tab:AI 字幕五区块

字幕 Tab 是 Speech Kit AI 字幕特性的配置中心,由五个区块组成:

实时预览卡AICaptionComponent 组件通过 isShown: this.captionShown@Link 双向绑定)、controller: this.captionControlleroptions: this.buildCaptionOptions() 三参数初始化。下方"开启字幕"按钮切换 captionShown,"写入演示音频"按钮调用 feedAudioStream() 生成 640 字节 PCM 块(16kHz/16bit/单声道 ≈ 20ms)并调用 captionController.writeAudio 写入音频流。错误信息区域在 captionErrMsg 非空时显示红色错误文案。buildCaptionOptions 方法是 Speech Kit 特性的配置中枢,它将组件顶层的四个状态变量(srcLang/tgtLang/captionSize/captionColor)组装为 AICaptionOptions 对象,同时注册 onPrepared 回调(字幕服务就绪时置 captionReady=true 并清空错误信息)和 onError 回调(捕获 BusinessError 并填充错误码与消息到 captionErrMsg)。feedAudioStream 方法演示了音频流写入的完整过程:创建 640 字节 Uint8Array,以 440Hz 正弦波填充每个 16bit 采样值(低字节在前的小端序),封装为 AudioData 对象后调用 captionController.writeAudio,成功时递增 captionFed 计数器,失败时设置错误信息。每次写入的 640 字节在 16kHz 采样率下约对应 20 毫秒音频,按此速率持续写入即可驱动字幕服务实时转写与翻译。

语言联动卡:源语言 chips(中文/英文)点击调用 switchSourceLang(code)。中文源时目标语言锁定 zh(显示"中文(锁定)"灰咖标签 + "中文源仅支持目标 zh"说明);英文源时目标语言 chips 显示中文/英文/中英双语三选项。该联动逻辑确保 AICaptionOptions 不会传入非法语言组合导致初始化失败。

字号四档卡ForEach 遍历 SIZE_OPTIONS,选中项 orange 底色 + onMain 深字,未选中项 dark 底色 + sub 文字。点击切换 captionSize 枚举值。

五色预设卡ForEach 遍历 CAPTION_FONT_COLORS,每个颜色用 26px 圆形色块展示,选中时叠加 ✓ 标记。点击更新 captionColor,该值通过 buildCaptionOptions 传入 fontColor 字段。

字幕场景卡ForEach 遍历 sceneList,每张场景卡展示场景名、说明、推荐语言组合胶囊。点击调用 applyScene(scene) 一键应用推荐的源语言与目标语言组合,胶囊颜色在当前组合匹配时变青柠绿。

9.6 我的 Tab:身份渐变卡 + 月度柱状 + 周报复盘

我的 Tab 是个人数据汇总页面:

身份渐变大卡linearGradient({ angle: 135, colors: [[COLORS.gradA, 0.0], [COLORS.gradB, 1.0]] }) 实现 135 度角从深橙棕到深橙的渐变背景。卡内展示跑者昵称"晨曦跑者 · Leo"与等级"Lv.6 体能进阶 · 已坚持 26 周",下方三格小统计(累计里程/周均消耗/获得徽章)使用 idStat Builder 渲染。

月度柱状图chartCard Builder 方法渲染近 6 个月卡路里消耗柱状图。ForEach 遍历 MONTH_IDX,每根柱子高度由 barHeight(v) 计算——将月度值除以 MONTH_MAX 映射到 78px 基准高度,breath 翻转时在基准与 93% 间波动形成呼吸效果。最新月份柱子用 orange 活力橙突出,其余用 orangeD 深橙,底部标注月份名。

周报复盘清单ForEach 遍历 WEEK_REVIEW 常量数组,每行展示图标、指标名、本周值、环比变化。环比配色由 deltaColor 函数决定:上升青柠绿、下降警示红、持平灰咖。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;
  // ① 背景环(deep 色底环)
  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);
}

进度环绘制分四步:先画 dark 色完整背景环作为底,再从 -π/2(12 点方向)起笔画青柠绿进度弧,弧长等于 2π × progress × breathValbreathVal 在 0.85~1.0 间波动使弧长每秒"呼吸"一次。最后在圆心绘制百分比大字与"今日完成率"副标签。lineCap = 'round' 让进度弧两端呈圆头,视觉更柔和。

10.2 折线图绘制 drawLine

drawLine() {
  const ctx = this.lineCtx;
  ctx.antialias = true;
  const w = ctx.width > 0 ? ctx.width : 340;
  const h = 170, pad = 26, max = 320, n = LINE_DATA.length;
  const stepX = (w - pad * 2) / (n - 1);
  const plotH = h - pad * 2 - 14;
  const baseY = h - pad - 14;
  // ① 背景网格(3 条横线)
  // ② 青柠渐变填充区域(createLinearGradient)
  // ③ 折线本体(青柠绿 2px)
  // ④ 数据点(末点半径随 breath 放大 3.5~4.5)
  // ⑤ 横轴周标签(隔点绘制 i += 2)
}

折线图绘制分五步:先用 COLORS.line 画 3 条等间距横线作为背景网格,网格线间距为 plotH / 3,从上到下分别在 padpad + plotH/3pad + 2*plotH/3baseY 四个 Y 坐标绘制,为折线提供刻度参考;再用 createLinearGradient 创建从 COLORS.green(顶部饱满青柠绿)到 COLORS.greenFade(底部近乎透明的 rgba(156,208,79,0.06))的垂直渐变,填充折线下方区域形成"青柠渐隐"效果,这种从浓到淡的渐变让数据区域有"能量消散"的视觉感,比纯色填充更高级;然后以 2px 线宽绘制折线本体,遍历 12 个数据点,首点用 moveTo 起笔、后续点用 lineTo 连线;接着在每个数据点位置画 COLORS.card 底色圆 + 青柠绿描边,末点(W12,即最近一周)半径随 breath 在 3.5~4.5 间放大,形成"最新数据点脉动"效果,吸引用户关注最新训练数据;最后以 9px 字号隔点绘制横轴标签 W1~W12(i += 2 即 W1/W3/W5/W7/W9/W11),避免 12 个标签在有限宽度内拥挤重叠。整个折线图的 pad 边距为 26px,既为 Y 轴刻度和 X 轴标签留出空间,又保证折线不贴边绘制。

十一、底部 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)
}

底部 Tab 栏使用 Row + ForEach 渲染 6 个等宽 Tab。选中 Tab 的图标透明度随 breath 在 0.75~1.0 间波动(呼吸高亮),文字使用 tabOn 活力橙;未选中 Tab 图标固定 0.75 透明度,文字使用 text3 灰咖弱化。点击即更新 currentTab 索引,触发内容区条件分支重新渲染对应 Tab 布局。backgroundColor(COLORS.card) 使底部栏与卡片底色统一,视觉上与内容区形成自然分隔。

十二、弹窗系统

12.1 遮罩层 modalOverlay

@Builder
modalOverlay(onClose: () => void) {
  Column().width('100%').height('100%').backgroundColor(COLORS.mask)
    .onClick(() => { onClose(); })
}

modalOverlay 是所有弹窗共享的全屏遮罩层,使用 rgba(0,0,0,0.6) 半透黑色覆盖整个页面。点击遮罩区域触发 onClose 回调关闭弹窗,实现"点击外部关闭"的交互习惯。

12.2 三态弹窗

三种弹窗均采用 Stack 容器包裹 modalOverlay 与内容 Column 的结构,内容区宽 78%,居中对齐:

新建提醒弹窗 panelAdd:包含提醒时间 TextInput、提醒标题 TextInput、重复类型 chips(每天/工作日/周末/单次)和取消/创建双按钮。点击"创建"调用 saveRemind(),空字段用默认值兜底(时间默认 07:00,标题默认"训练提醒"),创建后 todayTotal 自增。

编辑提醒弹窗 panelEdit:结构与新建弹窗一致,但表单字段预填当前提醒的值(openEditRemind 方法回填 editTime/editTitle/editRepeat)。点击"保存修改"调用 updateRemind(),空输入不覆盖原值。

删除确认弹窗 panelDel:展示删除警告文案(“确认删除该条训练提醒吗?删除后时间轴将移除该节点,今日计划数同步减少,且不可恢复”),取消按钮 dark 底色,确认删除按钮 red 警示红底色。点击"确认删除"调用 delRemind(),从数组 splice 移除并 todayTotal 自减。

三种弹窗在 build() 根构建中通过条件渲染挂载:if (this.addModal) { this.panelAdd(...) },关闭时回调将对应布尔状态置 false,弹窗即从 Stack 中移除。这种条件渲染方式确保未激活的弹窗不会占用渲染树节点,避免了不必要的布局计算与内存消耗。每个弹窗接收一个 onClose: () => void 闭包参数,调用方在传入时绑定具体的关闭逻辑(如 () => { this.addModal = false; }),实现了弹窗关闭行为的外部控制,使弹窗 Builder 方法本身保持通用性。Stack 作为弹窗容器使用 alignContent(Alignment.Center) 让内容卡片在屏幕居中显示,遮罩层 ColumnStack 中先渲染(底层),内容 Column 后渲染(顶层),形成"遮罩在底、内容在顶"的视觉层次。

弹窗与时间轴的数据联动是整个提醒管理业务的核心流程:用户新建提醒时,saveRemind() 将新 RemindItem 推入 remindList 数组并同步递增 todayTotal;编辑提醒时,updateRemind() 根据 editIdx 精确更新对应条目的字段,空输入不覆盖原值防止误清空;删除提醒时,delRemind() 通过 splice 移除指定索引的条目并递减 todayTotal。三操作均通过 this.remindList = this.remindList.slice() 触发数组引用变更,确保 @State 装饰器检测到变化并驱动 ForEach 重新渲染时间轴。

十三、功能模块对比表

功能模块技术/Kit核心接口/方法关键状态变量数据模型呼吸联动方式
训练统计Canvas 绘制drawRing()todayDone/todayTotal/breathWorkoutItem弧长 0.85~1.0 波动
周折线图Canvas 绘制drawLine()lineReady/breathLINE_DATA[]末点半径 3.5~4.5 放大
月柱状图ArkUI ColumnbarHeight()breathMONTH_CAL[]柱高基准~93% 波动
通知授权Notification KitisNotificationEnabled/requestEnableNotificationgranted授权圆点闪烁
沙箱铃声Notification Kit + CoreFileKitbuildWavBytes/saveRingToSandbox/publishNoticeringList/currentRingIdx/sandboxCountRingItem
通知发布Notification Kitpublish()notifyId/noticeLogsNoticeLog
AI 字幕Speech KitAICaptionComponent/writeAudiocaptionShown/srcLang/tgtLang/captionSize/captionColorCaptionScene
字幕音频流Speech KitcaptionController.writeAudiocaptionFed/captionErrMsgAudioData
提醒时间轴ArkUI ForEachtoggleRemind/openEditRemindremindList/editIdx/delIdxRemindItem节点圆点闪烁
课程宫格ArkUI ForEachchunkGridCOURSE_ROWSCourseItem
弹窗系统ArkUI StackpanelAdd/panelEdit/panelDeladdModal/editModal/delModalRemindItem
底部导航ArkUI ForEachtabBarcurrentTab/breathTabMeta选中图标透明度波动

深化解析:从代码结构到业务闭环

布局方式与数据流

运动健康页面围绕计划、训练、提醒、趋势和复盘形成路径。课程模型描述可选择的训练内容,训练记录表达一次真实执行,进度环、折线图和柱状图把结果转为可比较指标。分析时要说明动画如何由 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 ArkUI 框架在"数据可视化、通知触达、语音交互"三大维度的工程实践。通过 @Observed 装饰器实现训练数据与 UI 的响应式绑定,通过 @Builder 方法将 6 个 Tab 的差异化布局拆分为可维护的构建块,通过 @State 状态分层管理让基础 UI、弹窗、业务、三大特性各自独立又协同工作。

在数据可视化方面,Canvas 绘制的进度环与折线图结合 setInterval 呼吸动画,让静态数据图表拥有了生命律动——弧长波动模拟心跳脉冲、末点放大标记最新数据、柱高交替形成节奏感。呼吸动画的巧妙之处在于它不依赖 animateTo 属性动画,而是通过定时器每秒翻转 breath 布尔值,在翻转时手动调用 drawRing()drawLine() 重绘画布,这种方式虽然需要手动管理重绘时机,但可以精确控制每个图表元素的波动参数——进度环弧长系数 0.85~1.0、折线末点半径 3.5~4.5、柱高基准到 93%——实现各元素差异化呼吸。在通知触达方面,buildWavBytes 从零构造 WAV 音频字节流,通过 EL1 沙箱落盘与 fileUri.getUriFromPath 路径转换,最终填入 NotificationRequest.sound 字段,实现了完全用户自定义的运动提醒铃声链路,从破晓鸟鸣到冲刺号角,每种运动场景都能拥有专属听觉标识。沙箱铃声链路的技术价值在于它打破了通知铃声只能使用系统预设或 rawfile 资源的限制,让用户可以像音乐制作人一样自由定义运动提醒的"听觉品牌"。在语音交互方面,AICaptionComponent 的四新字段让 AI 字幕突破了语言与视觉的双重壁垒——源语言与目标语言的联动逻辑确保配置合法性、字号枚举与五色预设让字幕适配不同运动场景的视觉需求、640 字节 PCM 音频块写入实现了实时跟练字幕的技术闭环。字幕场景卡的设计进一步降低了用户配置门槛:跟练口令双语、教练讲解转写、晨跑英语听力等预设场景一键应用推荐语言组合,让运动字幕从"技术配置"变为"场景选择"。

展望未来,该平台可在以下方向持续演进:一是引入 @ObservedV2@Trace 更细粒度的响应式追踪,替代当前 slice() 手动触发数组刷新的方式,减少不必要的全列表重绘;二是将 Canvas 绘制升级为 Shape 组件 + 属性动画的声明式图表,让呼吸动画由系统自动驱动而非手动 setInterval;三是结合 SensorService 采集真实运动数据替代 Mock 数据,让折线图与柱状图展示真实训练轨迹;四是扩展 Notification Kit 的 liveView 能力,在锁屏实时展示今日训练进度环;五是利用 AICaptionComponentonPrepared/onError 回调构建更完善的字幕服务状态机,实现自动重连与错误恢复。HarmonyOS 的全场景分布式能力还可将训练数据跨设备同步,让手表采集、手机分析、大屏复盘形成闭环,真正实现运动健康的全场景陪伴。

附录: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 将自动执行以下操作:

  1. 生成项目骨架(Stage 模型目录结构)
  2. 执行 ohpm install 安装依赖
  3. 运行 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.1Release✅ 已安装

界面顶部提示:“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 246.1.1.100Release✅ 已安装
API Version 236.1.0.28Beta1未安装
API Version 226.0.2.112Release未安装

安装路径示例:D:\DevTools\ArkUI-X\sdk

说明:ArkUI-X 允许开发者使用一套 ArkTS 主代码,同时构建多平台应用。如果仅开发 HarmonyOS 原生应用,无需额外安装 ArkUI-X SDK。

在这里插入图片描述


三、小结

步骤操作关键点
创建项目欢迎页 → 新建项目 → 选择 Empty Ability 模板 → 配置项目信息 → 完成使用 Stage 模型 + ArkTS 语言
查看 SDK设置 → HarmonyOS SDKSDK 已内置,无需手动安装
跨平台扩展设置 → ArkUI-X根据需要安装对应 API 版本

至此,DevEco Studio 的项目创建与 SDK 环境确认全部完成,可以开始 HarmonyOS 应用的功能开发。


Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐