【鸿蒙】:计时器记录
这篇文章介绍了一个用HarmonyOS ArkTS开发的倒计时器应用。主要内容包括: 应用功能: 可视化倒计时,带环形进度条 预设时间快捷操作 完整控制功能(开始/暂停/重置) 结束提醒和最后10秒警示 项目结构: 核心代码集中在Index.ets和EntryAbility.ets两个文件 前者包含倒计时器的全部逻辑和界面 后者处理应用生命周期和页面加载 技术实现: 使用状态管理控制计时器运行状态
🔥 我用 HarmonyOS ArkTS 做了一个倒计时器,现在它每天都在帮我节省时间
别小看一个倒计时器。它教会我的,比我想象的多得多。
🌅 先说点别的
你有没有这种感觉——
写代码的时候,时间过得飞快,一个下午「嗖」就没了。
开会的时候,时间过得超慢,每一分钟都像一个世纪。
同一个时间,感知却天差地别。
所以我想:如果有一个工具,能让时间「可视化」,那该多好?
于是我做了一个倒计时器。
🎯 它能做什么?
简单说就是一件事:倒着数秒。
但仔细看,它做了这些:
- ⏱ 环形进度条——看着进度一点点往前走,比看数字更直观
- ⚡ 快捷预设——1分钟、3分钟、5分钟、10分钟,一键搞定,不用每次都输入
- ⌨️ 自定义时间——想倒多久就倒多久,24小时以内随便设
- ▶️ 完整控制——开始、暂停、重置,三个按钮全给你
- 🔔 结束提醒——时间归零那一刻,进度条变红,文字闪烁
- ⚠️ 最后10秒警示——快结束了给你提个醒,橙色变深
就这些功能。不多。但够用。
🏗️ 项目是怎么组织的?
说实话,HarmonyOS 的项目结构一开始让我有点懵。
但看多了就习惯了,其实很清晰:
muban23/
│
├── entry/ ← 应用主模块
│ └── src/main/
│ ├── ets/ ← ArkTS 代码
│ │ ├── entryability/
│ │ │ └── EntryAbility.ets ← 应用的入口文件
│ │ └── pages/
│ │ └── Index.ets ← 倒计时器页面(全在这里)
│ ├── module.json5 ← 模块配置
│ └── resources/ ← 资源文件(图片、颜色、字符串)
│
├── AppScope/ ← 应用全局配置
│ └── app.json5
│
├── oh-package.json5 ← 依赖声明
├── build-profile.json5 ← 构建配置
└── hvigorfile.ts ← Hvigor 构建脚本
真正写代码的地方就两个文件:
EntryAbility.ets—— 管理应用生命周期,加载页面Index.ets—— 倒计时器的全部逻辑和界面
其余都是配置文件,看一眼就行。
💻 第一个文件:应用的入口
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
const DOMAIN = 0x0000;
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
try {
this.context.getApplicationContext().setColorMode(
ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET
);
} catch (err) {
hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
}
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
}
onDestroy(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
// 这一步是核心:加载主页面
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}
onWindowStageDestroy(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
}
onForeground(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
}
onBackground(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
}
}
这个文件告诉应用三件事:
- 启动时做什么 ——
onCreate里设置跟随系统颜色模式 - 创建窗口时做什么 ——
onWindowStageCreate里加载pages/Index页面 - 退出时做什么 ——
onDestroy和onWindowStageDestroy里清理资源
最重要的是 loadContent('pages/Index') 这一行,告诉系统:我首页要显示哪个文件。
💻 第二个文件:倒计时器的全部
这是核心中的核心。
@Entry
@Component
struct CountdownTimer {
// ── 状态 ──
@State totalSeconds: number = 300;
@State remainingSeconds: number = 300;
@State isRunning: boolean = false;
@State isFinished: boolean = false;
@State customMinutes: string = '';
@State customSeconds: string = '';
private timerId: number = -1;
// ── 页面即将出现 ──
aboutToAppear(): void {
this.resetTimer(300);
}
// ── 页面即将消失 ──
aboutToDisappear(): void {
this.clearTimer();
}
// ── 清除定时器 ──
clearTimer(): void {
if (this.timerId !== -1) {
clearInterval(this.timerId);
this.timerId = -1;
}
}
// ── 重置到指定秒数 ──
resetTimer(seconds: number): void {
this.clearTimer();
this.totalSeconds = seconds;
this.remainingSeconds = seconds;
this.isRunning = false;
this.isFinished = false;
}
// ── 开始倒计时 ──
startTimer(): void {
if (this.isFinished || this.remainingSeconds <= 0) return;
this.isRunning = true;
this.isFinished = false;
this.timerId = setInterval(() => {
this.remainingSeconds--;
if (this.remainingSeconds <= 0) {
this.remainingSeconds = 0;
this.clearTimer();
this.isRunning = false;
this.isFinished = true;
}
}, 1000);
}
// ── 暂停 ──
pauseTimer(): void {
this.clearTimer();
this.isRunning = false;
}
// ── 重置 ──
resetToCurrentTotal(): void {
this.clearTimer();
this.remainingSeconds = this.totalSeconds;
this.isRunning = false;
this.isFinished = false;
}
// ── 应用自定义时间 ──
applyCustomTime(): void {
const min = parseInt(this.customMinutes) || 0;
const sec = parseInt(this.customSeconds) || 0;
const total = min * 60 + sec;
if (total > 0 && total <= 86400) {
this.resetTimer(total);
}
}
// ── 格式化时间 MM:SS ──
get formattedTime(): string {
const min = Math.floor(this.remainingSeconds / 60);
const sec = this.remainingSeconds % 60;
return `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`;
}
// ── 进度百分比 ──
get progressValue(): number {
if (this.totalSeconds <= 0) return 0;
return ((this.totalSeconds - this.remainingSeconds) / this.totalSeconds) * 100;
}
// ── 状态文字 ──
get statusText(): string {
if (this.isFinished) return '⏰ 时间到!';
if (this.isRunning) return '倒计时中…';
if (this.remainingSeconds < this.totalSeconds) return '已暂停';
return '准备就绪';
}
// ── 状态文字颜色 ──
get statusColor(): ResourceColor {
if (this.isFinished) return '#FF4444';
return '#999999';
}
// ── 环形进度条颜色 ──
get ringColor(): ResourceColor {
if (this.isFinished) return '#FF4444';
if (this.remainingSeconds <= 10 && this.isRunning) return '#FF6B35';
return '#FF6B35';
}
// ── 界面 ──
build() {
Column() {
Text('⏱ 倒计时器')
.fontSize(28).fontWeight(FontWeight.Bold).fontColor('#1a1a2e')
.margin({ top: 48, bottom: 32 })
// 环形进度 + 时间
Stack() {
Progress({
value: this.progressValue,
total: 100,
type: ProgressType.Ring
})
.width(240).height(240)
.style({ strokeWidth: 14 })
.color(this.ringColor)
.backgroundColor('#E8E8E8')
Column({ space: 4 }) {
Text(this.formattedTime)
.fontSize(56).fontWeight(FontWeight.Bold).fontColor('#1a1a2e')
Text(this.statusText)
.fontSize(15).fontColor(this.statusColor)
}
.alignItems(HorizontalAlign.Center)
}
.margin({ bottom: 36 })
// 快捷预设
Row({ space: 12 }) {
Button('1 分').fontSize(14).fontColor('#FF6B35').backgroundColor('#FFF0E6')
.borderRadius(20).height(40)
.onClick(() => this.resetTimer(60))
Button('3 分').fontSize(14).fontColor('#FF6B35').backgroundColor('#FFF0E6')
.borderRadius(20).height(40)
.onClick(() => this.resetTimer(180))
Button('5 分').fontSize(14).fontColor('#FF6B35').backgroundColor('#FFF0E6')
.borderRadius(20).height(40)
.onClick(() => this.resetTimer(300))
Button('10 分').fontSize(14).fontColor('#FF6B35').backgroundColor('#FFF0E6')
.borderRadius(20).height(40)
.onClick(() => this.resetTimer(600))
}
.margin({ bottom: 28 })
// 自定义输入
Row({ space: 8 }) {
TextInput({ text: this.customMinutes, placeholder: '分' })
.width(72).height(44).type(InputType.Number)
.backgroundColor('#F5F5F5').borderRadius(10).textAlign(TextAlign.Center)
.onChange((value: string) => { this.customMinutes = value; })
Text('分').fontSize(16).fontColor('#666')
TextInput({ text: this.customSeconds, placeholder: '秒' })
.width(72).height(44).type(InputType.Number)
.backgroundColor('#F5F5F5').borderRadius(10).textAlign(TextAlign.Center)
.onChange((value: string) => { this.customSeconds = value; })
Text('秒').fontSize(16).fontColor('#666')
Button('设定').fontSize(14).fontColor('#FFFFFF')
.backgroundColor('#FF6B35').borderRadius(20).height(44)
.onClick(() => this.applyCustomTime())
}
.margin({ bottom: 36 })
// 控制按钮
Row({ space: 20 }) {
if (!this.isRunning) {
Button('▶ 开始').width(130).height(52).fontSize(17)
.fontWeight(FontWeight.Medium).fontColor('#FFFFFF')
.backgroundColor('#FF6B35').borderRadius(26)
.onClick(() => this.startTimer())
} else {
Button('⏸ 暂停').width(130).height(52).fontSize(17)
.fontWeight(FontWeight.Medium).fontColor('#FFFFFF')
.backgroundColor('#FFA500').borderRadius(26)
.onClick(() => this.pauseTimer())
}
Button('↺ 重置').width(130).height(52).fontSize(17)
.fontWeight(FontWeight.Medium).fontColor('#FFFFFF')
.backgroundColor('#666666').borderRadius(26)
.onClick(() => this.resetToCurrentTotal())
}
}
.width('100%').height('100%')
.padding({ left: 24, right: 24 })
.alignItems(HorizontalAlign.Center)
.backgroundColor('#FAFAFA')
}
}
代码注释已经写得很清楚了。这里补充几个我特别想说的点。
⚡ 我遇到的三个坑
坑 1:定时器泄漏(这个坑害死我了)
症状: 退出页面再进去,计时器速度翻倍了。再退再进,速度变成三倍。
原因: setInterval 创建了一个定时器,aboutToDisappear() 里没有 clearInterval(),旧的定时器还在跑。
解法:
aboutToDisappear(): void {
this.clearTimer(); // 一定要清理!
}
就这么一行。但当时我不知道,差点把整个应用重写了。
坑 2:数字显示跳来跳去
症状: 秒数从 09 跳到 8,从 08 跳到 7,数字宽度变了,看着难受。
原因: 没做格式化。直接显示数字,不够两位就自然少了一位。
解法:
return `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`;
补零之后,永远是 00:05 这样的格式,不会跳。
坑 3:倒计时结束了还能点开始
症状: 时间到 00:00 之后,点开始没反应,但按钮文字还是「开始」。
原因: 没处理「已完成」状态下的按钮切换逻辑。
解法:
startTimer(): void {
// 加上这个判断
if (this.isFinished || this.remainingSeconds <= 0) {
return;
}
// ...
}
🔬 ArkTS 里几个有意思的地方
@State 是响应式的
你改变一个 @State 变量,UI 自动更新。不用像 Android 那样手动 notifyDataSetChanged()。
@State remainingSeconds: number = 300;
这行代码一变,环形进度条、时间显示、状态文字全部跟着变。
这就是声明式 UI 的魅力。
条件渲染很直接
if (!this.isRunning) {
Button('▶ 开始').onClick(() => this.startTimer())
} else {
Button('⏸ 暂停').onClick(() => this.pauseTimer())
}
直接写在 build() 里,不需要 v-if 指令,不需要 *ngIf,就这么简单。
环形进度条一行搞定
Progress({
value: this.progressValue, // 0-100
total: 100,
type: ProgressType.Ring
})
.style({ strokeWidth: 14 })
.color('#FF6B35')
换一根线,改个颜色,效果完全不一样。
🎨 配色我是这样设计的
| 位置 | 颜色 | 理由 |
|---|---|---|
| 进度条 | #FF6B35 |
橙色,有活力,不刺眼 |
| 警示橙 | #FFA500 |
暂停按钮,用来区分开始 |
| 警示红 | #FF4444 |
时间到,给用户强烈提示 |
| 背景 | #FAFAFA |
浅灰白,干净清爽 |
| 文字 | #1a1a2e |
深色,数字要清晰 |
| 快捷按钮底色 | #FFF0E6 |
浅橙底,不是纯白,和主色呼应 |
🖼️ 截图

