第11篇:倒计时组件封装与控制器模式
在"柚兔学伴"中,番茄钟倒计时是核心功能之一。倒计时组件需要满足:外部可设置时间、可远程重置、可监听状态变化。这就引出了一个经典的组件设计问题——如何让父组件从外部控制子组件的行为?
本篇从TimerComponent.ets的 202 行源码出发,详解控制器模式(Controller Pattern)在 ArkUI 中的实现。
一、控制器模式的核心思想
ArkUI 的@Component是一个封闭的作用域,@State、@Prop等状态装饰器只能在组件内部修改。如果父组件想要主动触发子组件的某个行为(如重置倒计时),单纯通过属性传递是做不到的。
控制器模式的解法是:子组件在初始化时,将自身的方法注册到控制器对象上;父组件持有控制器,通过调用控制器方法间接驱动子组件。
父组件 ──调用──> Controller.reset() ──回调──> 子组件.onResetFromController()二、TimerComponentController 实现
exportclassTimerComponentController{privateresetCallback:(()=>void)|null=null;setResetCallback(callback:()=>void){this.resetCallback=callback;}reset(){if(this.resetCallback){this.resetCallback();}}}控制器非常简洁,只做两件事:
setResetCallback:注册回调函数(由子组件调用)reset:执行回调函数(由父组件调用)
这种"注册-执行"的模式,本质是将子组件的方法引用"提升"到了控制器中,打破了组件边界对方法调用的限制。
三、TimerComponent 组件详解
3.1 属性定义
@Componentexportstruct TimerComponent{controller:TimerComponentController|null=null;@Prop@Watch('onTotalTimeChange')totalTime:number=60;onTotalTimeChange(){this.resetTimer();this.currentTime=this.totalTime;}onTimeUp?:()=>void;onTimeChange?:(currentTime:number)=>void;@StatecurrentTime:number=60;@StateisRunning:boolean=false;privatetimer:number=-1;privategradientColor:LinearGradient=newLinearGradient([{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{consthours=Math.floor(seconds/3600);constminutes=Math.floor((seconds%3600)/60);constsecs=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=newTimerComponentController()@StatesettingTime: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=falsethis.timerController.reset()// 远程重置倒计时})})八、控制器模式的适用场景
| 场景 | 说明 |
|---|---|
| 需要远程重置 | 弹窗确认后重置计时器 |
| 需要远程暂停/恢复 | 页面切换时暂停,返回时恢复 |
| 需要跨组件通信 | 兄弟组件间通过共享控制器协调行为 |
控制器模式的扩展方向:
- 添加更多回调:
setPauseCallback、setResumeCallback - 支持多实例:每个控制器绑定一个组件实例
- 结合
@Watch:控制器修改某个@State,子组件通过@Prop @Watch响应
九、小结
控制器模式在 ArkUI 中解决了"父组件主动驱动子组件行为"的需求,其核心三步:
- 定义 Controller 类:声明回调属性和注册/执行方法
- 子组件注册回调:在
aboutToAppear中controller.setResetCallback(this.method.bind(this)) - 父组件调用方法:
controller.reset()间接触发子组件行为
相比@Link双向绑定,控制器模式更语义化——它表达的是"命令"而非"数据同步",适合倒计时重置、播放器控制、动画触发等命令式交互场景。