HarmonyOS ArkUI 声动课堂演讲陪练平台实战笔记——从破冰术到大师结构的表达力阶梯
一、技术前言
在表达力教育与职场软技能培训行业,演讲口才训练正从"线下名师示范"向"数字化智能陪练"加速演进。从开场三分钟破冰术到 TED 式大师结构,从情绪感染力点燃到中英双语答辩训练,每一门课程背后都关联着不同的练习场景、评估维度和反馈机制。传统口才训练应用长期面临三大工程痛点:演讲跟拍时画面构图漂移导致演讲者出框、字幕语言切换割裂导致双语训练体验断层、练习评估维度扁平化导致进步方向不可量化。这三个痛点分别触及了相机能力、语音能力和可视化能力的底层边界。

HarmonyOS ArkUI 框架为这些痛点提供了系统级的声明式解决方案。ArkUI 的核心设计哲学是"状态驱动视图"——开发者只需声明 UI 结构与状态之间的映射关系,框架自动完成差分更新和渲染调度。具体到本平台的技术选型:@Entry 标记页面入口组件,@Component 封装可复用的结构体,@State 管理组件内部的响应式状态变量,当状态值发生变化时框架自动触发依赖该状态的 UI 片段重新渲染;@Builder 将复杂的 UI 结构拆解为独立的构建函数,既提升代码可读性又支持参数化复用;@Observed 装饰器赋予数据模型字段级变化感知能力,使"评分更新即视图刷新"的流畅体验得以实现,无需手动调用刷新接口。这套装饰器体系构成了 ArkUI 声明式范式的骨架。

本平台深度融合了 HarmonyOS 6.1.1 的三大前沿特性矩阵。Camera Kit 提供了两条并行的相机能力通道:VideoSession 的 ControlCenter 通道承载 AUTO_FRAMING(影随人动)能力链,通过 isControlCenterSupported 能力探测、getSupportedEffectTypes 效果枚举查询、enableControlCenter 构图接管三步实现演讲跟拍时演讲者始终居中构图;PhotoSession 的 ManualFocus 通道承载手动对焦三接口——isFocusDistanceSupported 能力查询、setFocusDistance 距离设定、getFocusDistance 读回校验,实现从铭牌演讲稿近拍到全场舞台远距的精确对焦控制。Speech Kit 的 AI 字幕组件引入了 sourceLanguage(源语言)、targetLanguage(目标语言)、fontSize(字号枚举)、fontColor(字体颜色)四个新字段,支持中英双向翻译、中英双语对照、四档字号调节和五色字体预设,配合 writeAudio 接口以 640 字节 PCM 音频块写入实现实时语音转字幕。Canvas 绘制 实现表达力五维雷达图(台风/逻辑/感染力/语速/眼神),通过 drawRadar 方法绘制三层背景多边形、五条轴线、数据多边形和数据点,breath 呼吸状态联动数据值 ±3% 微波动模拟 AI 实时复盘的动态感。

二、整体架构流程图
架构以主组件为根节点,使用 Stack 容器实现层叠布局:底层 Column 纵向排列头部舞台问候区、分割线、Scroll 可滚动内容区和底部 Tab 栏,顶层覆盖三个独立弹窗(新增、编辑、删除各自条件渲染)。内容区通过 currentTab 状态变量在六个 @Builder 方法间做条件分支,四大特性分散在舞台(Canvas 雷达图)、相机(影随人动)、对焦(手动对焦)和字幕(AI 字幕)四个 Tab 上,全部状态变量统一声明在组件顶层实现跨 Tab 数据共享。这种"单组件多 Builder"的架构选择使状态流转路径最短,任何 Tab 的操作结果都能即时反映到其他 Tab 的相关视图中。

三、色彩体系设计
3.1 ColorPalette 接口定义
interface ColorPalette {
bg: string; // 全局背景(深夜舞台)
card: string; // 卡片底色
title: string; // 主标题
sub: string; // 副标题
text3: string; // 弱化文字
purple: string; // 舞台紫主色
purpleD: string; // 舞台紫深色
gold: string; // 聚光金
blue: string; // 信息蓝
red: string; // 警示红
green: string; // 通过绿
line: string; // 分割线
tabOn: string; // 底部 Tab 选中色
mask: string; // 弹窗遮罩
}
色彩体系的设计起点是 ColorPalette 接口。它以 TypeScript 接口形式约束了全部颜色字段的类型为字符串,确保每一个颜色值在使用时都有明确的语义命名而非魔法字符串。接口字段覆盖了背景层(bg/card/dark)、文字层(title/sub/text3 三级文字层次)、语义色层(purple/gold/blue/red/green)、结构色层(line/tabOn/mask)四大分类。这种分类方式使开发者在新增加 UI 元素时能快速定位应该引用哪个颜色字段,避免随意硬编码颜色值导致视觉不一致。

3.2 COLORS 常量逐色分析
const COLORS: ColorPalette = {
bg: '#171226', // 深夜舞台紫黑,沉浸式暗光环境
card: '#221B38', // 卡片底色,比背景亮一档紫
dark: '#2C2347', // 次级容器底色(统计格/标签底)
title: '#F2EDFA', // 淡紫白标题,暗光高对比
sub: '#C0B4DC', // 淡紫副标题,层次柔和
text3: '#857AA8', // 灰紫弱文本,辅助信息不抢视觉
purple: '#8B5CF6', // 舞台紫主色,渐变横幅与按钮主色
purpleD: '#6D3FD6', // 深紫渐变起点,头部横幅过渡
gold: '#F5C04E', // 聚光金强调色,评分/等级/勋章
blue: '#4EA3E3', // 信息蓝,进阶等级标识
red: '#E85B6E', // 警示红,低分与删除操作
green: '#4EC98A', // 通过绿,入门等级与已生效
line: '#35294F', // 紫黑分割线,低对比不干扰
tabOn: '#F5C04E', // Tab 选中色为聚光金(非主色紫)
mask: 'rgba(0,0,0,0.6)' // 半透黑遮罩
};
COLORS 常量是整个应用的视觉基调。色彩体系以"舞台紫 + 聚光金"为核心对比关系——紫色代表深夜舞台的沉浸氛围,模拟剧场熄灯后观众席的暗紫色调;金色代表聚光灯下的高光时刻,模拟追光灯打在演讲者身上的暖金光泽。背景色 #171226 是极深的紫黑色,在 OLED 屏幕上几乎接近纯黑但带有微弱的紫色色温,营造出"舞台已就绪、灯光未全开"的仪式感。