🚀 运行方法
1. 打开项目
DevEco Studio → File → Open → 选择 muban23 文件夹
2. 等待同步
右下角会出现 Sync Running,等它变成 ✅。
3. 选择运行设备
顶部工具栏选择你的设备(真机或模拟器)。
4. 运行
按 Shift + F10,或者点 ▶️ 按钮。
5. 验证
底部 Log 面板输入 testTag,看到这两行就算成功了:
Ability onWindowStageCreate
Succeeded in loading the content.
🔮 如果让我继续做……
下一步我想加这些:
🔔 震动反馈
vibrator.startVibrate({ duration: 500 });
🔊 提示音
audio.createAudioPlayer().then(player => {
player.src = 'resources/base/media/ding.mp3';
player.play();
});
🌙 深色模式
根据系统颜色模式,动态切换 #FAFAFA → #121212 这样的深色背景。
📊 历史记录
把用户常用的时间保存下来,下次直接点就用。
🗂️ 多计时器
同时跑多个倒计时,泡面一个、开会一个、跑步一个。
📁 项目文件一览
| 你要找 | 在这里 |
|---|---|
| 入口Ability | entry\src\main\ets\entryability\EntryAbility.ets |
| 主页代码 | entry\src\main\ets\pages\Index.ets |
| 模块配置 | entry\src\main\module.json5 |
| 全局配置 | AppScope\app.json5 |
| 依赖 | oh-package.json5 |
📚 我参考了这些
做这个倒计时器花了一个下午,踩了几个坑,但最后跑起来的那一刻还是挺有成就感的。
如果你也在学 HarmonyOS,建议从一个小项目开始。别想着一口气做个复杂的,先做一个能跑的东西出来,比什么都重要。
有问题欢迎交流,我们一起进步 💪
更多推荐



所有评论(0)