HarmonyOS 6.1 全场景实战|《灵犀厨房》实战(十八):【手表协同】烹饪计时器流转至智能手表------手腕掌控烹饪节奏
摘要 :上一篇我们为菜谱详情页装上了"耳朵"------接入 CoreSpeechKit 语音识别实现了免提声控启动播报。但烹饪中还有一个高频痛点:计时 。炖牛肉要 40 分钟,蒸鱼要 8 分钟,你不可能一直盯着手机。如果计时器能流转到手表上------手腕一抬就看到倒计时,到点了轻震提醒------那才是真正的"云端厨房"。本篇,我们将接入 HarmonyOS 6.1.0(API 23)的分布式设备协同 能力,为《灵犀厨房》的厨电控制页实现计时器从手机流转至智能手表的全链路:手机端一键发送→手表端接管倒计时→倒计时最后 10 秒渐强震动→计时完成重复震动提醒。严格遵循 API 23 规范,代码即文档。
一、引言与系列定位
经过第 13 篇的厨电模拟和第 17 篇的声控播报,你的《灵犀厨房》已经成为一台强大的烹饪助手。但你很快会发现一个尴尬的现实:
| 场景 | 问题 | 解决 |
|---|---|---|
| 电磁炉定时 15 分钟煮汤 | 离开厨房就看不到倒计时 | 流转至手表,手腕上实时显示 |
| 烤箱烘烤 40 分钟 | 在客厅看电视,忘记还剩多久 | 抬手看手表,剩余时间一目了然 |
| 计时器响了但手机在隔壁 | 铃声听不到,菜烧糊了 | 手表震动提醒,戴在手上不会错过 |
| 倒计时最后 10 秒 | 需要紧张地等待关火 | 手表渐强震动,倒计时节奏可感知 |
设计决策:为何不在手表上操作计时器的"开始/暂停/停止"?
经过对烹饪场景的分析,我们发现:在厨房里操作手机已经满手油污,操作手表的小屏幕同样不便。真正的痛点不是"用哪只手操作",而是"不用手操作"。因此,本篇的设计原则是:
- 手机端发起计时:在厨电控制页点击"定时 5 分钟"或"定时 15 分钟"(第 13 篇已实现),计时器在手机上运行
- 一键流转至手表:计时器运行后,点击"⌚ 流转至手表",手机端计时停止,手表接管倒计时
- 手表专职提醒:倒计时最后 10 秒渐强震动 → 计时完成重复震动 → 手腕一抬就知道该关火了
- 手机端回看状态:手机端页面顶部显示"电磁炉 计时中 · HUAWEI WATCH GT 4 · 剩余 03:15"
这就是手表协同的核心价值------从"盯着屏幕"变为"手腕感知"。不是用手表控制厨电,而是让计时跟着你的手腕走。
二、核心原理与底层机制深度解读
2.1 HarmonyOS 分布式手表通信:三条技术路径
HarmonyOS 6.1.0(API 23)为手机与手表的协同提供了三条技术路径:
⌚ 手表端
📱 手机端
选择路径
通信路径
路径 A
@kit.WearEngine
wearEngine.sendMessage()
路径 B
@kit.DistributedServiceKit
continuationManager
路径 C
自定义蓝牙通道
(低功耗)
灵犀厨房 App
WatchTimerManager
WatchServiceHelper
手表 App/服务
倒计时引擎
震动反馈
| 路径 | 适用场景 | 优势 | 劣势 | 本篇选型 |
|---|---|---|---|---|
| A: WearEngine | 手表专属消息通道 | 延迟低(~50ms),支持后台保活 | 仅限手表设备 | ✅ 模拟实现 |
| B: Continuation | 跨设备任务迁移 | 通用性强,支持任意设备 | 延迟较高(~200ms),需用户确认 | 备选 |
| C: 自定义蓝牙 | 极端省电场景 | 功耗最低 | 开发成本高,需自行维护连接 | 不考虑 |
为什么选 WearEngine 模拟实现?
@kit.WearEngine 是 HarmonyOS 专门为手表协同设计的 Kit,提供了消息收发、设备状态查询、震动控制等 API。在不具备真实手表开发环境的情况下,我们采用与第 14 篇分布式流转一致的策略:用模拟代码完整保留 API 23 的真实接口签名,后续接入真实手表仅需替换服务层实现,上层 UI 零改动。
2.2 计时器流转的状态机设计
这是本篇最核心的设计------手机与手表之间的计时器流转不是一个简单的数据拷贝,而是一个涉及设备间状态同步的分布式状态机:
无计时器
用户点击"定时5分钟"
点击"⌚流转至手表"
手表确认接收
流转失败→回退
手表端倒计时结束
用户取消手表计时
用户清除完成状态
IDLE
COUNTING_ON_PHONE
TRANSFERRING_TO_WATCH
COUNTING_ON_WATCH
COMPLETED_ON_WATCH
手机端 setInterval 每秒递减
手表端接管倒计时
手机端通过回调显示进度
手表震动已触发
手机端显示"计时完成"
核心机制 :流转不是"同步两份数据",而是单源倒计时 + 状态上报。计时器在任一时刻只在一个设备上运行(手机或手表),另一端通过回调被动接收状态更新。这避免了双端倒计时不一致的问题。
2.3 手表震动反馈的三段式设计
倒计时从"数字"变成"触觉",需要分阶段提醒:
| 阶段 | 触发条件 | 震动模式 | 用户感知 |
|---|---|---|---|
| 安静期 | 剩余 > 10 秒 | 无震动 | 手腕无感,正常看倒计时数字 |
| 预警期 | 剩余 10 秒 | RAMP_UP 渐强震动(500ms) |
"快好了,准备好关火" |
| 完成期 | 剩余 0 秒 | REPEAT 重复震动(3 秒) |
"到了!马上去关火!" |
这就是震动反馈的三段式设计:安静 → 预警 → 告警,让用户不依赖屏幕就能感知计时节奏。
三、架构设计 / 核心逻辑图解
3.1 手表协同的四层架构
🏗️ 基础设施层
⚙️ 服务能力层
🧠 业务功能层
🎨 用户交互层
KitchenDevicePage
⌚ 手表状态指示条
设备卡片中的流转按钮
计时完成通知
WatchTimerManager
流转状态机
手表回调桥接
transferToWatch / cancelFromWatch
KitchenDeviceSimulator
(第13篇已实现)
计时器引擎
WatchServiceHelper
手表设备发现
计时器数据传输
震动控制
倒计时模拟
@kit.WearEngine
(模拟)
消息收发 + 设备管理
@kit.DistributedServiceKit
(可选)
continuationManager
设计原则 :手表协同(WatchTimerManager)与计时器引擎(KitchenDeviceSimulator)是独立的 Business 层模块。WatchTimerManager 的职责是"协调流转 + 状态上报 + 回调通知 UI",由 UI 层(KitchenDevicePage)的 handleWatchTransfer 方法来实际触发停止手机端计时和发起手表流转。这样:
- 单一职责:WatchTimerManager 只管"何时流转、流转到哪、流转状态",UI 层决定"流转前停止哪些手机端计时器、流转后显示什么状态"。
- 双端解耦 :
transferToWatch时不依赖 KitchenDeviceSimulator 的内部状态------它只接收remainingSeconds(剩余秒数)作为参数,手表端独立倒计时,与手机端彻底隔离。 - 震动独立控制 :手表震动逻辑在
WatchServiceHelper层,不污染业务层。
3.2 流转时序图
⌚ 手表 WatchServiceHelper WatchTimerManager KitchenDeviceSimulator KitchenDeviceViewModel KitchenDevicePage 👤 用户 ⌚ 手表 WatchServiceHelper WatchTimerManager KitchenDeviceSimulator KitchenDeviceViewModel KitchenDevicePage 👤 用户 用户在厨电控制页 手机端倒计时开始 手机端计时器停止(保留剩余秒数) 顶部显示"电磁炉 计时中 · HUAWEI WATCH GT 4" 更新手表倒计时显示 loop [每秒] 顶部显示"电磁炉 计时完成!" 点击"定时 5分钟" startTimer(deviceId, 300) startCookingTimer(deviceId, 300) 手机端显示 04:59, 04:58... 点击"⌚ 流转至手表" stopTimer(deviceId) transferToWatch(deviceId, deviceName, remaining, total) 状态 → TRANSFERRING_TO_WATCH transferTimer(context) 模拟蓝牙传输 (~250ms) 发送计时数据 手表端启动倒计时 流转成功 状态 → COUNTING_ON_WATCH onTransferred(context) mockWatchRemaining-- onWatchTimerStateChange(COUNTING, remaining) onWatchTick(remainingSeconds, totalSeconds) 最后10秒 → 渐强震动 remaining=0 重复震动 3秒 状态 → COMPLETED onWatchComplete()
关键时序:手机端停止计时 → 再发起手表流转 → 手表端启动倒计时。这个顺序很重要------如果先流转再停止,可能出现双端同时计时的竞态。
四、实战:为厨电控制页装上"手腕大脑"
Step 1:WatchServiceHelper ------ 封装手表通信
新建 services/WatchServiceHelper.ets。核心任务:将手表的设备发现、消息传输、震动控制封装为可复用的服务层,完整保留 API 23 的真实接口签名。
typescript
// services/WatchServiceHelper.ets
// 所属层:服务能力层 (Service Layer)
// 职责:封装 @kit.WearEngine 的手表通信能力
// 版本:v1 --- 第18篇,基于 API 23
// ========== 真实手表 API(上架后取消注释) ==========
// import { wearEngine } from '@kit.WearEngine';
// import { distributedDeviceManager } from '@kit.DistributedServiceKit';
/** 手表设备信息 */
export interface WatchDevice {
deviceId: string; // 设备唯一 ID
name: string; // 'HUAWEI WATCH GT 4'
model: string; // 'PNX-B19'
status: WatchConnectionStatus;
osVersion: string;
batteryLevel: number; // 0-100
isWorn: boolean; // 是否穿戴中
}
/** 计时器流转上下文:手机→手表传输的计时数据 */
export interface WatchTimerContext {
recipeName: string;
deviceName: string; // 厨电名称,如"电磁炉"
remainingSeconds: number; // 剩余秒数
totalSeconds: number; // 总秒数
timestamp: number;
vibrationPattern?: WatchVibrationPattern;
}
/** 手表震动模式 */
export enum WatchVibrationPattern {
SHORT = 'short', // 单次短震
REPEAT = 'repeat', // 重复短震(计时到点)
RAMP_UP = 'rampUp' // 渐强震动(最后10秒)
}
/** 手表端计时器状态 */
export enum WatchTimerState {
IDLE = 'idle', RECEIVING = 'receiving',
COUNTING = 'counting', COMPLETED = 'completed', CANCELLED = 'cancelled'
}
export class WatchServiceHelper {
private watchDevice: WatchDevice | null = null;
private timerState: WatchTimerState = WatchTimerState.IDLE;
private mockWatchRemaining: number = 0;
// ★ 初始化:扫描并连接手表
async initialize(): Promise<WatchDevice | null> {
await this.delay(400); // 模拟蓝牙扫描延迟
this.watchDevice = {
deviceId: 'WA:TC:H0:00:00:01',
name: 'HUAWEI WATCH GT 4',
model: 'PNX-B19',
status: WatchConnectionStatus.CONNECTED,
osVersion: 'HarmonyOS 4.2.0',
batteryLevel: 85,
isWorn: true
};
return this.watchDevice;
}
/** ★ 检查手表是否可用(已连接 + 穿戴中) */
isWatchAvailable(): boolean {
return this.watchDevice !== null
&& this.watchDevice.status === WatchConnectionStatus.CONNECTED
&& this.watchDevice.isWorn;
}
/** ★ 将计时器流转至手表 */
async transferTimer(context: WatchTimerContext): Promise<boolean> {
if (!this.isWatchAvailable()) return false;
// 模拟数据传输延迟
await this.delay(250);
// 启动模拟手表端倒计时
this.startMockWatchCountdown(context.remainingSeconds);
return true;
}
/** ★ 启动模拟手表端倒计时(每秒递减 + 震动逻辑) */
private startMockWatchCountdown(seconds: number): void {
this.mockWatchRemaining = seconds;
this.mockWatchInterval = setInterval(() => {
if (this.mockWatchRemaining <= 0) {
// 计时完成 → 触发手表震动
this.triggerVibration(WatchVibrationPattern.REPEAT, 3000);
this.notifyTimerCompleted();
return;
}
this.mockWatchRemaining--;
// 最后 10 秒 → 渐强震动提醒
if (this.mockWatchRemaining === 10) {
this.triggerVibration(WatchVibrationPattern.RAMP_UP, 500);
}
this.notifyWatchTimerStateChange(WatchTimerState.COUNTING, this.mockWatchRemaining);
}, 1000);
}
}
export const watchServiceHelper: WatchServiceHelper = new WatchServiceHelper();
核心点解读:
isWorn检查 :如果手表摘下放在桌上(isWorn=false),震动提醒将失效------流转前必须确认手表佩戴状态,避免"手表在桌上震,人在厨房听不到"。- 模拟倒计时的真实性 :
setInterval每秒触发一次递减,完全模拟真实手表端的计时节奏。后续接入真机时,倒计时逻辑移至手表 App 内执行,手机端通过onMessage回调被动接收进度。- 全局单例导出 :
export const watchServiceHelper确保整个应用共用一个手表连接实例。
Step 2:WatchTimerManager ------ 流转状态机 + 业务逻辑
新建 business/WatchTimerManager.ets。核心任务:(1)定义 6 种流转状态;(2)实现 transferToWatch 核心方法,协调"停止手机计时→发起手表流转"的全流程;(3)桥接 Service 层回调到 Business 层语义。
typescript
// business/WatchTimerManager.ets
// 所属层:业务功能层 (Business Layer)
// 职责:管理计时器在手机与手表之间的流转状态
/** 手表计时器管理器状态枚举 */
export enum TimerTransferState {
IDLE = 'idle', // 无计时器运行
COUNTING_ON_PHONE = 'countingOnPhone', // 仅在手机端
TRANSFERRING_TO_WATCH = 'transferringToWatch', // 流转中
COUNTING_ON_WATCH = 'countingOnWatch', // 在手表端
COMPLETED_ON_WATCH = 'completedOnWatch', // 手表完成
CANCELLED = 'cancelled'
}
export class WatchTimerManager {
private state: TimerTransferState = TimerTransferState.IDLE;
private preservedSeconds: number = 0; // 流转前保存的剩余秒数
private totalSeconds: number = 0;
/** ★ 核心方法:将计时器流转至手表 */
async transferToWatch(
deviceId: string, deviceName: string, recipeName: string,
remainingSeconds: number, totalSeconds: number
): Promise<boolean> {
if (!watchServiceHelper.isWatchAvailable()) {
this.notifyError('手表未连接或未佩戴,请戴好手表后重试');
return false;
}
// 保存上下文(用于从手表收回时恢复)
this.preservedSeconds = remainingSeconds;
this.totalSeconds = totalSeconds;
this.setState(TimerTransferState.TRANSFERRING_TO_WATCH);
const context: WatchTimerContext = {
recipeName, deviceName, remainingSeconds, totalSeconds,
timestamp: Date.now()
};
const success = await watchServiceHelper.transferTimer(context);
if (success) {
this.setState(TimerTransferState.COUNTING_ON_WATCH);
} else {
this.setState(TimerTransferState.COUNTING_ON_PHONE); // 回退
}
return success;
}
/** 从手表取消计时器 */
async cancelFromWatch(resumeOnPhone: boolean = true): Promise<void> {
await watchServiceHelper.cancelWatchTimer();
if (resumeOnPhone && this.preservedSeconds > 0) {
this.setState(TimerTransferState.COUNTING_ON_PHONE);
} else {
this.setState(TimerTransferState.IDLE);
}
}
}
export const watchTimerManager: WatchTimerManager = new WatchTimerManager();
核心点解读:
- 流转失败的状态回退 :如果
transferTimer失败(如蓝牙中断),状态立即从TRANSFERRING_TO_WATCH回退到COUNTING_ON_PHONE------用户不需要额外操作,手机端计时器继续运行。preservedSeconds预留恢复能力:保留流转前的剩余秒数,即使流转到手表后断连,也能在取消时回到手机端重新计时。- 状态变更全部通过
setState做日志记录:每个状态迁移都有控制台日志,方便调试分布式场景下的时序问题。
Step 3:改造 KitchenDevicePage.ets,集成手表流转 UI
在厨电控制页中新增手表状态管理和流转逻辑。
(1)新增状态变量
typescript
// ★ mock 场景下手表始终可用,初始 true 确保首帧即显示手表电量 badge
@Local isWatchConnected: boolean = true;
@Local watchDeviceName: string = 'HUAWEI WATCH GT 4';
@Local watchDeviceBattery: number = 85;
@Local transferState: TimerTransferState = TimerTransferState.IDLE;
@Local watchRemainingSeconds: number = 0;
@Local watchTimerDeviceName: string = '';
// ★ 记录各设备的原始计时总秒数(流转时传 totalSeconds,而非当前剩余秒数)
private deviceTimerTotal: Map<string, number> = new Map();
(2)生命周期集成
typescript
async aboutToAppear(): Promise<void> {
this.vm.loadDevices();
await this.initWatchManager(); // ★ 初始化手表连接
}
aboutToDisappear(): void {
this.vm.destroy();
watchTimerManager.destroy(); // ★ 销毁手表连接
}
(3)手表初始化和回调注册
typescript
private async initWatchManager(): Promise<void> {
watchTimerManager.registerCallbacks({
onStateChange: (state) => { this.transferState = state; },
onWatchTick: (remaining, total) => {
this.watchRemainingSeconds = remaining;
this.watchTotalSeconds = total;
},
onWatchComplete: () => { /* 显示计时完成状态 */ },
onWatchConnectionChange: (status, device) => {
this.isWatchConnected = (status === WatchConnectionStatus.CONNECTED);
if (device) {
this.watchDeviceName = device.name;
this.watchDeviceBattery = device.batteryLevel;
}
}
});
await watchTimerManager.initialize();
const device = watchTimerManager.getWatchDevice();
if (device) { /* this.isWatchConnected already true; only update name/battery */ }
}
(4)handleWatchTransfer ------ 发起流转的核心方法
typescript
private async handleWatchTransfer(
deviceId: string, deviceName: string,
remainingSeconds: number, totalSeconds: number
): Promise<void> {
console.info(`[KitchenDevicePage] 🔄 流转计时器: ${deviceName}, 剩余 ${remainingSeconds}秒`);
this.watchTimerDeviceName = deviceName;
this.watchRemainingSeconds = remainingSeconds;
this.watchTotalSeconds = totalSeconds;
// ★ 先停止手机端计时器
this.vm.stopTimer(deviceId);
// 执行手表流转
const success = await watchTimerManager.transferToWatch(
deviceId, deviceName, '', remainingSeconds, totalSeconds
);
if (!success) {
console.error('[KitchenDevicePage] 流转至手表失败');
this.watchTimerDeviceName = '';
}
}
(5)UI 层:手表流转状态指示条
typescript
// ★ 流转中 / 计时中状态
if (this.transferState === TimerTransferState.COUNTING_ON_WATCH) {
Row() {
Text('⌚').fontSize(18)
Column() {
Text(`${this.watchTimerDeviceName} 计时中`)
.fontSize(13).fontColor('#FF6B35').fontWeight(FontWeight.Medium)
Text(`${this.watchDeviceName} · 剩余 ${this.formatWatchTimer(this.watchRemainingSeconds)}`)
.fontSize(11).fontColor('#999')
}
Blank()
Button('取消手表计时').onClick(() => { /* ... */ })
}
.backgroundColor('#FFF3E0').borderRadius(10)
}
// ★ 计时完成状态
if (this.transferState === TimerTransferState.COMPLETED_ON_WATCH) {
Row() {
Text('⌚').fontSize(18)
Text(`${this.watchTimerDeviceName} 计时完成!`)
.fontSize(13).fontColor('#4CAF50')
Button('清除')
}
.backgroundColor('#F0FFF0').borderRadius(10)
}
(6)DeviceControlCard 新增流转按钮
在 DeviceControlCard 中新增 @Event onWatchTransfer 并在计时区域渲染按钮:
typescript
// ★ @Event 声明(与 onTimerStop 等并列)
@Event onWatchTransfer: () => void = () => {};
// build() 中计时区域新增按钮
if (this.device.timerSeconds > 0) {
Row() {
Text('⏱️').fontSize(18)
Text(this.formatTimer(this.device.timerSeconds))
.fontSize(22).fontWeight(FontWeight.Bold).fontColor('#FF6B35').fontFamily('monospace')
.margin({ left: 8 })
Blank()
// ★ "⌚ 流转至手表"按钮 --- onClick 直接传 @Event 引用
Button() {
Text('⌚ 流转至手表').fontSize(11).fontColor('#FF6B35')
}
.height(30).backgroundColor('#FFF0E6').borderRadius(8)
.margin({ right: 6 })
.onClick(this.onWatchTransfer)
Button('停止计时')
.fontSize(12).height(30).backgroundColor('#F44336').fontColor(Color.White).borderRadius(8)
.onClick(() => this.onTimerStop?.())
}
.width('100%').padding(10).backgroundColor('#FFF3E0').borderRadius(8).margin({ bottom: 12 })
}
父组件 KitchenDevicePage 在 ForEach 中绑定回调,记录原始总秒数并通过 handleWatchTransfer 执行流转:
typescript
DeviceControlCard({
device: device,
// ...其他属性...
onTimerStart: (seconds: number) => {
this.deviceTimerTotal.set(device.deviceId, seconds);
this.vm.startTimer(device.deviceId, seconds);
},
onTimerStop: () => { this.vm.stopTimer(device.deviceId); },
// ★ 绑定流转回调:从 deviceTimerTotal 取原始总秒数
onWatchTransfer: () => {
const total: number = this.deviceTimerTotal.get(device.deviceId) ?? device.timerSeconds;
console.info(`[KitchenDevicePage] ⌚ 流转: device=${device.deviceId}, name=${device.name}, remaining=${device.timerSeconds}, total=${total}`);
this.handleWatchTransfer(device.deviceId, device.name, device.timerSeconds, total);
}
})
核心点解读:
- "先停止手机端计时,再发起手表流转"的顺序至关重要 :如果反过来,可能出现手机端
setInterval仍在递减而手表端也在递减的双端竞态,导致两个设备显示的剩余时间不一致。- 手表状态指示条位于页面顶部,全局可见:不是嵌在某个设备卡片里------流转后用户切换到"设备控制"页或其他 Tab,仍然能看到手表上的计时进度。
onClick(this.onWatchTransfer)直接传@Event引用 :与 ArkUI 原生onClick签名匹配,无需额外箭头函数包装,避免?.()可选链或闭包捕获过期引用的问题。deviceTimerTotalMap 记录原始总秒数 :onTimerStart时存入原始值(300 / 900),onWatchTransfer时取出作为totalSeconds,与当前剩余秒数分开传递。
五、代码交付清单
| 文件 | 新增/修改 | 职责 |
|---|---|---|
services/WatchServiceHelper.ets |
新增 | 手表通信封装:设备发现、数据传输、震动控制、模拟手表端倒计时 |
business/WatchTimerManager.ets |
新增 | 流转状态机:6 种状态管理、transferToWatch 核心方法、Service 回调桥接 |
components/DeviceControlCard.ets |
修改 | ★ v3 新增 ⌚ 流转至手表 按钮和 @Event onWatchTransfer 回调 |
pages/KitchenDevicePage.ets |
修改 | ★ v3 新增手表连接状态、流转状态指示条、生命周期集成 |
六、设计决策
| 决策 | 选择 | 理由 |
|---|---|---|
| 流转后手机端计时 | 立即停止 | 避免双端竞态。手表是单源倒计时,手机端仅被动显示进度 |
| 手表连接可用性检查 | isWorn 穿戴检测 |
手表摘下放桌上时震动无效,必须在佩戴状态下才允许流转 |
| 流转失败恢复策略 | 状态回退到 COUNTING_ON_PHONE |
不丢失计时进度,用户不必重新设置 |
| 手表震动时机 | 最后 10 秒 + 完成 | 三段式触觉反馈:安静→预警→告警,不依赖屏幕 |
| 手机端流转状态位置 | 页面顶部全局指示条 | 用户切换到其他 Tab 也能看到手表上的计时进度 |
preservedSeconds |
Business 层保存 | 流转时记录的剩余秒数,供取消流转时恢复使用 |
| 是否支持手表操作"暂停" | 不支持 | 设计原则:手表专职感知(看时间 + 感受震动),手机专职控制(开始/停止/流转) |
| 模拟实现策略 | API 签名对齐真实 @kit.WearEngine |
后续接入真机仅需取消注释 import 并替换 transferTimer 内部实现 |
七、运行与结果验证
7.1 操作步骤
-
部署到真机或者模拟器。
-
进入厨电控制页,点击某台电磁炉的**「定时 5 分钟」**按钮 → 手机端倒计时开始显示。

