一、案例介绍
在掌握了Hello World和计数器应用后,如何迈入HarmonyOS应用开发的下一级台阶?自定义计时器是理想的进阶案例。它不仅是日常应用中的高频功能------从考试倒计时到番茄工作法,从运动计时到在线课程提醒------更因为它完美融合了状态管理、用户交互和组件控制的核心思想,是初学者从"显示"迈向"控制"的关键一步。
本案例将基于HarmonyOS 6,围绕TextTimer组件实现一个功能纯净且完整的自定义计时器。其核心功能清晰易用:
- 正/倒计时切换:通过一个开关,让同一个组件在正向累加与反向递减两种模式间流畅转换。
- 启停控制:提供"开始"、"暂停"、"重置"三个基础操作按钮,实现对计时生命周期的完全掌控。
- 时间格式化:将毫秒级的计时数值,按照"分:秒.毫秒"或"时:分:秒"等易读格式展示。
通过这个案例,你将亲手搭建一个由数据驱动、可实时交互的"微型时间系统",为开发更复杂的HarmonyOS应用奠定坚实基础。
二、知识点概览
理解了计时器的应用价值后,我们需要系统性地梳理实现它所需的核心技术栈。在HarmonyOS 6下,一个功能完整的自定义计时器由五个层次协同构成:展示 、控制 、数据 、交互 与布局。
2.1 组件:TextTimer的"显示逻辑"
TextTimer是ArkTS中负责计时信息显示的专用UI组件。它在页面上就像一个数字时钟屏幕。
- isCountDown: boolean:决定计时器的运转方向。设为true时,时间从预设值递减,实现倒计时;设为false时,时间从0开始递增,实现正计时。这是区分"番茄钟"倒计时与"运动耗时"正计时的关键开关。
- count: number:与isCountDown配合使用。仅在倒计时模式下生效,用于设定计时的初始总毫秒数(例如,300000代表5分钟)。
- controller: TextTimerController:用于控制该组件的控制器实例。每个TextTimer通常需要绑定一个独立的控制器,这是实现"启、停、重置"等命令的通信桥梁。
- format: string:格式化显示字符串。默认格式为'HH:mm:ss.SS',开发者可以根据需要精简为'mm:ss.SS'(分:秒.毫秒)或'HH:mm:ss'(时分秒)等。它让底层的毫秒数据变得清晰可读。
相关文档:TextTimer-信息展示-ArkTS组件-ArkUI(方舟UI框架)-应用框架 - 华为HarmonyOS开发者
2.2 控制:TextTimerController的"命令逻辑"
TextTimerController是TextTimer组件的指挥中枢,通过三个核心方法发出指令:
- start(): void:启动计时器。调用后,时间开始按照isCountDown设定的方向持续流动。如果之前处于暂停状态,调用start()将从暂停点恢复计时。
- pause(): void:暂停计时器。时间停止流动,但当前计时进度会被保留。这是实现"临时中断"功能的核心。
- reset(): void:重置计时器。无论计时器处于运行或暂停状态,调用此方法将立即恢复到初始状态:对于倒计时,会回到count设定的时间;对于正计时,则归零。
此层构成了用户对计时进程的"操作接口",我们将通过点击事件来触发这些命令。
相关文档:TextTimerController
2.3 状态:@State装饰器驱动的"数据逻辑"
计时器的模式切换(正/倒计时)和显示格式,本质上都是需要动态响应的应用状态。ArkTS的@State装饰器是管理这类状态的最佳工具。
- 声明一个@State isCountDownMode: boolean = false变量,当它的值在true和false之间切换时,绑定它的TextTimer组件的isCountDown属性会自动更新,从而无缝切换计时模式。
- 同样,声明@State displayFormat: string = 'mm:ss.SS',可以实现运行时动态改变时间显示格式。
@State装饰器确保当这些变量的值变化时,ArkUI框架会自动重新渲染UI中所有依赖它们的部分,这是实现"声明式UI"和"响应式编程"的关键。
2.4 交互:Button组件绑定的"事件逻辑"
技术栈再好,也需为用户提供一个操作界面。我们将使用Button组件来承载用户意图。
- "开始/暂停/重置"按钮:每个按钮都通过.onClick(() => { ... })事件绑定到对应的TextTimerController方法(start、pause、reset),实现点击操控。
- "切换模式"按钮:其点击事件将用于改变@State isCountDownMode的值,从而驱动计时器在正、倒计时之间切换。
按钮的设计将用户的一个简单点击动作,翻译为对控制器或状态变量的精确调用,完成了从"意图"到"指令"的闭环。
2.5 布局:Flex与Row组合的"空间逻辑"
如何优雅地将TextTimer显示区域和下方的一排操作按钮组织在屏幕上?这需要用到ArkTS的弹性布局能力。
- 外层Flex布局:使用Flex({ direction: FlexDirection.Column })将整体布局设置为纵向(垂直)排列。它通常包裹整个页面内容,可以方便地设置全局的居中对齐(justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center),让计时器界面在屏幕上居中显示。
- 内层Row布局:使用Row({ space: 20 })来水平排列"开始"、"暂停"、"重置"等操作按钮。其中的space参数为按钮之间添加等间距,确保视觉上的规整与平衡。
通过Flex与Row的嵌套组合,我们构建了一个结构清晰、易于维护的视觉层次,让核心的计时显示区与操作区各安其位。
三、开发步骤
3.1 创建新项目
可参考我之前写的文章:从零开始创建你的第一个HarmonyOS6项目-CSDN博客
3.2 基础知识点详解
TextTimer组件是本次计时器案例的核心。
组件参数:定义计时的起止与形式
在ArkTS中初始化一个TextTimer,需要传一个对象参数。这三个参数共同决定了计时器的行为:
-
count: number:设定计时的初始总时长,单位是毫秒 。例如,30000代表30秒,300000代表5分钟。它的作用并非一成不变------只有当isCountDown为true,即倒计时模式下,它才作为倒计数的起点值。如果使用者设置为正计时模式(isCountDown: false),无论count设为多少,计时都会从0开始累加。这要视你的应用场景而定:在"考试结束"场景中,你需要一个明确的时长限制作为count;而在"记录耗时"场景中,你只需关注起点。 -
isCountDown: boolean:这是计时器的"方向舵"。设置为true时,时间从count值开始递减,屏幕上显示的是"剩余时间";设置为false时,时间从0开始递增,显示的是"已用时长"。通过切换这一个布尔值,你就能在"倒计时警报"和"正计时记录"两个经典模式间自由切换。 -
format: string:时间格式化的模版。默认值'HH:mm:ss.SS'包含了完整的时、分、秒和两位数毫秒。每个占位符必须大写,代表着不同的时间单位:- HH:24小时制的小时(00-23)
- mm:分钟(00-59)
- ss:秒(00-59)
- SS:两位数毫秒(00-99)
你可以根据精度需求自由组合,比如'mm:ss'(分:秒)适合于不需要毫秒的粗略计时,'HH:mm:ss'(时:分:秒)用于跨越小时的长时间任务。这个字符串直接决定了用户看到的时间样式。
TextTimerController:执掌时间的三把钥匙
控制器的方法调用直接映射到用户的操作意图,但其内部状态的变化需要了然于心。
start():这个方法启动或恢复计时。如果计时器从未启动或已被reset(),调用它会从初始状态(倒计时的count值或正计时的0)开始计时;如果计时器处于pause()后的暂停状态,start()会让它从暂停的时间点继续流动。pause():它并非"停止",而是"冻结"。调用后,计时器内部时钟暂停,但这只是暂时中断,而非终结。当前流逝或剩余的时间会被精确记住,以便后续start()可以无缝接续。这在"接电话暂停计时"的场景中至关重要。reset():这才是真正意义上的"归零重启"。无论计时器当前是运行中还是暂停中,reset()都会强制将其内部计时值恢复到初始状态------对于倒计时,回到count值;对于正计时,重置为0。相当于销毁当前计时进程,并重新创建了一个全新的副本。
@State的特殊作用:动态切换的根基
在计时场景中,@State装饰的变量是驱动界面动态变化的关键。
试想你的计时器需要运行时切换正/倒计时模式。如果直接将一个普通变量赋值给TextTimer的isCountDown属性,那么这个值在初始化后就固定了。ArkUI框架不知道何时该重新检查它。而当你用@State装饰一个变量,如@State isCountDownMode: boolean = false,并将TextTimer的isCountDown属性绑定为isCountDownMode时,一切都变得动态了。
此时,你只需在某个按钮的点击事件中编写this.isCountDownMode = !this.isCountDownMode。这一赋值行为,会被@State装饰器截获,并通知ArkUI框架:"我管理的这个数据源变了"。框架随即启动响应式更新流程,它会找到所有UI中引用了isCountDownMode的地方(这里就是TextTimer组件的isCountDown属性),并使用新值重新计算和渲染这一小部分UI。于是,计时器无需重新构建整个页面,就在毫秒间完成了模式切换。
同理,@State displayFormat: string变量绑定到TextTimer的.format(this.displayFormat)上。当你在应用运行时改变displayFormat的值(例如从'mm:ss'切换到'HH:mm:ss'),同样的响应式机制被触发,TextTimer会立即按照新的格式字符串重新绘制时间文本。这种"数据变,视图自动变"的机制,是声明式UI开发的高效秘密,也是实现我们案例中动态功能切换的核心技术保障。
3.3 完整代码
在详尽了解了TextTimer的组件属性和控制原理后,现在让我们将这些知识整合起来,构建出可以直接运行、涵盖所有功能的完整页面代码。
需要特别留意的一点是控制器的初始化位置 。TextTimerController的实例必须在struct顶层进行创建和初始化(textTimerController: TextTimerController = new TextTimerController()),而不能在build()方法或其他事件函数内部声明。这是因为控制器需要在组件的整个生命周期内保持同一实例,以确保其状态(运行、暂停等)的正确管理和持久化。如果在函数内部声明,每次函数调用都会创建一个全新的控制器,之前绑定的计时器状态将丢失。
TypeScript
@Entry
@Component
struct Index {
// 【核心】计时器控制器,必须在组件顶层初始化,确保实例唯一
textTimerController: TextTimerController = new TextTimerController();
// 【状态一】控制计时方向:true为倒计时,false为正计时
@State isCountDown: boolean = true;
// 【状态二】控制时间显示格式,与TextTimer的.format属性动态绑定
@State timeFormat: string = 'mm:ss.SS';
// 倒计时的初始时长(单位:毫秒),例如 30000ms = 30秒
@State countDownTime: number = 30000;
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
// 主计时器显示区
TextTimer({
// 动态绑定计时方向
isCountDown: this.isCountDown,
// 仅当isCountDown为true时,此值作为倒计时起点
count: this.countDownTime,
// 绑定控制器
controller: this.textTimerController
})
// 动态绑定格式,格式变化时UI自动更新
.format(this.timeFormat)
.fontSize(40)
.fontColor(Color.Black)
.margin({ bottom: 40 })
.onTimer((utc: number, elapsedTime: number) => {
// 计时回调,可用于处理计时结束等逻辑
})
// 第一行按钮:计时控制
Row({ space: 15 }) {
Button('开始')
.width(90)
.height(45)
.onClick(() => {
// 启动或继续计时
this.textTimerController.start();
})
Button('暂停')
.width(90)
.height(45)
.onClick(() => {
// 暂停当前计时
this.textTimerController.pause();
})
Button('重置')
.width(90)
.height(45)
.onClick(() => {
// 重置计时器到初始状态
this.textTimerController.reset();
})
}
.margin({ bottom: 30 })
// 第二行按钮:模式切换
Row({ space: 15 }) {
Button(this.isCountDown ? '切换为正计时' : '切换为倒计时')
.width(160)
.height(45)
.onClick(() => {
// 切换计时方向,@State变量变化驱动UI自动更新
this.isCountDown = !this.isCountDown;
})
Button('格式 HH:mm:ss')
.width(140)
.height(45)
.onClick(() => {
// 切换显示格式
this.timeFormat = 'HH:mm:ss';
})
}
}
.width('100%')
.height('100%')
}
}
3.4 运行效果展示
