HarmonyOS ArkTS 实战:实现一个简约倒计时器
HarmonyOS ArkTS 实战:实现一个简约倒计时器
项目效果
本文实现的是一个基于 HarmonyOS 和 ArkTS 的简约倒计时器应用。项目中使用 ArkUI 组件完成页面布局,通过 @State 管理状态数据,实现时间设置、开始/暂停/重置、倒计时结束提示、快捷时间选择和进度环显示等功能。
最终运行效果如下:
页面主要包含以下内容:
- 顶部应用标题;
- 圆形进度环显示剩余时间;
- 大字体时间显示;
- 快捷时间选择按钮(1/3/5/10/15/30分钟);
- 自定义时间设置;
- 开始/暂停/重置按钮;
- 倒计时结束状态提示;
- 页面整体采用 ArkUI 声明式布局。
本文重点是演示如何在 HarmonyOS 项目中使用 ArkTS 和 ArkUI 实现一个倒计时类单页面应用。项目代码主要写在 entry/src/main/ets/pages/Index.ets 文件中,适合作为 HarmonyOS ArkTS 入门到进阶之间的练习案例。
前言
倒计时器是生活中常用的工具,无论是煮蛋、健身、冥想还是工作专注,都需要一个简单好用的倒计时。手机自带的计时器往往功能复杂,而我们需要一个界面简洁、操作方便的倒计时器。
从应用开发角度来看,这个项目不依赖后端接口,也不需要数据库,但能练习 ArkTS 中的状态管理、定时器、进度动画、按钮交互和时间计算等内容。
本文基于 HarmonyOS 和 ArkTS 实现一个简约倒计时器。用户可以选择快捷时间或自定义时间,开始倒计时,随时暂停或重置,倒计时结束时有明显的视觉提示。
这个项目的核心不是简单的数字递减,而是流畅的倒计时体验和直观的进度展示。每一次开始、暂停和重置,本质上都是对计时器状态的管理。状态变化后,页面会自动刷新,这正是 ArkUI 声明式开发的基本思想。
一、项目目标
本次实践主要实现以下目标:
- 创建 HarmonyOS ArkTS 页面;
- 使用
@Entry和@Component定义页面组件; - 使用
@State管理页面状态; - 使用定时器实现倒计时功能;
- 圆形进度环显示倒计时进度;
- 提供快捷时间选择按钮;
- 支持自定义分钟和秒数;
- 实现开始/暂停/重置功能;
- 倒计时结束时的状态提示;
- 时间格式化显示;
- 使用
@Builder封装快捷按钮和控制按钮; - 完成一个可以运行的倒计时器页面。
这个项目虽然是单页面应用,但它有完整的时间设置、倒计时控制和进度显示功能,比普通静态页面更适合练习 ArkTS。
二、技术栈
| 类型 | 内容 |
|---|---|
| 开发方向 | HarmonyOS 应用开发 |
| 开发语言 | ArkTS |
| UI 框架 | ArkUI |
| SDK 版本 | HarmonyOS API 23 及以上 |
| 工程模型 | Stage 模型 |
| 核心组件 | Text / Button / Column / Row / Circle / Stack / TextInput |
| 状态管理 | @State |
| 数据处理 | 时间计算 / 定时器 / 进度动画 |
| 项目入口 | entry/src/main/ets/pages/Index.ets |
| 运行平台 | 模拟器或真机 |
本项目是 HarmonyOS 原生 ArkTS 项目。页面主体由 ArkUI 组件构建,核心逻辑写在 Index.ets 文件中,不依赖后端接口,也不需要额外配置数据库。
三、为什么选择倒计时器项目
简约倒计时器适合作为 ArkTS 练习项目,主要有以下几个原因。
第一,实用性强。倒计时是非常常用的功能,在很多场景下都会用到。
第二,业务场景真实。烹饪、运动、专注、休息等场景都需要倒计时,是手机必备工具之一。
第三,适合练习状态管理。运行状态、剩余时间、总时间、定时器ID都属于页面状态。状态变化后,页面会自动刷新。
第四,适合练习定时器和动画。倒计时需要每秒更新,进度环需要平滑动画。
第五,适合练习时间格式化。将秒数格式化为易读的分秒格式。
第六,扩展空间比较大。基础功能完成后,可以继续增加多组倒计时、提示音、震动提醒、后台运行和预设模板。
在本项目中,剩余时间和运行状态是核心数据。页面中的时间显示和进度环都由它们计算或渲染得到。
四、功能规则说明
快捷时间选项:1分钟、3分钟、5分钟、10分钟、15分钟、30分钟。
自定义时间范围:0-99分钟,0-59秒。
按钮状态规则:
| 当前状态 | 主按钮 | 副按钮 |
|---|---|---|
| 初始/重置 | 开始 | 重置(禁用) |
| 运行中 | 暂停 | 重置 |
| 暂停中 | 继续 | 重置 |
| 倒计时结束 | 重新开始 | 重置 |
时间显示格式:MM:SS,例如05:00表示5分钟。
倒计时结束时,时间显示为00:00,进度环填满,颜色变为绿色提示完成。
五、项目结构
本项目主要修改首页文件:
entry
└── src
└── main
└── ets
└── pages
└── Index.ets
其中:
| 文件 | 作用 |
|---|---|
| Index.ets | 编写页面结构、状态数据和倒计时逻辑 |
本文不涉及复杂路由,也不需要额外创建多个页面。对于练习项目来说,把主要逻辑集中在一个 Index.ets 文件中更方便理解。
六、核心实现思路
本项目的核心流程如下:
- 使用
@State保存总时间、剩余时间、运行状态和定时器ID; - 用户点击快捷按钮设置时间;
- 用户也可以自定义分钟和秒数;
- 点击开始按钮启动定时器;
- 每秒减少1秒剩余时间;
- 圆形进度环显示倒计时进度;
- 点击暂停按钮停止定时器;
- 点击重置按钮回到初始状态;
- 剩余时间为0时倒计时结束;
- 格式化时间显示为分秒格式。
项目中最重要的状态变量如下:
@State isRunning: boolean = false
@State isFinished: boolean = false
@State totalSeconds: number = 300
@State remainingSeconds: number = 300
@State timerId: number = -1
@State customMinutes: string = '5'
@State customSeconds: string = '0'
其中:
| 状态变量 | 作用 |
|---|---|
| isRunning | 倒计时是否运行中 |
| isFinished | 倒计时是否结束 |
| totalSeconds | 总时间(秒) |
| remainingSeconds | 剩余时间(秒) |
| timerId | 定时器ID |
| customMinutes | 自定义分钟输入 |
| customSeconds | 自定义秒数输入 |
remainingSeconds 是本项目中最重要的数据。时间显示和进度环都依赖这个状态。
七、Index.ets 完整代码
打开文件:
entry/src/main/ets/pages/Index.ets
将其中内容替换为下面代码:
@Entry
@Component
struct Index {
@State isRunning: boolean = false
@State isFinished: boolean = false
@State totalSeconds: number = 300
@State remainingSeconds: number = 300
@State timerId: number = -1
@State customMinutes: string = '5'
@State customSeconds: string = '0'
private quickTimes: number[] = [60, 180, 300, 600, 900, 1800]
aboutToDisappear() {
this.stopTimer()
}
private setTime(seconds: number): void {
if (this.isRunning) return
this.totalSeconds = seconds
this.remainingSeconds = seconds
this.isFinished = false
this.customMinutes = Math.floor(seconds / 60).toString()
this.customSeconds = (seconds % 60).toString()
}
private applyCustomTime(): void {
if (this.isRunning) return
let mins = parseInt(this.customMinutes) || 0
let secs = parseInt(this.customSeconds) || 0
mins = Math.min(Math.max(mins, 0), 99)
secs = Math.min(Math.max(secs, 0), 59)
let total = mins * 60 + secs
if (total > 0) {
this.setTime(total)
}
}
private startTimer(): void {
if (this.isRunning || this.remainingSeconds <= 0) return
this.isRunning = true
this.isFinished = false
this.timerId = setInterval(() => {
if (this.remainingSeconds > 0) {
this.remainingSeconds--
} else {
this.finishTimer()
}
}, 1000)
}
private pauseTimer(): void {
if (!this.isRunning) return
this.isRunning = false
this.stopTimer()
}
private stopTimer(): void {
if (this.timerId !== -1) {
clearInterval(this.timerId)
this.timerId = -1
}
}
private resetTimer(): void {
this.pauseTimer()
this.remainingSeconds = this.totalSeconds
this.isFinished = false
}
private finishTimer(): void {
this.pauseTimer()
this.isFinished = true
this.remainingSeconds = 0
}
private getProgress(): number {
if (this.totalSeconds === 0) return 0
return 1 - (this.remainingSeconds / this.totalSeconds)
}
private formatTime(seconds: number): string {
let mins = Math.floor(seconds / 60)
let secs = seconds % 60
return `${this.formatTwo(mins)}:${this.formatTwo(secs)}`
}
private formatTwo(value: number): string {
return value < 10 ? '0' + value.toString() : value.toString()
}
private getRingColor(): string {
if (this.isFinished) return '#10B981'
if (this.isRunning) return '#0A59F7'
return '#9CA3AF'
}
@Builder
QuickTimeButton(seconds: number) {
let mins = Math.floor(seconds / 60)
Button(`${mins}分钟`)
.height(40)
.padding({ left: 16, right: 16 })
.fontSize(14)
.fontColor(this.totalSeconds === seconds && !this.isRunning ? Color.White : '#4B5563')
.backgroundColor(this.totalSeconds === seconds && !this.isRunning ? '#0A59F7' : '#F3F4F6')
.borderRadius(20)
.onClick(() => {
this.setTime(seconds)
})
}
build() {
Column() {
Text('倒计时器')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#182431')
.margin({ top: 22 })
Text('基于 HarmonyOS ArkTS 的简约计时工具')
.fontSize(14)
.fontColor('#6B7280')
.margin({ top: 8, bottom: 30 })
Stack() {
Circle()
.width(260)
.height(260)
.fill('#F5F7FA')
.stroke('#E5E7EB')
.strokeWidth(12)
Circle()
.width(260)
.height(260)
.fill(Color.Transparent)
.stroke(this.getRingColor())
.strokeWidth(12)
.strokeDashArray([2 * Math.PI * 124 * this.getProgress(), 2 * Math.PI * 124])
.strokeDashOffset(2 * Math.PI * 124 * 0.25)
.animation({ duration: 1000, curve: Curve.Linear })
Column() {
if (this.isFinished) {
Text('⏰')
.fontSize(40)
.margin({ bottom: 8 })
Text('时间到!')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#10B981')
}
Text(this.formatTime(this.remainingSeconds))
.fontSize(56)
.fontWeight(FontWeight.Bold)
.fontColor(this.isFinished ? '#10B981' : '#182431')
.fontFamily('monospace')
Text(this.isRunning ? '倒计时中' : this.isFinished ? '已完成' : '准备开始')
.fontSize(14)
.fontColor('#6B7280')
.margin({ top: 8 })
}
}
.width(280)
.height(280)
.margin({ bottom: 30 })
.justifyContent(FlexAlign.Center)
.alignItems(Alignment.Center)
Text('快捷时间')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#182431')
.width('100%')
.margin({ bottom: 12 })
Flex({ wrap: FlexWrap.Wrap, justifyContent: FlexAlign.Start }) {
ForEach(this.quickTimes, (time: number) => {
this.QuickTimeButton(time)
}, (t: number) => t.toString())
}
.width('100%')
.margin({ bottom: 20 })
Text('自定义时间')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#182431')
.width('100%')
.margin({ bottom: 12 })
Row() {
TextInput({
text: this.customMinutes,
placeholder: '分'
})
.layoutWeight(1)
.height(48)
.fontSize(18)
.textAlign(TextAlign.Center)
.backgroundColor('#F5F7FA')
.borderRadius(12)
.type(InputType.Number)
.onChange((value: string) => {
this.customMinutes = value
})
Text('分')
.fontSize(16)
.margin({ left: 8, right: 16 })
TextInput({
text: this.customSeconds,
placeholder: '秒'
})
.layoutWeight(1)
.height(48)
.fontSize(18)
.textAlign(TextAlign.Center)
.backgroundColor('#F5F7FA')
.borderRadius(12)
.type(InputType.Number)
.onChange((value: string) => {
this.customSeconds = value
})
Text('秒')
.fontSize(16)
.margin({ left: 8 })
Button('应用')
.height(48)
.margin({ left: 16 })
.padding({ left: 16, right: 16 })
.fontSize(15)
.fontColor(Color.White)
.backgroundColor('#0A59F7')
.borderRadius(12)
.onClick(() => {
this.applyCustomTime()
})
}
.width('100%')
.margin({ bottom: 30 })
Row() {
if (this.isRunning) {
Button('暂停')
.height(56)
.layoutWeight(2)
.fontSize(18)
.fontColor(Color.White)
.backgroundColor('#F59E0B')
.borderRadius(28)
.onClick(() => {
this.pauseTimer()
})
} else {
Button(this.isFinished ? '重新开始' : '开始')
.height(56)
.layoutWeight(2)
.fontSize(18)
.fontColor(Color.White)
.backgroundColor('#10B981')
.borderRadius(28)
.onClick(() => {
if (this.isFinished) {
this.resetTimer()
}
this.startTimer()
})
}
Blank().width(16)
Button('重置')
.height(56)
.layoutWeight(1)
.fontSize(16)
.fontColor(this.remainingSeconds < this.totalSeconds ? '#EF4444' : '#9CA3AF')
.backgroundColor(this.remainingSeconds < this.totalSeconds ? '#FEF2F2' : '#F3F4F6')
.borderRadius(28)
.onClick(() => {
this.resetTimer()
})
}
.width('100%')
}
.width('100%')
.height('100%')
.padding({ left: 18, right: 18 })
.backgroundColor('#F5F7FA')
}
}
八、代码实现说明
1. 倒计时核心逻辑
使用setInterval每秒减少1秒剩余时间,时间到0时调用finishTimer:
this.timerId = setInterval(() => {
if (this.remainingSeconds > 0) {
this.remainingSeconds--
} else {
this.finishTimer()
}
}, 1000)
2. 圆形进度环
和番茄钟类似,使用两个Circle实现进度环,进度随时间增加,动画平滑过渡。倒计时结束时进度环变为绿色。
3. 快捷时间设置
提供常用时间的快捷按钮,点击即可快速设置,无需手动输入。
4. 自定义时间
支持输入分钟和秒数,点击应用设置自定义时间,有范围限制防止非法值。
5. 按钮状态管理
根据运行状态和结束状态动态改变按钮文字和颜色:
- 运行中显示暂停按钮(橙色)
- 未运行显示开始按钮(绿色)
- 结束后显示重新开始
- 重置按钮在有进度时才是红色可点击状态
6. 结束状态
倒计时结束时显示闹钟emoji和"时间到!"提示,进度环变绿,给用户明确的反馈。
九、运行项目
代码编写完成后,在DevEco Studio中运行项目。测试快捷时间选择、自定义时间、开始/暂停/重置功能,检查倒计时是否准确,进度环是否正常动画,结束时是否有正确提示。
十、开发中遇到的问题
- 定时器清理:页面销毁时清除定时器防止内存泄漏
- 运行中禁止修改时间:倒计时进行时不能修改时间设置
- 自定义时间范围验证:防止输入超过范围的数值
- 结束状态处理:时间到后正确显示完成状态
十一、总结
本文基于HarmonyOS和ArkTS实现了一个简约倒计时器。项目通过@State管理计时状态,使用定时器实现倒计时功能,实现了快捷时间选择、自定义时间、开始/暂停/重置和进度显示等功能。这个项目展示了ArkTS中定时器使用、动画效果和状态管理的基本方法,适合作为入门练习项目。
更多推荐

所有评论(0)