-
在计时器显示区域点击**「⌚ 流转至手表」**按钮 → 手机端计时器停止,页面顶部出现橙色状态条"电磁炉 计时中 · HUAWEI WATCH GT 4 · 剩余 04:58"。


-
等待倒计时进入最后 10 秒 → 日志显示手表渐强震动。
-
倒计时归零 → 状态条变绿"电磁炉 计时完成!",日志显示手表重复震动。

7.2 预期结果
| 操作 | 预期 UI 变化 | 预期日志 |
|---|---|---|
| 点击「定时 5 分钟」 | 设备卡片显示 "⏱️ 05:00" | [KitchenDeviceVM] 启动计时器: dev_02 → 300 秒 |
| 点击「⌚ 流转至手表」 | 手机端计时消失,顶部出现橙色条 | [KitchenDevicePage] 🔄 流转计时器 → [WatchTimerManager] 状态: countingOnPhone → transferringToWatch → countingOnWatch |
| 手表倒计时中每秒 | 橙色条实时更新剩余时间 | [WatchServiceHelper] 手表端倒计时: 287 → 286 → ... |
| 剩余 10 秒 | 日志显示渐强震动 | [WatchServiceHelper] ⏰ 倒计时最后 10 秒,手表渐强震动... |
| 倒计时归零 | 橙色条变绿"计时完成" | [WatchServiceHelper] ⌛ 手表端计时完成,已触发震动提醒 |
| 点击「清除」 | 绿色条消失,状态回 IDLE | [WatchTimerManager] 状态: completedOnWatch → idle |
7.3 控制台完整日志
[KitchenDevicePage] 初始化手表计时器服务
[WatchServiceHelper] 初始化手表服务...
[WatchServiceHelper] 发现手表: HUAWEI WATCH GT 4 (PNX-B19), 电量: 85%
[KitchenDevicePage] 手表已连接: HUAWEI WATCH GT 4
[KitchenDeviceVM] 启动计时器: dev_02 → 300 秒
[MockDS] 电磁炉 计时器: 300秒
...(手机端倒计时: 299, 298, 297...)
[KitchenDevicePage] 🔄 流转计时器: 电磁炉, 剩余 287秒
[KitchenDeviceVM] 停止计时器: dev_02
[WatchTimerManager] 状态变更: countingOnPhone → transferringToWatch
[WatchServiceHelper] ⏱️ 计时器流转至手表: "电磁炉" → 287秒
[WatchServiceHelper] ✅ 计时器已成功流转至手表,手表端开始倒计时
[WatchTimerManager] 状态变更: transferringToWatch → countingOnWatch
[KitchenDevicePage] ✅ 计时器已流转至手表
...(手表端倒计时: 286, 285, 284...)
[WatchServiceHelper] ⏰ 倒计时最后 10 秒,手表渐强震动...
[WatchServiceHelper] 📳 手表震动: 渐强震动, 500ms
...(手表端倒计时: 3, 2, 1, 0)
[WatchServiceHelper] 📳 手表震动: 重复震动, 3000ms
[WatchServiceHelper] ⌛ 手表端计时完成,已触发震动提醒
[WatchTimerManager] 状态变更: countingOnWatch → completedOnWatch
[KitchenDevicePage] ⌛ 手表计时完成!
验证要点:
- 流转后手机端的
setInterval确认已停止------不会再看到[MockDS] 电磁炉 计时器: X秒的日志。- 手表端倒计时独立运行,手机端通过
onWatchTimerStateChange回调被动更新。- 渐强震动在剩余 10 秒时准确触发,重复震动在归零时触发。
八、本阶段总结与下篇预告
今天,我们为《灵犀厨房》装上了"手腕大脑"------实现了烹饪计时器从手机流转至智能手表的完整链路:
- 封装
WatchServiceHelper:将@kit.WearEngine的手表通信 API 包装为服务层,实现设备发现、计时数据传输、三段式震动控制,完整保留 API 23 的真实接口签名。 - 构建
WatchTimerManager:定义 6 种流转状态 + 状态机,transferToWatch协调"停止手机计时→发起手表流转"的全流程,preservedSeconds预留恢复能力。 - 关键设计决策------手表专职感知,手机专职控制:流转后手机端立即停止计时,手表端独立倒计时并做震动反馈。不支持手表端操作"暂停/停止",保持"手机发起、手表感知"的简洁交互模型。
- 集成流转 UI:厨电控制页顶部全局状态指示条 + 设备卡片内"⌚ 流转至手表"按钮 + 手表连接电量显示。
现在,你在厨房给电磁炉定时 15 分钟煮汤,点一下「⌚ 流转至手表」------然后去客厅看电视。手腕一抬,剩余时间一目了然;最后 10 秒渐强震动让你从容走向厨房;计时完成,重复震动告诉你:该关火了。
但烹饪中的提醒不止计时------明天要做红烧肉但忘了买五花肉?炖汤该加盐了但正在切菜腾不出手?
下篇预告 :第 19 篇《通知系统:多设备推送提醒》。我们将接入 @kit.NotificationKit,实现购物清单到期提醒、计时完成跨设备推送、烹饪小贴士定时弹窗------让《灵犀厨房》从"被动查询"变为"主动告知"。
📚 本系列持续更新中:下一篇将实现多设备通知推送,让烹饪提醒无处不在。
🔗 专栏入口:[《HarmonyOS6.1全场景实战》合集]
📦 获取基线版本源码包 :包括第1-15篇所有代码 + 架构文档 + Flask 后端
如果你觉得这篇文章对你有帮助,请不要吝啬你的点赞 👍、收藏 ⭐ 和评论 💬。你的支持,是我继续输出高质量技术内容的全部动力。
纯血鸿蒙,手腕掌控。我们下一篇见!