HarmonyOS《柚兔学伴》项目实战11-倒计时组件封装与控制器模式

📅 2026/7/15 2:09:24 👁️ 阅读次数 📝 编程学习
HarmonyOS《柚兔学伴》项目实战11-倒计时组件封装与控制器模式

第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();}}}

控制器非常简洁,只做两件事:

  1. setResetCallback:注册回调函数(由子组件调用)
  2. 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()// 远程重置倒计时})})

八、控制器模式的适用场景

场景说明
需要远程重置弹窗确认后重置计时器
需要远程暂停/恢复页面切换时暂停,返回时恢复
需要跨组件通信兄弟组件间通过共享控制器协调行为

控制器模式的扩展方向:

  • 添加更多回调:setPauseCallbacksetResumeCallback
  • 支持多实例:每个控制器绑定一个组件实例
  • 结合@Watch:控制器修改某个@State,子组件通过@Prop @Watch响应

九、小结

控制器模式在 ArkUI 中解决了"父组件主动驱动子组件行为"的需求,其核心三步:

  1. 定义 Controller 类:声明回调属性和注册/执行方法
  2. 子组件注册回调:在aboutToAppearcontroller.setResetCallback(this.method.bind(this))
  3. 父组件调用方法controller.reset()间接触发子组件行为

相比@Link双向绑定,控制器模式更语义化——它表达的是"命令"而非"数据同步",适合倒计时重置、播放器控制、动画触发等命令式交互场景。