HarmonyOS《柚兔学伴》项目实战11-倒计时组件封装与控制器模式
第11篇:倒计时组件封装与控制器模式
在"柚兔学伴"中,番茄钟倒计时是核心功能之一。倒计时组件需要满足:外部可设置时间、可远程重置、可监听状态变化。这就引出了一个经典的组件设计问题——如何让父组件从外部控制子组件的行为?
本篇从 TimerComponent.ets 的 202 行源码出发,详解控制器模式(Controller Pattern)在 ArkUI 中的实现。

一、控制器模式的核心思想
ArkUI 的 @Component 是一个封闭的作用域,@State、@Prop 等状态装饰器只能在组件内部修改。如果父组件想要主动触发子组件的某个行为(如重置倒计时),单纯通过属性传递是做不到的。
控制器模式的解法是:子组件在初始化时,将自身的方法注册到控制器对象上;父组件持有控制器,通过调用控制器方法间接驱动子组件。
父组件 ──调用──> Controller.reset() ──回调──> 子组件.onResetFromController()
二、TimerComponentController 实现
export class TimerComponentController {
private resetCallback: (() => void) | null = null;
setResetCallback(callback: () => void) {
this.resetCallback = callback;
}
reset() {
if (this.resetCallback) {
this.resetCallback();
}
}
}
控制器非常简洁,只做两件事:
setResetCallback:注册回调函数(由子组件调用)reset:执行回调函数(由父组件调用)
这种"注册-执行"的模式,本质是将子组件的方法引用"提升"到了控制器中,打破了组件边界对方法调用的限制。
三、TimerComponent 组件详解
3.1 属性定义
@Component
export struct TimerComponent {
controller: TimerComponentController | null = null;
@Prop @Watch('onTotalTimeChange') totalTime: number = 60;
onTotalTimeChange() {
this.resetTimer();
this.currentTime = this.totalTime;
}
onTimeUp?: () => void;
onTimeChange?: (currentTime: number) => void;
@State currentTime: number = 60;
@State isRunning: boolean = false;
private timer: number = -1;
private gradientColor: LinearGradient = new LinearGradient([
{ color: $r('app.color.color_color_1'), offset: 0.5 },
{ color: $r('app.color.color_color_2'), offset: 1.0 }
])
}
关键设计点:
| 属性 | 装饰器 | 说明 |
|---|---|---|
totalTime |
@Prop @Watch |
从父组件单向传入,变化时自动重置计时器 |
currentTime |
@State |
当前剩余时间,驱动 UI 刷新 |
isRunning |
@State |
运行状态,控制按钮文字和颜色 |
onTimeUp |
普通属性 | 回调函数,时间归零时通知父组件 |
onTimeChange |
普通属性 | 回调函数,每秒通知父组件时间变化 |
controller |
普通属性 | 控制器实例,父组件通过它远程控制 |
@Prop 保证 totalTime 从父组件单向流入;@Watch 则监听其变化,当外部重新设置时间时自动重置。
3.2 控制器绑定
aboutToAppear() {
this.currentTime = this.totalTime;
if (this.controller) {
this.controller.setResetCallback(this.onResetFromController.bind(this));
}
}
onResetFromController() {
this.resetTimer();
this.currentTime = this.totalTime;
}
aboutToAppear 是组件的生命周期回调,在组件首次创建时执行。此时将 onResetFromController 方法通过 bind(this) 绑定正确的 this 指向后,注册到控制器。
为什么需要 .bind(this)? 因为 setResetCallback 存储的是函数引用,当控制器调用 resetCallback() 时,this 可能丢失。bind 确保无论谁调用这个函数,this 都指向组件实例。
3.3 倒计时核心逻辑
toggleTimer() {
if (this.isRunning) {
clearInterval(this.timer);
this.isRunning = false;
} else {
this.isRunning = true;
this.timer = setInterval(() => {
if (this.currentTime > 0) {
this.currentTime--;
if (this.onTimeChange) {
this.onTimeChange(this.currentTime);
}
} else {
this.isRunning = false;
clearInterval(this.timer);
if (this.onTimeUp) {
this.onTimeUp();
}
}
}, 1000);
}
}
每 1000ms(1秒)执行一次:
- 剩余时间 > 0:递减
currentTime,触发onTimeChange回调 - 剩余时间 = 0:停止定时器,标记为未运行,触发
onTimeUp回调
toggleTimer 是一个切换函数——运行时暂停,暂停时开始。clearInterval 确保不会产生多个定时器叠加。
3.4 重置逻辑
resetTimer() {
clearInterval(this.timer);
this.isRunning = false;
this.currentTime = this.totalTime;
}
重置操作包含三个步骤:清除定时器 → 停止运行状态 → 恢复初始时间。无论是本地重置还是通过控制器远程重置,最终都调用此方法。
四、时间格式化
formatTime(seconds: number): string {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString()
.padStart(2, '0')}`;
}
将总秒数转换为 HH:MM:SS 格式。padStart(2, '0') 确保个位数补零,如 5 显示为 05。
五、UI 构建
5.1 圆形进度条
Stack() {
Progress({
value: this.getProgress(),
total: this.totalTime,
type: ProgressType.ScaleRing
})
.width('100%')
.height('100%')
.style({
strokeWidth: 10,
})
.color($r('app.color.app_primary'))
Column() {
Text(this.formatTime(this.currentTime))
.fontSize(20)
.fontWeight(FontWeight.Bolder)
.textAlign(TextAlign.Center)
Text('剩余时间')
.fontSize(14)
.fontColor(Color.Gray)
.margin({ top: 10 })
}
}
ProgressType.ScaleRing 是圆环进度条,value 为已用时间(totalTime - currentTime),total 为总时间。Stack 布局将时间文字叠加在进度环中心。
5.2 控制按钮
Row({ space: 12 }) {
Button('重置')
.fontSize(12)
.height(30)
.backgroundColor('#F0F0F0')
.fontColor(Color.Black)
.borderRadius(25)
.onClick(() => {
this.resetTimer();
})
Button(this.isRunning ? '暂停' : '开始')
.fontSize(12)
.height(30)
.backgroundColor(this.isRunning ? $r('app.color.app_alert') : $r('app.color.app_primary'))
.fontColor(Color.White)
.borderRadius(25)
.onClick(() => {
this.toggleTimer();
})
}
按钮文字和颜色根据 isRunning 状态动态切换,实现了"开始/暂停"一体化按钮。
六、生命周期清理
aboutToDisappear() {
if (this.timer !== -1) {
clearInterval(this.timer);
}
}
组件销毁时必须清除定时器,否则会持续消耗资源并可能导致对已销毁组件的状态修改。timer 初始值为 -1,仅在有活跃定时器时清理。
七、父组件中的使用
在 TodoView 中,倒计时组件的完整使用方式:
timerController: TimerComponentController = new TimerComponentController()
@State settingTime: number = 25 * 60;
onTimerFinished = async () => {
this.alarmVisible = true
// 播放提醒音
}
onTimeChanged = (currentTime: number) => {
console.log(`剩余时间: ${currentTime}秒`);
}
// 在 build 中使用
TimerComponent({
totalTime: this.settingTime,
onTimeUp: this.onTimerFinished,
onTimeChange: this.onTimeChanged,
controller: this.timerController
})
当用户在弹窗中确认关闭闹钟时,通过控制器重置:
IBestDialog({
visible: $alarmVisible,
defaultBuilder: (): void => this.alarmBuild(),
onConfirm: (() => {
this.alarmVisible = false
this.timerController.reset() // 远程重置倒计时
})
})
八、控制器模式的适用场景
| 场景 | 说明 |
|---|---|
| 需要远程重置 | 弹窗确认后重置计时器 |
| 需要远程暂停/恢复 | 页面切换时暂停,返回时恢复 |
| 需要跨组件通信 | 兄弟组件间通过共享控制器协调行为 |
控制器模式的扩展方向:
- 添加更多回调:
setPauseCallback、setResumeCallback - 支持多实例:每个控制器绑定一个组件实例
- 结合
@Watch:控制器修改某个@State,子组件通过@Prop @Watch响应
九、小结
控制器模式在 ArkUI 中解决了"父组件主动驱动子组件行为"的需求,其核心三步:
- 定义 Controller 类:声明回调属性和注册/执行方法
- 子组件注册回调:在
aboutToAppear中controller.setResetCallback(this.method.bind(this)) - 父组件调用方法:
controller.reset()间接触发子组件行为
相比 @Link 双向绑定,控制器模式更语义化——它表达的是"命令"而非"数据同步",适合倒计时重置、播放器控制、动画触发等命令式交互场景。
更多推荐


所有评论(0)