值得特别关注的是 Tab 选中色使用 gold(聚光金 #F5C04E)而非 purple(舞台紫 #8B5CF6)。这是一个经过对比度计算的设计决策:在深紫背景上,金色的明度(L≈74)远高于紫色(L≈53),用户视觉定位选中 Tab 的速度更快。头部今日简报横幅和学员渐变大卡都使用 linearGradient 从 purple 到 purpleD 的 135° 渐变,模拟舞台灯光由中心向四周渐暗的物理光照效果。三级文字层次 title/sub/text3 的明度递减确保了信息层级的视觉优先级——标题最先被看到、副标题次之、辅助信息最后。

四、Tab 元数据与辅助数据
4.1 底部导航 Tab 定义
interface TabMeta {
icon: string;
label: string;
}
const TAB_LIST: TabMeta[] = [
{ icon: '🎤', label: '舞台' },
{ icon: '📷', label: '相机' },
{ icon: '🎯', label: '对焦' },
{ icon: '🗣', label: '字幕' },
{ icon: '📊', label: '复盘' },
{ icon: '👤', label: '我的' }
];
底部导航采用 6 Tab 单排布局,每个 Tab 由 TabMeta 接口约束为图标(icon)和标签(label)两字段。TabMeta 接口确保每个 Tab 项的结构一致性,TAB_LIST 常量数组在组件加载时即固定,底部 Tab 栏通过 ForEach 遍历渲染。六个 Tab 分别对应:舞台(课程浏览与表达力评估)、相机(影随人动跟拍)、对焦(手动对焦精控)、字幕(AI 字幕设置)、复盘(练习记录管理)、我的(学员信息与数据可视化)。图标使用 Emoji 字符而非图片资源,避免了资源加载开销和跨分辨率适配问题。
4.2 辅助数据常量
辅助数据涵盖了应用各 Tab 所需的静态配置信息。EFFECT_INFOS 展示 ControlCenterEffectType 枚举的三种效果(BEAUTY 美颜/PORTRAIT 人像/AUTO_FRAMING 影随人动),其中 AUTO_FRAMING 标注为 6.1.1 新增,在相机 Tab 的效果枚举卡中渲染为列表行。FOCUS_PRESETS 定义三档景别预设(近拍 0.1/中距 0.5/远距 0.9),分别对应铭牌演讲稿特写、半身演讲构图和全场舞台收纳。SRC_LANGS 和 TGT_LANGS_EN 定义字幕语言选项,中文源时目标语言锁定 zh 无选项,英文源时可选中文/英文/中英双语三种目标。SIZE_OPTIONS 映射 AICaptionFontSize 枚举四档(SMALL/NORMAL/BIG/LARGE)。CAPTION_FONT_COLORS 预设五色字体卡(白/暖黄/薄荷绿/天蓝/樱花粉)。RADAR_LABELS 和 RADAR_VALUES 定义雷达图五维标签与数值。MONTH_NAME 和 PRACTICE_VAL 定义月度柱状图数据。BADGE_LIST 定义学员勋章清单。STAGE_QUOTE 定义舞台每日金句。
const FOCUS_PRESETS: FocusPreset[] = [
{ label: '近拍', distance: 0.1, scene: '0.1 · 铭牌演讲稿' },
{ label: '中距', distance: 0.5, scene: '0.5 · 半身演讲' },
{ label: '远距', distance: 0.9, scene: '0.9 · 全场舞台' }
];
对焦预设的设计映射了演讲训练的真实场景:近拍档位用于演讲者手持铭牌演讲稿练习时特写文字内容,中距档位用于半身构图让评委观察手势与体态,远距档位用于收纳全场舞台评估走位与台风。三档预设将抽象的 0.0~1.0 数值映射为具象的演讲场景,降低了用户理解手动对焦参数的认知门槛。
五、工具函数
工具函数层是组件的业务逻辑胶水,将状态值映射为用户可读的文案和颜色。
function framingStateColor(state: string): string {
if (state === '影随人动已启用') { return COLORS.green; }
if (state === '已交还手动构图') { return COLORS.blue; }
if (state.indexOf('失败') >= 0 || state.indexOf('被拒') >= 0 || state.indexOf('不支持') >= 0) { return COLORS.red; }
if (state === '未查询') { return COLORS.text3; }
return COLORS.gold;
}
framingStateColor 函数将影随人动的运行状态文案映射为语义颜色:已启用映射为通过绿、已交还映射为信息蓝、失败/被拒/不支持映射为警示红、未查询映射为灰紫弱文本、其他中间态映射为聚光金。这种"文案→颜色"的映射函数使状态卡片的颜色能够即时反映能力链的健康度,用户一眼即可判断影随人动是否正常工作。
function distanceLabel(d: number): string {
if (d < 0.3) { return '近拍景别 · 铭牌演讲稿特写'; }
if (d < 0.7) { return '中距景别 · 半身演讲构图'; }
return '远距景别 · 全场舞台收纳';
}
distanceLabel 函数将 0.0~1.0 的连续对焦距离值映射为景别文案,使用 0.3 和 0.7 作为分段阈值。低于 0.3 为近拍景别(铭牌演讲稿特写),0.3~0.7 为中距景别(半身演讲构图),高于 0.7 为远距景别(全场舞台收纳)。这个函数在 Slider 滑杆下方实时显示当前距离对应的景别名称,让用户在拖动滑杆时能理解数值的物理含义。
function readOkColor(ok: string): string {
if (ok === '已生效') { return COLORS.green; }
if (ok === '偏差') { return COLORS.gold; }
return COLORS.red;
}
function levelColor(level: string): string {
if (level === '入门') { return COLORS.green; }
if (level === '进阶') { return COLORS.blue; }
if (level === '大师') { return COLORS.gold; }
return COLORS.purple;
}
function scoreColor(score: number): string {
if (score >= 90) { return COLORS.gold; }
if (score >= 75) { return COLORS.green; }
if (score >= 60) { return COLORS.blue; }
return COLORS.red;
}
readOkColor 将对焦读回校验结果映射颜色:已生效为绿、偏差为金、失败为红。levelColor 将课程难度等级映射颜色:入门为绿、进阶为蓝、大师为金,形成"从易到难、从绿到金"的颜色阶梯,暗示用户从入门走向大师的过程如同从幕后走向聚光灯。scoreColor 将练习评分映射颜色:90 分以上为金(大师级)、75-89 为绿(良好)、60-74 为蓝(及格)、60 以下为红(待提升),评分颜色阶梯与等级颜色阶梯保持一致的语义逻辑。
function sizeLabel(size: AICaptionFontSize): string {
if (size === AICaptionFontSize.SMALL) { return 'AICaptionFontSize.SMALL'; }
if (size === AICaptionFontSize.BIG) { return 'AICaptionFontSize.BIG'; }
if (size === AICaptionFontSize.LARGE) { return 'AICaptionFontSize.LARGE'; }
return 'AICaptionFontSize.NORMAL';
}
function nowTime(): string {
const d = new Date();
const pad = (v: number): string => {
if (v < 10) { return '0' + v; }
return '' + v;
};
return pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
}
sizeLabel 函数将字号枚举值转换为可读的枚举全名字符串,避免直接打印枚举的数字值造成困惑。nowTime 函数生成 HH:mm:ss 格式的时间戳用于对焦记录时间线,内部使用闭包 pad 函数对个位数补零。totalMinutes 和 avgScore 两个聚合函数分别汇总练习列表的总时长和平均评分,用于复盘 Tab 的统计三卡展示。
六、数据模型层
数据模型层使用 @Observed 装饰器定义四个可观察数据类,实现字段级变化感知。
@Observed export class LessonItem {
icon: string; // 课程图标
name: string; // 课程名称
level: string; // 难度等级(入门/进阶/大师)
duration: string; // 课程时长
rate: string; // 课程评分
constructor(icon: string, name: string, level: string, duration: string, rate: string) {
this.icon = icon;
this.name = name;
this.level = level;
this.duration = duration;
this.rate = rate;
}
}
LessonItem 是课程模型,包含图标、名称、难度等级、时长和评分五个字段。使用 @Observed 装饰后,当任何字段被重新赋值时,依赖该字段的 UI 片段会自动刷新。LESSON_LIST 初始化了 8 门课程,从"开场三分钟破冰术"(入门)到"TED 式大师结构"(大师),覆盖了从破冰到大师的完整表达力阶梯。课程模型同时服务于横滑大卡和课程清单行两种 UI 形态,一份数据驱动两种视图。
@Observed export class FocusRecord {
time: string; // 操作时间
distance: number; // 设定对焦距离
readback: number; // getFocusDistance 读回值
ok: string; // 校验结果(已生效/偏差/失败)
constructor(time: string, distance: number, readback: number, ok: string) {
this.time = time;
this.distance = distance;
this.readback = readback;
this.ok = ok;
}
}
FocusRecord 是对焦记录模型,记录每次对焦操作的时间、设定值、读回值和校验结果。FOCUS_RECORD_INIT 预置了三条初始记录,后续每次 applyFocus 或 readBackFocus 调用都会通过 unshift 在列表头部插入新记录,并限制最大长度为 20 条(超出时 pop 移除尾部)。这种"最新置顶、容量受限"的列表管理策略保证用户总能第一时间看到最近一次对焦操作的结果。
@Observed export class CaptionScene {
scene: string; // 场景名
desc: string; // 场景说明
src: string; // 推荐源语言
tgt: string; // 推荐目标语言
constructor(scene: string, desc: string, src: string, tgt: string) {
this.scene = scene;
this.desc = desc;
this.src = src;
this.tgt = tgt;
}
}
CaptionScene 是字幕场景模型,预置了五种演讲字幕场景:英文演讲跟练(en→zh)、中文即兴播报(zh→zh)、双语答辩模拟(en→zh-en)、英文原文精听(en→en)、中文稿复盘转写(zh→zh)。每个场景卡点击后调用 applyScene 自动应用推荐的语言组合,用户无需手动逐项设置源语言和目标语言。
@Observed export class PracticeItem {
date: string; // 练习日期
topic: string; // 练习主题
duration: number; // 练习时长(分钟)
score: number; // AI 评分(0~100)
constructor(date: string, topic: string, duration: number, score: number) {
this.date = date;
this.topic = topic;
this.duration = duration;
this.score = score;
}
}
PracticeItem 是练习记录模型,包含日期、主题、时长和 AI 评分四个字段。PRACTICE_LIST 预置了 8 条练习记录,评分从 64 到 95 跨越了从及格到大师级的完整区间。这个模型是复盘 Tab 和弹窗系统的核心实体——新增弹窗创建新实例 unshift 置顶、编辑弹窗修改现有实例字段、删除弹窗 splice 移除实例,所有操作都因 @Observed 而即时反映到 UI。
七、组件主体
7.1 状态变量体系
@Entry
@Component
struct Page1219 {
// --- Tab / 弹窗状态 ---
@State currentTab: number = 0; // 当前 Tab 索引
@State addModal: boolean = false; // 新增练习记录弹窗
@State editModal: boolean = false; // 编辑练习记录弹窗
@State delModal: boolean = false; // 删除练习记录弹窗
@State editIdx: number = -1; // 编辑目标下标
@State delIdx: number = -1; // 删除目标下标
@State breath: boolean = false; // 呼吸动画开关
timer: number = -1; // 呼吸动画定时器
组件状态变量按职责分为五大区块。第一区块是 Tab 与弹窗状态:currentTab 控制 6 Tab 分支切换,三个布尔弹窗开关控制弹窗的条件渲染,editIdx 和 delIdx 记录当前操作的记录下标,breath 是呼吸动画的布尔翻转开关,配合 timer 定时器每秒翻转一次。breath 不使用 @State 的 timer 是因为定时器 ID 不需要触发 UI 刷新,只需在 aboutToDisappear 时清理。
// --- Camera Kit 成员 ---
private previewController: XComponentController = new XComponentController();
private cameraInput?: camera.CameraInput;
private videoSession?: camera.VideoSession; // 影随人动宿主
private photoSession?: camera.PhotoSession; // 手动对焦宿主
@State surfaceReady: boolean = false;
@State sessionMode: string = 'idle'; // idle / video / photo
@State cameraGranted: boolean = false;
@State framingState: string = '未查询';
@State framingSupported: boolean = false;
@State framingOn: boolean = false;
@State focusSupported: boolean = false;
@State focusDistance: number = 1.0;
@State focusReadback: number = -1;
@State focusResult: string = '未验证';
@State focusRecords: FocusRecord[] = FOCUS_RECORD_INIT;
第二区块是 Camera Kit 成员。previewController 是 XComponent 的控制器实例,用于获取 Surface ID 供预览输出绑定。cameraInput/videoSession/photoSession 三个可选类型的私有成员分别持有相机输入、影随人动宿主和手动对焦宿主,使用可选类型是因为它们在会话未启动时为 undefined。关键设计是 VideoSession 和 PhotoSession 不能同时存活——一个相机输入只能绑定到一个 Session,因此模式切换时必须先释放旧会话再创建新会话。sessionMode 用三态字符串 idle/video/photo 追踪当前会话模式,驱动按钮的 enabled 状态和文案切换。
// --- Speech Kit 成员 ---
private captionController: AICaptionController = new AICaptionController();
@State captionShown: boolean = false;
@State srcLang: string = 'zh';
@State tgtLang: string = 'zh';
@State captionSize: AICaptionFontSize = AICaptionFontSize.NORMAL;
@State captionColor: string = CAPTION_FONT_COLORS[0];
@State captionReady: boolean = false;
@State captionErrMsg: string = '';
@State captionFed: number = 0;
第三区块是 Speech Kit 成员。captionController 是 AI 字幕组件的控制器实例,用于调用 writeAudio 写入音频流。captionShown 与 AICaptionComponent 的 isShown 双向绑定控制字幕显隐。srcLang/tgtLang/captionSize/captionColor 四个状态分别对应 6.1.1 新增的四个字幕字段,每次变化都会通过 buildCaptionOptions 重新组装传递给 AICaptionComponent。captionReady 追踪 onPrepared 回调,captionErrMsg 追踪 onError 回调,captionFed 统计已写入的音频块数。
7.2 生命周期
aboutToAppear() {
this.timer = setInterval(() => {
this.breath = !this.breath;
this.drawRadar();
}, 1000);
}
aboutToDisappear() {
clearInterval(this.timer);
this.releaseSession();
}
aboutToAppear 在组件即将出现时启动一个每秒执行的定时器,做两件事:翻转 breath 布尔值触发呼吸动画联动,调用 drawRadar 重绘雷达图使数据值产生 ±3% 微波动。aboutToDisappear 在组件即将消失时清理定时器并调用 releaseSession 释放可能存活的相机会话,防止后台占用相机资源。这两个生命周期钩子确保了动画的启动和资源的释放成对出现,不会产生定时器泄漏或相机资源泄漏。
7.3 build 主布局
build() {
Stack() {
Column() {
this.headerStage()
Divider().strokeWidth(1).color(COLORS.line)
Scroll() {
Column() {
if (this.currentTab === 0) {
this.tabStage()
} else if (this.currentTab === 1) {
this.tabCamera()
} else if (this.currentTab === 2) {
this.tabFocus()
} else if (this.currentTab === 3) {
this.tabCaption()
} else if (this.currentTab === 4) {
this.tabReview()
} else {
this.tabMine()
}
}.width('100%').padding({ left: 12, right: 12, top: 10, bottom: 12 })
}.layoutWeight(1).scrollBar(BarState.Off).align(Alignment.Top)
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; }) }
}.alignContent(Alignment.Center).backgroundColor(COLORS.bg).height('100%')
}
build 方法是组件的渲染入口,使用 Stack 作为根容器实现层叠效果。底层 Column 纵向排列四个区域:头部舞台问候、分割线、可滚动内容区、底部 Tab 栏。内容区使用 Scroll 包裹 Column,通过 layoutWeight(1) 占满中间空间,scrollBar(BarState.Off) 隐藏滚动条保持视觉干净。内容区内部通过 if-else 链根据 currentTab 调用对应的 @Builder 方法,实现 6 Tab 的条件分支渲染。顶层三个弹窗通过各自的条件布尔值控制渲染,每个弹窗接收一个 onClose 回调函数用于点击遮罩或按钮时关闭。Stack 的 alignContent(Alignment.Center) 使弹窗在屏幕居中显示。
八、头部区域
@Builder headerStage() {
Column({ space: 10 }) {
Row() {
Column({ space: 2 }) {
Text('声动课堂').fontSize(18).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Text('聚光灯下的表达力训练场').fontSize(10).fontColor(COLORS.text3)
}.alignItems(HorizontalAlign.START)
Blank()
Row({ space: 6 }) {
Text('🎤').fontSize(14)
Text('Lv.6 舞台新星').fontSize(10).fontColor(COLORS.gold)
}.padding({ left: 10, right: 10, top: 5, bottom: 5 }).backgroundColor(COLORS.dark).borderRadius(14)
}.width('100%')
Row({ space: 12 }) {
Column({ space: 4 }) {
Text('今日简报').fontSize(10).fontColor(COLORS.gold).fontWeight(FontWeight.Bold)
Text('已完成 1 次跟练 · 表达力 +2.4').fontSize(12).fontColor(COLORS.title)
Text('连续训练 21 天,保持舞台手感').fontSize(9).fontColor(COLORS.sub)
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
Column() {
Text('🎙').fontSize(26).opacity(this.breath ? 1 : 0.55)
}.padding(8)
}.width('100%').padding(14).borderRadius(14)
.linearGradient({ angle: 135, colors: [[COLORS.purple, 0.0], [COLORS.purpleD, 1.0]] })
}.width('100%').padding({ left: 12, right: 12, top: 10, bottom: 10 }).backgroundColor(COLORS.bg)
}
头部区域分为上下两块。上块是品牌行:左侧"声动课堂"主标题(18px 加粗淡紫白)和"聚光灯下的表达力训练场"副标题(10px 灰紫),右侧是等级徽章"🎤 Lv.6 舞台新星"以聚光金文字配深色圆角胶囊背景。下块是今日简报渐变横幅,使用 linearGradient 从 purple 到 purpleD 的 135° 渐变模拟舞台灯光效果。横幅内三行文字层次递进:标题(今日简报)→ 数据(已完成 1 次跟练·表达力 +2.4)→ 鼓励语(连续训练 21 天)。右侧的麦克风图标 🎙 通过 opacity 绑定 breath 状态实现明暗呼吸效果,每秒在 1.0 和 0.55 之间翻转,暗示系统正在"监听"用户的表达。
九、Tab0 舞台分析
舞台 Tab 是应用的首页,整合了金句激励、课程浏览和表达力评估三大功能。
@Builder tabStage() {
Column({ space: 12 }) {
// —— 每日金句卡 ——
Row({ space: 10 }) {
Text('💡').fontSize(18)
Column({ space: 3 }) {
Text('每日金句').fontSize(10).fontColor(COLORS.text3)
Text(STAGE_QUOTE).fontSize(12).fontColor(COLORS.title).maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
}.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(14)
每日金句卡位于舞台 Tab 顶部,使用灯泡图标 💡 配合金句文案"好的演讲不是背诵,而是把思想放进听众的口袋。"金句文案使用 maxLines(2) 限制两行并配 TextOverflow.Ellipsis 在超长时以省略号截断,保证卡片高度稳定。
// —— 精选课程横滑大卡 ——
Column({ space: 8 }) {
Row() {
Text('今日精选课程').fontSize(14).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Blank()
Text(`${this.lessonList.length} 门`).fontSize(10).fontColor(COLORS.text3)
}.width('100%')
Scroll() {
Row({ space: 10 }) {
ForEach(this.lessonList, (lesson: LessonItem, idx: number) => {
this.lessonBigCard(lesson, idx)
}, (lesson: LessonItem, idx: number) => `big-${lesson.name}-${idx}`)
}.padding({ bottom: 2 })
}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).width('100%')
}.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(14)
精选课程区域使用横向 Scroll 容器实现大卡横滑浏览。ForEach 遍历 lessonList 数组为每门课程渲染一个 lessonBigCard 大卡,键值生成器使用 big-${lesson.name}-${idx} 格式确保列表项的唯一标识。横滑区域 scrollable(ScrollDirection.Horizontal) 限定水平滚动方向,隐藏滚动条保持视觉整洁。
@Builder lessonBigCard(lesson: LessonItem, idx: number) {
Column({ space: 8 }) {
Text(lesson.icon).fontSize(30)
Text(lesson.name).fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
Row({ space: 6 }) {
Text(lesson.level).fontSize(9).fontColor(levelColor(lesson.level))
.padding({ left: 6, right: 6, top: 2, bottom: 2 }).backgroundColor(COLORS.dark).borderRadius(6)
Text(lesson.duration).fontSize(9).fontColor(COLORS.text3)
}.width('100%')
Row({ space: 4 }) {
Text('⭐').fontSize(10)
Text(lesson.rate).fontSize(11).fontColor(COLORS.gold).fontWeight(FontWeight.Bold)
Blank()
Text(idx === 0 ? '热练中' : '跟练').fontSize(10)
.fontColor(idx === 0 ? COLORS.bg : COLORS.sub)
.padding({ left: 8, right: 8, top: 3, bottom: 3 }).borderRadius(10)
.backgroundColor(idx === 0 ? COLORS.gold : COLORS.dark)
}.width('100%')
}.width(132).padding(12).backgroundColor(COLORS.dark).borderRadius(12)
}
lessonBigCard 是课程横滑大卡的构建器,固定宽度 132px。卡片从上到下依次展示:课程图标(30px Emoji)、课程名称(13px 加粗,单行省略截断)、等级标签和时长行、评分和操作按钮行。等级标签通过 levelColor 函数动态着色(入门绿/进阶蓝/大师金)。操作按钮根据 idx === 0 判断是否为第一门课程:第一门显示"热练中"金色按钮(背景聚光金、文字深紫背景色),其余显示"跟练"灰色按钮。这种首项高亮设计引导用户优先跟练推荐课程。
// —— 课程清单行 ——
this.lessonRows()
课程清单行使用 List + ListItem + ForEach 渲染纵向课程列表,每行展示课程图标、名称、等级、时长和评分,右侧有紫色播放箭头 ▶ 暗示可点击进入。清单行与大卡共用同一数据源 lessonList,但展示形态不同——大卡侧重视觉吸引力,清单行侧重信息密度。
十、Tab1 相机分析
相机 Tab 是影随人动(AUTO_FRAMING)特性的主场,集成了相机权限管理、XComponent 预览、会话模式切换、效果枚举展示和能力链状态追踪。
@Builder tabCamera() {
Column({ space: 12 }) {
// —— 授权状态卡 ——
Column({ space: 8 }) {
Row() {
Text('相机权限').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Blank()
Text(this.cameraGranted ? '已授权' : '未授权').fontSize(10)
.fontColor(this.cameraGranted ? COLORS.green : COLORS.text3)
}.width('100%')
Text('影随人动与手动对焦均依赖 ohos.permission.CAMERA(user_grant,需运行时动态申请)')
.fontSize(10).fontColor(COLORS.text3)
Button(this.cameraGranted ? '重新申请权限' : '申请相机权限')
.fontSize(11).height(34).backgroundColor(COLORS.purple)
.fontColor(COLORS.title).width('100%')
.onClick(() => { this.requestCameraPermission(); })
}.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(14)
授权状态卡是相机 Tab 的第一道关卡。cameraGranted 状态绑定"已授权/未授权"文案和颜色。按钮文案根据授权态动态切换,点击调用 requestCameraPermission 异步方法。该方法通过 abilityAccessCtrl.createAtManager() 创建权限管理器,调用 requestPermissionsFromUser 动态申请 ohos.permission.CAMERA 权限,根据 authResults[0] === 0 判断是否已授权并更新状态。
// —— XComponent 预览 + 模式切换 ——
Column({ space: 8 }) {
Row() {
Text('舞台跟拍预览').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Blank()
Text(this.surfaceReady ? 'Surface 已就绪' : 'Surface 未就绪').fontSize(10)
.fontColor(this.surfaceReady ? COLORS.green : COLORS.text3)
}.width('100%')
XComponent({ id: 'stageCameraPreview', type: XComponentType.SURFACE,
controller: this.previewController })
.width('100%').height(240).borderRadius(12).backgroundColor(COLORS.dark)
.onLoad(() => { this.surfaceReady = true; })
XComponent 是相机预览的渲染载体,类型设为 SURFACE 用于接收相机预览流。onLoad 回调在 Surface 创建完成时将 surfaceReady 置为 true,这是启动任何相机会话的前置条件——startVideoMode 和 switchToPhotoMode 方法第一步都会检查此标记。previewController 提供的 getXComponentSurfaceId() 方法返回的 Surface ID 会被传递给 createPreviewOutput 用于绑定预览输出。
Row({ space: 8 }) {
Button('开启影随人动').fontSize(11).height(34)
.backgroundColor(this.sessionMode === 'video' ? COLORS.purpleD : COLORS.purple)
.fontColor(COLORS.title).layoutWeight(1)
.enabled(this.surfaceReady && this.sessionMode !== 'video')
.onClick(() => { this.startVideoMode(); })
Button('停止跟拍').fontSize(11).height(34).backgroundColor(COLORS.dark)
.fontColor(COLORS.sub).layoutWeight(1).enabled(this.sessionMode === 'video')
.onClick(() => { this.releaseSession(); this.framingState = '未查询'; })
}.width('100%')
模式切换按钮行的设计体现了会话互斥原则。"开启影随人动"按钮仅在 Surface 就绪且当前非 video 模式时可点击(enabled 条件),点击调用 startVideoMode 创建 VideoSession。"停止跟拍"按钮仅在 video 模式时可点击,点击调用 releaseSession 释放会话并重置影随人动状态文案。两个按钮的 enabled 状态互斥,防止用户在会话切换过程中产生竞态操作。
// —— 会话三态状态卡 ——
Row({ space: 10 }) {
Column({ space: 3 }) {
Text('会话模式').fontSize(9).fontColor(COLORS.text3)
Text(this.sessionMode === 'video' ? 'VIDEO' : (this.sessionMode === 'photo' ? 'PHOTO' : 'IDLE'))
.fontSize(12).fontColor(COLORS.title).fontWeight(FontWeight.Bold).fontFamily('monospace')
}.layoutWeight(1).alignItems(HorizontalAlign.Center).padding(10)
.backgroundColor(COLORS.dark).borderRadius(10)
Column({ space: 3 }) {
Text('AUTO_FRAMING').fontSize(9).fontColor(COLORS.text3)
Text(this.framingSupported ? '已声明' : '未声明').fontSize(12)
.fontColor(this.framingSupported ? COLORS.green : COLORS.text3).fontWeight(FontWeight.Bold)
}.layoutWeight(1).alignItems(HorizontalAlign.Center).padding(10)
.backgroundColor(COLORS.dark).borderRadius(10)
Column({ space: 3 }) {
Text('构图接管').fontSize(9).fontColor(COLORS.text3)
Text(this.framingOn ? '系统接管' : '手动').fontSize(12)
.fontColor(this.framingOn ? COLORS.gold : COLORS.text3).fontWeight(FontWeight.Bold)
}.layoutWeight(1).alignItems(HorizontalAlign.Center).padding(10)
.backgroundColor(COLORS.dark).borderRadius(10)
}.width('100%')
会话三态状态卡用三列等宽布局展示会话模式(IDLE/VIDEO/PHOTO)、AUTO_FRAMING 声明态(已声明/未声明)和构图接管态(系统接管/手动)。三列分别使用 layoutWeight(1) 等分宽度,文字使用 monospace 等宽字体增强技术感。这三态卡片让用户在不查看代码的情况下也能理解相机会话的内部状态机。
// —— ControlCenterEffectType 效果枚举卡 ——
Column({ space: 8 }) {
Row() {
Text('ControlCenterEffectType 效果枚举').fontSize(13)
.fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Blank()
Text(this.framingState).fontSize(10).fontColor(framingStateColor(this.framingState))
}.width('100%')
ForEach(EFFECT_INFOS, (info: EffectInfo) => {
Row({ space: 8 }) {
Text(`${info.type}`).fontSize(10).fontColor(COLORS.gold).fontFamily('monospace')
Text(info.name).fontSize(11).fontColor(COLORS.title).fontFamily('monospace')
Text(info.desc).fontSize(9).fontColor(COLORS.text3).layoutWeight(1)
if (info.type === 2) {
Text(this.framingSupported ? '本机已声明' : '待查询').fontSize(9)
.fontColor(this.framingSupported ? COLORS.green : COLORS.text3)
}
}.width('100%').padding({ left: 10, right: 10, top: 8, bottom: 8 })
.backgroundColor(COLORS.dark).borderRadius(10)
}, (info: EffectInfo) => `effect-${info.type}`)
效果枚举卡遍历 EFFECT_INFOS 数组展示三种 ControlCenter 效果。每行显示效果编号(type 值 0/1/2)、枚举名(BEAUTY/PORTRAIT/AUTO_FRAMING)和描述文案。当 info.type === 2(即 AUTO_FRAMING)时额外显示"本机已声明/待查询"标记,绑定 framingSupported 状态。影随人动状态文案通过 framingStateColor 函数动态着色,用户可一眼判断当前是已启用(绿)、已交还(蓝)、失败(红)还是未查询(灰)。
Row({ space: 8 }) {
Text('enableControlCenter 影随人动').fontSize(11).fontColor(COLORS.sub)
Blank()
Toggle({ type: ToggleType.Switch, isOn: this.framingOn })
.selectedColor(COLORS.purple)
.onChange((isOn: boolean) => { this.toggleFraming(isOn); })
}.width('100%')
}.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(14)
Toggle 开关绑定 framingOn 状态,onChange 回调调用 toggleFraming 方法。该方法仅在 VideoSession 存活时调用 enableControlCenter(isOn) 请求系统接管或交还构图,并更新状态文案为"影随人动已启用"或"已交还手动构图"。如果会话未启动则直接回退开关状态并提示"会话未启动"。
// —— 能力链三步状态卡 ——
Column({ space: 6 }) {
Text('影随人动能力链(VideoSession · ControlCenter)').fontSize(12)
.fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%')
this.chainRow('① session.isControlCenterSupported()', this.sessionMode === 'video')
this.chainRow('② getSupportedEffectTypes().includes(AUTO_FRAMING)', this.framingSupported)
this.chainRow('③ session.enableControlCenter(true)', this.framingOn)
}.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(14)
能力链三步状态卡是相机 Tab 的精华,使用 chainRow 构建器渲染三行步骤,每行包含步骤文案和通过/待验证标记。三步分别是:① isControlCenterSupported 检查设备是否支持控制中心(绑定 sessionMode === 'video'),② getSupportedEffectTypes 查询支持的效果列表是否包含 AUTO_FRAMING(绑定 framingSupported),③ enableControlCenter(true) 请求系统接管构图(绑定 framingOn)。这三步构成了影随人动的完整能力探测与启用链路。
十一、Tab2 对焦分析
对焦 Tab 是手动对焦三接口的实战主场,集成了能力查询、景别预设、连续滑杆调节、读回校验和操作记录时间线。
@Builder tabFocus() {
Column({ space: 12 }) {
// —— 能力查询卡 ——
Column({ space: 8 }) {
Row() {
Text('手动对焦能力查询').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Blank()
Text(this.focusSupported ? 'isFocusDistanceSupported = true' : '未启动/不支持').fontSize(9)
.fontColor(this.focusSupported ? COLORS.green : COLORS.text3).fontFamily('monospace')
}.width('100%')
Text('手动对焦三接口挂在 PhotoSession 的 ManualFocus 通道,须先启动拍照会话')
.fontSize(10).fontColor(COLORS.text3)
Row({ space: 8 }) {
Button('启动拍照会话').fontSize(11).height(32)
.backgroundColor(this.sessionMode === 'photo' ? COLORS.purpleD : COLORS.purple)
.fontColor(COLORS.title).layoutWeight(1).enabled(this.sessionMode !== 'photo')
.onClick(() => { this.switchToPhotoMode(); })
Button('重新查询能力').fontSize(11).height(32).backgroundColor(COLORS.dark)
.fontColor(COLORS.sub).layoutWeight(1)
.onClick(() => { this.queryFocusSupport(); })
}.width('100%')
}.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(14)
能力查询卡是对焦 Tab 的入口。手动对焦三接口(isFocusDistanceSupported/setFocusDistance/getFocusDistance)挂在 PhotoSession 的 ManualFocus 通道上,必须先通过 switchToPhotoMode 创建并启动 PhotoSession 后才能调用。queryFocusSupport 方法调用 isFocusDistanceSupported() 同步返回布尔值,更新 focusSupported 状态。"启动拍照会话"按钮在非 photo 模式时可点击,"重新查询能力"按钮始终可点击用于刷新能力探测结果。
// —— 三档景别预设 ——
Column({ space: 8 }) {
Text('演讲景别预设(点击即设焦)').fontSize(13)
.fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%')
Row({ space: 8 }) {
ForEach(FOCUS_PRESETS, (preset: FocusPreset) => {
Column({ space: 4 }) {
Text(preset.label).fontSize(12).fontWeight(FontWeight.Bold)
.fontColor(Math.abs(this.focusDistance - preset.distance) < 0.02 ? COLORS.gold : COLORS.title)
Text(preset.scene).fontSize(9).fontColor(COLORS.text3)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
}.layoutWeight(1).alignItems(HorizontalAlign.Center).padding(10).borderRadius(10)
.backgroundColor(Math.abs(this.focusDistance - preset.distance) < 0.02 ? COLORS.purpleD : COLORS.dark)
.onClick(() => { this.applyFocus(preset.distance); })
}, (preset: FocusPreset) => `preset-${preset.label}`)
}.width('100%')
}.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(14)
三档景别预设卡展示近拍(0.1)、中距(0.5)、远距(0.9)三个预设。每个预设卡片的文字颜色和背景色根据当前 focusDistance 与预设距离的差值是否小于 0.02 动态切换——匹配时显示金色文字配深紫背景(选中态),不匹配时显示白色文字配暗紫背景(未选中态)。点击调用 applyFocus 方法,该方法先设置 focusDistance 值,再调用 setFocusDistance 设定相机对焦距离,然后调用 readBackFocus 读回校验。
// —— 对焦距离滑杆 ——
Column({ space: 6 }) {
Row() {
Text('对焦距离').fontSize(12).fontColor(COLORS.title)
Blank()
Text(this.focusDistance.toFixed(2)).fontSize(14).fontColor(COLORS.gold)
.fontFamily('monospace').fontWeight(FontWeight.Bold)
}.width('100%')
Slider({ value: this.focusDistance, min: 0, max: 1, step: 0.01 })
.width('100%').blockColor(COLORS.gold).selectedColor(COLORS.purple).trackColor(COLORS.dark)
.onChange((value: number, mode: SliderChangeMode) => { this.focusDistance = value; this.setFocusLive(); })
Text(distanceLabel(this.focusDistance)).fontSize(10).fontColor(COLORS.text3).width('100%')
}.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(14)
对焦距离滑杆使用 Slider 组件实现 0.0~1.0 的连续调节,步进 0.01。滑杆的 blockColor(滑块色)为聚光金、selectedColor(已选轨道色)为舞台紫、trackColor(未选轨道色)为暗紫,三色搭配形成清晰的视觉层次。onChange 回调在用户拖动时实时更新 focusDistance 并调用 setFocusLive 方法——该方法调用 setFocusDistance 将距离值实时下发给相机但不写入记录时间线,实现"拖动即对焦"的流畅体验。滑杆下方通过 distanceLabel 函数显示当前距离对应的景别文案。
// —— 读回验证卡 ——
Column({ space: 8 }) {
Text('读回验证 · getFocusDistance()').fontSize(13)
.fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%')
Row({ space: 10 }) {
Column({ space: 3 }) {
Text('设定值').fontSize(9).fontColor(COLORS.text3)
Text(this.focusDistance.toFixed(2)).fontSize(14)
.fontColor(COLORS.title).fontFamily('monospace')
}.layoutWeight(1).alignItems(HorizontalAlign.Center).padding(10)
.backgroundColor(COLORS.dark).borderRadius(10)
Column({ space: 3 }) {
Text('读回值').fontSize(9).fontColor(COLORS.text3)
Text(this.focusReadback >= 0 ? this.focusReadback.toFixed(2) : '—').fontSize(14)
.fontColor(COLORS.blue).fontFamily('monospace')
}.layoutWeight(1).alignItems(HorizontalAlign.Center).padding(10)
.backgroundColor(COLORS.dark).borderRadius(10)
Column({ space: 3 }) {
Text('校验').fontSize(9).fontColor(COLORS.text3)
Text(this.focusResult).fontSize(12)
.fontColor(readOkColor(this.focusResult)).fontWeight(FontWeight.Bold)
}.layoutWeight(1).alignItems(HorizontalAlign.Center).padding(10)
.backgroundColor(COLORS.dark).borderRadius(10)
}.width('100%')
读回验证卡用三列等宽布局展示对焦操作的设定值、读回值和校验结果。设定值取自 focusDistance,读回值取自 focusReadback(初始为 -1 显示"—“),校验结果取自 focusResult(初始为"未验证”)。readBackFocus 方法调用 getFocusDistance() 获取相机当前实际对焦距离,与设定值做差值比较——差值小于 0.01 判"已生效"(绿色),否则判"偏差"(金色),调用异常判"失败"(红色)。每次读回操作都会创建新的 FocusRecord 实例 unshift 到记录时间线。
// —— 对焦记录时间线 ——
Column({ space: 6 }) {
Row() {
Text('对焦记录').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Blank()
Text(`共 ${this.focusRecords.length} 条`).fontSize(10).fontColor(COLORS.text3)
}.width('100%')
Scroll() {
Column({ space: 6 }) {
ForEach(this.focusRecords, (rec: FocusRecord) => {
Row({ space: 8 }) {
Text(rec.time).fontSize(10).fontColor(COLORS.text3).fontFamily('monospace')
Text(rec.distance.toFixed(2)).fontSize(11).fontColor(COLORS.title).fontFamily('monospace')
Text('→').fontSize(10).fontColor(COLORS.text3)
Text(rec.readback >= 0 ? rec.readback.toFixed(2) : '—').fontSize(11)
.fontColor(COLORS.blue).fontFamily('monospace')
Blank()
Text(rec.ok).fontSize(10).fontColor(readOkColor(rec.ok))
}.width('100%').padding(8).backgroundColor(COLORS.dark).borderRadius(8)
}, (rec: FocusRecord) => `rec-${rec.time}-${rec.distance}`)
}.width('100%')
}.height(150).width('100%').scrollBar(BarState.Off)
}.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(14)
对焦记录时间线使用固定高度 150px 的 Scroll 容器纵向滚动,每条记录显示时间、设定距离、箭头→、读回值和校验结果。校验结果通过 readOkColor 着色,与读回验证卡的校验列保持颜色一致。记录列表上限 20 条,超出时自动移除尾部最旧记录,保证时间线始终展示最近 20 次操作。
十二、Tab3 字幕分析
字幕 Tab 是 AI 字幕组件的四字段配置主场,集成了实时预览、语言联动、外观设置、场景推荐和音频写入演示。
@Builder tabCaption() {
Column({ space: 12 }) {
// —— ① AICaptionComponent 实时预览 ——
Column({ space: 8 }) {
Row() {
Text('AI 字幕实时预览').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Blank()
Text(this.captionReady ? '已就绪' : '初始化中').fontSize(10)
.fontColor(this.captionReady ? COLORS.green : COLORS.text3)
}.width('100%')
AICaptionComponent({
isShown: this.captionShown,
controller: this.captionController,
options: this.buildCaptionOptions()
}).width('100%').height(110).borderRadius(10)
AI 字幕实时预览区使用 AICaptionComponent 组件,接收三个参数:isShown 控制显隐、controller 提供音频写入接口、options 通过 buildCaptionOptions 方法组装配置对象。buildCaptionOptions 方法将 srcLang、tgtLang、captionSize、captionColor 四个状态字段组装为 AICaptionOptions 对象,同时注册 onPrepared 和 onError 回调。onPrepared 在字幕服务就绪时将 captionReady 置为 true,onError 在出错时将错误码和消息写入 captionErrMsg。
Row({ space: 8 }) {
Button(this.captionShown ? '隐藏字幕' : '开启字幕').fontSize(11).height(32)
.backgroundColor(this.captionShown ? COLORS.purpleD : COLORS.purple)
.fontColor(COLORS.title).layoutWeight(1)
.onClick(() => { this.captionShown = !this.captionShown; })
Text(`已写入 ${this.captionFed} 块`).fontSize(10)
.fontColor(COLORS.text3).layoutWeight(1).textAlign(TextAlign.Center)
}.width('100%')
if (this.captionErrMsg !== '') {
Text(this.captionErrMsg).fontSize(10).fontColor(COLORS.red).width('100%')
}
}.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(14)
字幕显隐按钮文案根据 captionShown 状态切换,背景色也同步变化。已写入块数 captionFed 实时显示在右侧。当 captionErrMsg 非空时条件渲染红色错误提示文本,保证用户能及时发现字幕服务异常。
// —— ② 源语言 / 目标语言联动 ——
Column({ space: 8 }) {
Text('语言设置(sourceLanguage / targetLanguage)').fontSize(13)
.fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%')
Row({ space: 8 }) {
Text('源语言').fontSize(11).fontColor(COLORS.sub).width(64)
ForEach(SRC_LANGS, (lang: LangOption) => {
Text(lang.name).fontSize(11)
.fontColor(this.srcLang === lang.code ? COLORS.bg : COLORS.sub)
.padding({ left: 14, right: 14, top: 6, bottom: 6 }).borderRadius(14)
.backgroundColor(this.srcLang === lang.code ? COLORS.gold : COLORS.dark)
.onClick(() => { this.switchSourceLang(lang.code); })
}, (lang: LangOption) => `src-${lang.code}`)
Blank()
Text(langName(this.srcLang)).fontSize(10).fontColor(COLORS.text3)
}.width('100%')
if (this.srcLang === 'zh') {
Text('中文源 targetLanguage 仅支持 zh,已自动锁定(无翻译方向可选)')
.fontSize(10).fontColor(COLORS.gold).width('100%')
} else {
Row({ space: 8 }) {
Text('目标语言').fontSize(11).fontColor(COLORS.sub).width(64)
ForEach(TGT_LANGS_EN, (lang: LangOption) => {
Text(lang.name).fontSize(11)
.fontColor(this.tgtLang === lang.code ? COLORS.bg : COLORS.sub)
.padding({ left: 12, right: 12, top: 6, bottom: 6 }).borderRadius(14)
.backgroundColor(this.tgtLang === lang.code ? COLORS.gold : COLORS.dark)
.onClick(() => { this.tgtLang = lang.code; })
}, (lang: LangOption) => `tgt-${lang.code}`)
Blank()
Text(langName(this.tgtLang)).fontSize(10).fontColor(COLORS.text3)
}.width('100%')
}
}.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(14)
语言设置区实现了源语言与目标语言的联动逻辑。switchSourceLang 方法在切换源语言时自动联动目标语言:中文源锁定 zh(不显示目标语言选择器,只显示锁定提示),英文源默认切为 zh-en(中英双语,用户可再选 zh 或 en)。源语言选项卡选中时显示金色背景配深色文字,未选中时显示暗紫背景配灰紫文字。这种"选中反色"设计在深色主题下具有较高的视觉辨识度。
// —— ③ 字号四档 + 颜色五卡 ——
Column({ space: 10 }) {
Text('外观设置(fontSize / fontColor)').fontSize(13)
.fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%')
Row({ space: 8 }) {
ForEach(SIZE_OPTIONS, (opt: SizeOption) => {
Text(opt.name).fontSize(11)
.fontColor(this.captionSize === opt.size ? COLORS.bg : COLORS.sub)
.padding({ left: 12, right: 12, top: 6, bottom: 6 }).borderRadius(14)
.backgroundColor(this.captionSize === opt.size ? COLORS.purple : COLORS.dark)
.onClick(() => { this.captionSize = opt.size; })
}, (opt: SizeOption) => `size-${opt.name}`)
Blank()
}.width('100%')
Row({ space: 10 }) {
ForEach(CAPTION_FONT_COLORS, (c: string) => {
Column().width(26).height(26).borderRadius(13).backgroundColor(c)
.border({
width: this.captionColor === c ? 2 : 1,
color: this.captionColor === c ? COLORS.gold : COLORS.line
})
.onClick(() => { this.captionColor = c; })
}, (c: string) => `capColor-${c}`)
Blank()
Text(this.captionColor).fontSize(10).fontColor(COLORS.text3).fontFamily('monospace')
}.width('100%')
}.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(14)
外观设置区分字号和颜色两部分。字号四档(小号/标准/大号/超大)映射 AICaptionFontSize 枚举,选中时显示紫色背景。颜色五卡是五个圆形色块(白/暖黄/薄荷绿/天蓝/樱花粉),选中时边框加粗为 2px 聚光金,未选中时边框为 1px 分割线色。右侧显示当前选中颜色的十六进制值。字号和颜色的变化会通过 buildCaptionOptions 即时反映到 AICaptionComponent 的渲染中。
// —— ⑤ 字幕场景卡 + 写入演示音频 ——
Column({ space: 8 }) {
List({ space: 8 }) {
ForEach(this.sceneList, (scene: CaptionScene) => {
ListItem() {
Row({ space: 10 }) {
Text('🗣').fontSize(16)
Column({ space: 3 }) {
Text(scene.scene).fontSize(12).fontColor(COLORS.title)
Text(scene.desc).fontSize(9).fontColor(COLORS.text3)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
Text(`${langName(scene.src)}→${langName(scene.tgt)}`).fontSize(9)
.fontColor(COLORS.blue)
}.width('100%').padding(10).backgroundColor(COLORS.dark).borderRadius(10)
.onClick(() => { this.applyScene(scene); })
}
}, (scene: CaptionScene) => `scene-${scene.scene}`)
}.width('100%').scrollBar(BarState.Off)
Button('写入演示音频(640 字节 PCM 块)').fontSize(11).height(34)
.backgroundColor(COLORS.purple).fontColor(COLORS.title).width('100%')
.onClick(() => { this.feedAudioStream(); })
}.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(14)
字幕场景卡遍历 sceneList 展示五种预设场景,每行显示场景名、说明和推荐语言组合(如"中文→中文"、“英文→中英双语”)。点击调用 applyScene 方法自动应用推荐的语言组合。底部的"写入演示音频"按钮调用 feedAudioStream 方法——该方法生成 640 字节的 PCM 音频块(16kHz/16bit/单声道,约 20ms 音频),内部使用正弦波公式 sin(2π·440·t) 生成 440Hz 标准音的波形数据,通过 captionController.writeAudio 写入字幕组件触发实时转写。
十三、Tab4 复盘分析
复盘 Tab 是练习记录管理中心,集成了统计聚合和 CRUD 全生命周期管理。
@Builder tabReview() {
Column({ space: 12 }) {
// —— 统计三卡 ——
Row({ space: 10 }) {
Column({ space: 3 }) {
Text(`${this.practiceList.length}`).fontSize(20).fontColor(COLORS.gold)
.fontWeight(FontWeight.Bold).fontFamily('monospace')
Text('累计练习(次)').fontSize(9).fontColor(COLORS.text3)
}.layoutWeight(1).alignItems(HorizontalAlign.Center).padding(12)
.backgroundColor(COLORS.card).borderRadius(12)
Column({ space: 3 }) {
Text(`${totalMinutes(this.practiceList)}`).fontSize(20).fontColor(COLORS.purple)
.fontWeight(FontWeight.Bold).fontFamily('monospace')
Text('累计时长(分钟)').fontSize(9).fontColor(COLORS.text3)
}.layoutWeight(1).alignItems(HorizontalAlign.Center).padding(12)
.backgroundColor(COLORS.card).borderRadius(12)
Column({ space: 3 }) {
Text(`${avgScore(this.practiceList)}`).fontSize(20).fontColor(COLORS.green)
.fontWeight(FontWeight.Bold).fontFamily('monospace')
Text('平均评分').fontSize(9).fontColor(COLORS.text3)
}.layoutWeight(1).alignItems(HorizontalAlign.Center).padding(12)
.backgroundColor(COLORS.card).borderRadius(12)
}.width('100%')
统计三卡用三列等宽布局展示累计练习次数(金色)、累计时长分钟(紫色)和平均评分(绿色),三个数值分别使用不同的语义色形成视觉区分。三个数值分别通过 practiceList.length、totalMinutes 聚合函数和 avgScore 聚合函数实时计算,当练习列表发生增删改时三卡数值即时更新。
// —— 练习记录进度条清单 ——
Column({ space: 8 }) {
Row() {
Text('练习记录').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Blank()
Button('新增记录').fontSize(10).height(28).backgroundColor(COLORS.purple)
.fontColor(COLORS.title).padding({ left: 10, right: 10 })
.onClick(() => { this.clearForm(); this.addModal = true; })
}.width('100%')
List({ space: 8 }) {
ForEach(this.practiceList, (p: PracticeItem, idx: number) => {
ListItem() {
Column({ space: 6 }) {
Row({ space: 8 }) {
Text(p.date).fontSize(10).fontColor(COLORS.text3).fontFamily('monospace')
Text(p.topic).fontSize(12).fontColor(COLORS.title)
.maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }).layoutWeight(1)
Text(`${p.duration}分钟`).fontSize(10).fontColor(COLORS.sub)
Text(`${p.score}`).fontSize(12).fontColor(scoreColor(p.score)).fontWeight(FontWeight.Bold)
}.width('100%')
Progress({ value: p.score, total: 100, type: ProgressType.Linear })
.width('100%').height(6).color(scoreColor(p.score)).backgroundColor(COLORS.dark)
Row({ space: 8 }) {
Text('AI 点评').fontSize(9).fontColor(COLORS.text3)
Blank()
Text('编辑').fontSize(10).fontColor(COLORS.blue)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor(COLORS.dark).borderRadius(8)
.onClick(() => { this.openEdit(idx); })
Text('删除').fontSize(10).fontColor(COLORS.red)
.padding({ left: 8, right: 8, top: 3, bottom: 3 })
.backgroundColor(COLORS.dark).borderRadius(8)
.onClick(() => { this.delIdx = idx; this.delModal = true; })
}.width('100%')
}.width('100%').padding(12).backgroundColor(COLORS.dark).borderRadius(12)
}
}, (p: PracticeItem, idx: number) => `practice-${p.date}-${p.topic}-${idx}`)
}.width('100%').scrollBar(BarState.Off)
}.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(14)
练习记录进度条清单是复盘 Tab 的核心。每条记录展示日期、主题、时长和评分,中间使用 Progress 线性进度条可视化评分(0~100 映射),进度条颜色和评分数字颜色均通过 scoreColor 函数动态着色。底部"编辑"按钮(蓝色)点击调用 openEdit 预填表单数据并打开编辑弹窗,"删除"按钮(红色)点击设置 delIdx 并打开删除确认弹窗。顶部"新增记录"按钮先调用 clearForm 清空表单再打开新增弹窗。三个弹窗的打开入口都集中在此清单中,形成了完整的 CRUD 交互闭环。
十四、Tab5 我的分析
我的 Tab 是学员个人中心,集成了学员信息渐变大卡、勋章墙横滑和月度柱状图。
@Builder tabMine() {
Column({ space: 12 }) {
// —— 学员渐变大卡 ——
Column({ space: 10 }) {
Row({ space: 12 }) {
Stack() {
Text('🎤').fontSize(28)
}.width(56).height(56).borderRadius(28).backgroundColor(COLORS.dark)
.alignContent(Alignment.Center)
Column({ space: 3 }) {
Text('星野 · 声动课堂学员').fontSize(15)
.fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Text('Lv.6 舞台新星 · 已坚持 21 天').fontSize(10).fontColor(COLORS.sub)
}.alignItems(HorizontalAlign.Start).layoutWeight(1)
}.width('100%')
Row({ space: 10 }) {
Column({ space: 2 }) {
Text('86').fontSize(16).fontColor(COLORS.gold).fontWeight(FontWeight.Bold)
Text('表达力指数').fontSize(9).fontColor(COLORS.sub)
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column().width(1).height(26).backgroundColor(COLORS.line)
Column({ space: 2 }) {
Text('124').fontSize(16).fontColor(COLORS.gold).fontWeight(FontWeight.Bold)
Text('累计分钟').fontSize(9).fontColor(COLORS.sub)
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column().width(1).height(26).backgroundColor(COLORS.line)
Column({ space: 2 }) {
Text('8').fontSize(16).fontColor(COLORS.gold).fontWeight(FontWeight.Bold)
Text('已获勋章').fontSize(9).fontColor(COLORS.sub)
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}.width('100%').padding({ top: 10, bottom: 10 })
.backgroundColor(COLORS.dark).borderRadius(12)
}.width('100%').padding(14).borderRadius(14)
.linearGradient({ angle: 135, colors: [[COLORS.purple, 0.0], [COLORS.purpleD, 1.0]] })
学员渐变大卡使用与头部横幅相同的 linearGradient 135° 紫色渐变背景。卡片分为上下两区:上区是圆形头像(🎤 Emoji 配深色圆形背景)和学员信息(姓名 + 等级 + 坚持天数),下区是三列数据展示(表达力指数 86 / 累计分钟 124 / 已获勋章 8),三列之间使用 1px 宽的分割线 Column 分隔。三列数值统一使用聚光金加粗字体,形成"聚光灯下的成就展示"视觉效果。
// —— 勋章清单行(横滑) ——
Column({ space: 8 }) {
Row() {
Text('勋章墙').fontSize(13).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Blank()
Text(`已解锁 3 / ${BADGE_LIST.length}`).fontSize(10).fontColor(COLORS.text3)
}.width('100%')
Scroll() {
Row({ space: 10 }) {
ForEach(BADGE_LIST, (badge: BadgeItem) => {
Column({ space: 5 }) {
Text(badge.icon).fontSize(24).opacity(badge.got ? 1 : 0.35)
Text(badge.name).fontSize(9)
.fontColor(badge.got ? COLORS.title : COLORS.text3)
}.width(72).padding({ top: 10, bottom: 10 }).alignItems(HorizontalAlign.Center)
.backgroundColor(COLORS.dark).borderRadius(12)
}, (badge: BadgeItem) => `badge-${badge.name}`)
}
}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).width('100%')
}.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(14)
勋章墙使用横向 Scroll 展示六枚勋章(百场练习/21天连训/即兴之星/对焦大师/双语演说/五星认证),已解锁的 got 为 true 显示完整不透明度,未解锁的 got 为 false 降低不透明度至 0.35 并使用灰紫文字,形成"已获得鲜亮、未获得暗淡"的对比效果。右上角显示解锁进度"已解锁 3 / 6"。
十五、图表卡片
图表卡片包含雷达图卡片和月度柱状图卡片,两者都通过 breath 呼吸状态实现动态视觉效果。
15.1 雷达图卡片
@Builder radarCard() {
Column({ space: 8 }) {
Row() {
Text('表达力五维评估').fontSize(14).fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Blank()
Text(this.breath ? 'AI 复盘中 ●' : 'AI 复盘中 ○').fontSize(10)
.fontColor(this.breath ? COLORS.gold : COLORS.text3)
}.width('100%')
Row() {
Canvas(this.radarCtx).width(230).height(230)
.onReady(() => { this.drawRadar(); })
}.width('100%').justifyContent(FlexAlign.Center)
}.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(14)
}
雷达图卡片使用 Canvas 组件承载五维雷达图绘制。Canvas 接收 radarCtx(CanvasRenderingContext2D 实例)作为绘制上下文,onReady 回调在 Canvas 准备就绪时调用 drawRadar 方法执行首次绘制。标题右侧的"AI 复盘中 ●/○"标记绑定 breath 状态,实心圆和空心圆每秒翻转一次,暗示 AI 正在实时分析表达力数据。
drawRadar 方法的绘制流程分为五步:第一步绘制三层背景多边形(从内到外逐渐放大,使用分割线色描边),第二步绘制五条从中心到顶点的轴线,第三步绘制数据多边形(使用舞台紫填充并设 globalAlpha 为 0.3 实现半透明效果,描边为不透明紫色),第四步在数据多边形顶点绘制金色数据点,第五步在半径外圈绘制五维标签文字。breath 状态通过 wob 变量(±0.03)联动数据值产生微波动,模拟 AI 实时复盘时数据微小变化的动态感。
15.2 月度柱状图卡片
@Builder chartCard() {
Column({ space: 10 }) {
Row() {
Text('近 6 个月练习时长(分钟)').fontSize(13)
.fontColor(COLORS.title).fontWeight(FontWeight.Bold)
Blank()
Text('柱高呼吸联动').fontSize(9).fontColor(COLORS.text3)
}.width('100%')
Row({ space: 10 }) {
ForEach(MONTH_IDX, (mi: number) => {
Column({ space: 6 }) {
Text(`${PRACTICE_VAL[mi]}`).fontSize(9).fontColor(COLORS.sub)
Column().width(22)
.height(16 + PRACTICE_VAL[mi] * (this.breath ? 0.72 : 0.66))
.borderRadius({ topLeft: 4, topRight: 4 })
.backgroundColor(mi % 2 === 0 ? COLORS.purple : COLORS.purpleD)
Text(MONTH_NAME[mi]).fontSize(9).fontColor(COLORS.text3)
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
}, (mi: number) => `month-${mi}`)
}.width('100%').height(150).alignItems(VerticalAlign.Bottom)
}.width('100%').padding(12).backgroundColor(COLORS.card).borderRadius(14)
}
月度柱状图使用纯 ArkUI 组件(Column + ForEach)实现而非 Canvas 绘制。六根柱子对应 03~08 月的练习时长数据(42/55/61/58/73/84 分钟),柱高公式为 16 + PRACTICE_VAL[mi] * (breath ? 0.72 : 0.66),breath 翻转时系数在 0.72 和 0.66 间切换使柱高产生约 6% 的呼吸波动。柱子颜色按 mi % 2 交替使用 purple 和 purpleD 两档紫色,形成视觉节奏感。整个 Row 设 alignItems(VerticalAlign.Bottom) 使六根柱子底部对齐,模拟传统柱状图的基线对齐效果。
十六、底部 Tab 栏
@Builder tabBar() {
Row() {
ForEach(TAB_LIST, (tab: TabMeta, idx: number) => {
Column({ space: 3 }) {
Text(tab.icon).fontSize(18).opacity(this.currentTab === idx ? 1 : 0.6)
Text(tab.label).fontSize(9)
.fontColor(this.currentTab === idx ? COLORS.tabOn : COLORS.text3)
.fontWeight(this.currentTab === idx ? FontWeight.Bold : FontWeight.Normal)
}.layoutWeight(1).alignItems(HorizontalAlign.Center)
.padding({ top: 6, bottom: 6 })
.onClick(() => { this.currentTab = idx; })
}, (tab: TabMeta, idx: number) => `tab-${tab.label}-${idx}`)
}.width('100%').backgroundColor(COLORS.card).border({ width: { top: 1 }, color: COLORS.line })
}
底部 Tab 栏使用 Row + ForEach 渲染六个等宽 Tab 项,每项 layoutWeight(1) 等分宽度。选中态的三重视觉反馈:图标不透明度从 0.6 提升至 1.0、文字颜色从 text3 灰紫切换为 tabOn 聚光金、字重从 Normal 切换为 Bold。Tab 栏顶部使用 border({ width: { top: 1 }, color: COLORS.line }) 添加 1px 分割线与内容区分隔。点击事件直接设置 currentTab 状态变量触发内容区的条件分支重新渲染。
十七、弹窗系统
弹窗系统由通用遮罩层和三个业务弹窗组成,采用 Stack 层叠实现覆盖整屏的效果。
@Builder modalOverlay(onClose: () => void) {
Column().width('100%').height('100%').backgroundColor(COLORS.mask)
.onClick(() => { onClose(); })
}
modalOverlay 是通用遮罩层构建器,接收一个 onClose 回调函数。遮罩层是一个铺满全屏的 Column,背景色为半透黑 rgba(0,0,0,0.6),点击任意区域触发 onClose 回调关闭弹窗。这种"遮罩点击关闭"的交互模式是移动端弹窗的标准范式。
@Builder panelAdd(onClose: () => void) {
Stack() {
this.modalOverlay(onClose)
Column({ space: 12 }) {
Text('新增练习记录').fontSize(15)
.fontColor(COLORS.title).fontWeight(FontWeight.Bold).width('100%')
TextInput({ placeholder: '日期(如 08-30)', text: this.formDate })
.fontSize(12).height(40).backgroundColor(COLORS.dark).borderRadius(10)
.fontColor(COLORS.title)
.onChange((v: string) => { this.formDate = v; })
// ... 主题、时长、评分三个 TextInput 同构
Row({ space: 10 }) {
Button('取消').fontSize(12).height(38).backgroundColor(COLORS.dark)
.fontColor(COLORS.sub).layoutWeight(1)
.onClick(() => { onClose(); })
Button('保存记录').fontSize(12).height(38).backgroundColor(COLORS.purple)
.fontColor(COLORS.title).layoutWeight(1)
.onClick(() => { this.confirmAdd(); onClose(); })
}.width('100%')
}.width('86%').padding(16).backgroundColor(COLORS.card).borderRadius(14)
}.alignContent(Alignment.Center).width('100%').height('100%')
}
新增弹窗 panelAdd 使用 Stack 叠加遮罩层和内容卡。内容卡宽度 86% 居中显示,包含标题和四个 TextInput 输入框(日期/主题/时长/评分),每个输入框的 onChange 回调将值写入对应的 formXxx 状态变量。底部"取消"按钮调用 onClose 关闭弹窗,“保存记录"按钮调用 confirmAdd 方法创建新 PracticeItem 实例 unshift 到列表头部再关闭弹窗。confirmAdd 方法对空值做了默认值兜底处理:日期为空默认"08-29”、主题为空默认"自由练习"、时长非法默认 15、评分非法默认 70。
编辑弹窗 panelEdit 与新增弹窗结构同构,区别在于打开前由 openEdit 方法预填当前记录的数据到表单字段,保存时调用 confirmEdit 方法对现有 PracticeItem 实例做字段级更新(因 @Observed 装饰器,字段赋值即时触发 UI 刷新)。删除弹窗 panelDel 更简洁,只展示删除确认文案和"再想想/确认删除"两个按钮,确认删除调用 confirmDel 方法通过 splice 移除指定下标的记录。
十八、功能模块对比表
| Tab 模块 | 核心能力 | 关键状态变量 | 主要 Builder | 技术特性归属 |
|---|---|---|---|---|
| 舞台 | 课程浏览 + 表达力评估 | lessonList, breath | tabStage, lessonBigCard, radarCard, lessonRows | Canvas 雷达图绘制 |
| 相机 | 影随人动跟拍 | sessionMode, framingState, framingSupported, framingOn | tabCamera, chainRow | Camera Kit VideoSession AUTO_FRAMING |
| 对焦 | 手动对焦精控 | focusSupported, focusDistance, focusReadback, focusResult, focusRecords | tabFocus | Camera Kit PhotoSession 手动对焦三接口 |
| 字幕 | AI 字幕配置 + 音频写入 | captionShown, srcLang, tgtLang, captionSize, captionColor, captionFed | tabCaption | Speech Kit AI 字幕四新字段 + writeAudio |
| 复盘 | 练习记录 CRUD | practiceList, addModal, editModal, delModal | tabReview, panelAdd, panelEdit, panelDel | @Observed 字段级刷新 + CRUD 全生命周期 |
| 我的 | 学员信息 + 数据可视化 | breath | tabMine, chartCard | Column+ForEach 柱状图 + linearGradient 渐变 |
从对比表可以看出,六大 Tab 模块各自承担了明确的技术职责:舞台和我的 Tab 偏重视觉呈现(Canvas 绘制、渐变、柱状图),相机和对焦 Tab 偏重系统能力调用(Camera Kit 双 Session 双通道),字幕 Tab 偏重组件配置(Speech Kit 四字段联动),复盘 Tab 偏重数据管理(CRUD 全生命周期)。breath 呼吸状态是跨 Tab 共享的视觉纽带,同时驱动雷达图微波动和柱状图柱高波动,使整个应用在静态展示时也保持"活着"的动态感。
十九、总结与展望
本平台以"声动课堂"为产品定位,以"舞台紫 + 聚光金"为视觉基调,构建了一个覆盖"课程浏览→演讲跟拍→精确对焦→AI 字幕→练习复盘→个人成长"完整闭环的演讲口才训练应用。从工程实现角度,平台成功融合了 HarmonyOS 6.1.1 的三大前沿特性:Camera Kit 的 VideoSession + PhotoSession 双 Session 架构实现了影随人动和手动对焦的能力隔离与互斥切换,通过 sessionMode 三态状态机确保同一时刻只有一个 Session 存活;Speech Kit 的 AI 字幕组件通过 buildCaptionOptions 方法将四个状态字段动态组装为配置对象,实现了"状态变化即配置更新"的响应式体验;Canvas 绘制通过 breath 呼吸状态联动数据值微波动,使雷达图和柱状图都具备"AI 实时复盘"的动态视觉语义。
从架构设计角度,平台采用了"单组件多 Builder"的扁平架构而非嵌套子组件。这种选择的优势在于状态流转路径最短——所有状态变量声明在组件顶层,任何 Tab 的操作结果都能直接反映到其他 Tab 的视图中,无需通过 @Prop/@Link/@Provide/@Consume 等跨组件通信机制。代价是组件体量较大(约 1760 行代码),但通过 @Builder 方法将 UI 结构按 Tab 拆解为独立构建函数,可读性和可维护性得到了有效保障。弹窗系统采用 Stack 层叠 + 条件渲染 + 通用遮罩层的模式,三个弹窗共享同一套遮罩交互逻辑,通过 onClose 回调函数实现关闭行为的参数化注入。
展望未来,平台可在以下方向持续演进。第一,Camera Kit 能力链可进一步整合 BEAUTY 美颜和 PORTRAIT 人像效果,当前效果枚举卡仅展示枚举信息但未提供切换入口,后续可增加 Toggle 开关让用户在影随人动同时叠加美颜效果。第二,Speech Kit 的 writeAudio 当前使用正弦波演示数据,后续可接入真实麦克风采集的 PCM 音频流,实现真正的实时语音转字幕。第三,Canvas 雷达图当前使用静态数据,后续可将 RADAR_VALUES 改为 @State 状态变量,在每次练习结束后由 AI 评估引擎动态更新五维数值,使雷达图真正反映用户的表达力成长轨迹。第四,练习记录的持久化当前依赖内存数组,后续可接入关系型数据库或分布式数据管理实现跨设备同步。第五,月度柱状图当前使用静态 PRACTICE_VAL 数组,后续可从练习记录中按月份聚合计算真实时长,使柱状图与复盘数据保持一致。这些演进方向将使平台从"特性演示"走向"产品落地",真正成为演讲口才训练的数字化智能陪练。
附录: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)