TimerApp - 计时器应用教程
项目介绍
项目背景
计时器/秒表是移动设备上最常用的功能之一。它展示了如何处理时间相关的逻辑,包括定时器的创建、暂停、恢复和清除。通过构建一个计时器应用,你将深入理解 HarmonyOS NEXT 中的定时器、时间格式化、状态管理等核心概念。
应用场景
计时器应用在日常生活中有着广泛的应用场景:
- 运动健身:记录跑步、游泳等运动时间
- 烹饪计时:控制烹饪时间,避免食物过熟或不熟
- 学习计时:番茄工作法,专注学习时间管理
- 工作计时:记录工作任务耗时,提高工作效率
- 实验计时:科学实验中的精确计时
功能特性
本计时器应用实现了以下功能:
- 开始/暂停:启动和暂停计时器
- 计次记录:记录多个时间点,用于对比分析
- 重置功能:清空所有数据,重新开始
- 精确显示:显示分钟、秒和百分秒
- 状态指示:通过颜色和文字指示当前状态
最终效果
应用界面分为以下区域:
- 头部区域:显示应用标题
- 计时器显示:圆形设计,显示当前时间
- 控制按钮:重置、开始/暂停、计次三个按钮
- 计次列表:显示所有计次记录
技术栈
- 开发框架:HarmonyOS NEXT API 23
- 编程语言:ArkTS
- UI 框架:ArkUI 声明式 UI
- 定时器:setInterval/clearInterval
- 时间处理 :Math.floor、padStart


开发环境准备
1. 创建项目
创建一个新的 HarmonyOS NEXT 项目:
方式一:使用 DevEco Studio 创建
- 打开 DevEco Studio
- 选择 "Create HarmonyOS Project"
- 选择 "Empty Ability" 模板
- 设置项目名称为 "TimerApp"
- 选择 API 版本为 23
方式二:复制模板项目
- 复制
project-template目录 - 重命名为 "TimerApp"
- 修改配置文件
2. 项目结构
TimerApp/
├── AppScope/ # 应用全局配置
│ ├── app.json5 # 应用级配置
│ └── resources/ # 应用级资源
├── entry/ # 主模块
│ └── src/main/
│ ├── ets/ # ArkTS 源代码
│ │ ├── entryability/ # UIAbility 入口
│ │ └── pages/ # 页面组件
│ └── resources/ # 资源文件
├── build-profile.json5 # 构建配置
└── oh-package.json5 # 依赖配置
知识点讲解
1. setInterval - 定时器详解
setInterval 是 JavaScript/TypeScript 中的全局函数,用于创建定时器,按照指定间隔重复执行代码。
基本语法:
typescript
// 创建定时器
const timerId = setInterval(callback, delay);
// callback: 定时执行的函数
// delay: 执行间隔(毫秒)
// 返回值: 定时器ID,用于清除定时器
在计时器中的应用:
typescript
// 每10毫秒执行一次,更新时间
this.timer = setInterval(() => {
this.time += 10; // 增加10毫秒
}, 10);
定时器的工作原理:
- 调用
setInterval创建定时器 - 每隔指定时间执行一次回调函数
- 回调函数更新状态变量
- 状态变量改变触发 UI 更新
- 用户看到时间变化
注意事项:
- 定时器会一直执行,直到被清除
- 每个定时器都有唯一的 ID
- 多次创建定时器会导致多个定时器同时运行
- 组件销毁时需要清除定时器,避免内存泄漏
2. clearInterval - 清除定时器详解
clearInterval 用于停止定时器,防止定时器继续执行。
基本语法:
typescript
// 清除定时器
clearInterval(timerId);
// timerId: setInterval 返回的定时器ID
在计时器中的应用:
typescript
// 暂停计时
stopTimer(): void {
this.isRunning = false;
clearInterval(this.timer); // 清除定时器
}
定时器管理最佳实践:
typescript
// 1. 创建定时器前先清除旧的
startTimer(): void {
this.stopTimer(); // 先清除
this.isRunning = true;
this.timer = setInterval(() => {
this.time += 10;
}, 10);
}
// 2. 组件销毁时清除定时器
aboutToDisappear(): void {
this.stopTimer();
}
// 3. 重置时清除定时器
resetTimer(): void {
this.stopTimer(); // 先暂停
this.time = 0; // 重置时间
this.laps = []; // 清空计次
}
3. 条件渲染 - 状态切换详解
根据状态显示不同的按钮文本和样式,实现动态 UI。
按钮文本切换:
typescript
Button(this.isRunning ? '暂停' : '开始')
按钮颜色切换:
typescript
.backgroundColor(this.isRunning ? '#F59E0B' : '#10B981')
边框颜色切换:
typescript
.border({
width: 4,
color: this.isRunning ? '#10B981' : '#E2E8F0'
})
条件渲染与样式结合:
typescript
Column() {
Text(this.formatTime(this.time))
.fontSize(64)
.fontColor('#1E293B')
// 根据状态显示提示文字
if (this.isRunning) {
Text('计时中...')
.fontSize(12)
.fontColor('#10B981')
.margin({ top: 8 })
}
}
.border({
width: 4,
color: this.isRunning ? '#10B981' : '#E2E8F0'
})
4. 字符串填充 - padStart详解
padStart 方法用于在字符串前面填充字符,达到指定长度。这在时间格式化中非常常用。
基本语法:
typescript
string.padStart(targetLength, padString)
// targetLength: 目标长度
// padString: 填充字符(默认为空格)
使用示例:
typescript
// 数字填充到2位
const minutes = 5;
const formatted = minutes.toString().padStart(2, '0');
// 结果: "05"
// 已经是2位的数字不会填充
const seconds = 15;
const formatted2 = seconds.toString().padStart(2, '0');
// 结果: "15"
// 填充其他字符
const padded = 'abc'.padStart(10, '-');
// 结果: "-------abc"
在时间格式化中的应用:
typescript
formatTime(ms: number): string {
const minutes = Math.floor(ms / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
const milliseconds = Math.floor((ms % 1000) / 10);
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(2, '0')}`;
}
5. 数学计算 - 时间转换详解
使用数学方法将毫秒转换为分、秒、毫秒。
时间单位换算:
- 1秒 = 1000毫秒
- 1分钟 = 60秒 = 60000毫秒
- 1小时 = 60分钟 = 3600000毫秒
提取分钟:
typescript
const minutes = Math.floor(ms / 60000);
// 例如: 125000毫秒 = 2分钟
提取秒:
typescript
const seconds = Math.floor((ms % 60000) / 1000);
// 例如: 125000毫秒 % 60000 = 5000毫秒
// 5000 / 1000 = 5秒
提取百分秒:
typescript
const milliseconds = Math.floor((ms % 1000) / 10);
// 例如: 125450毫秒 % 1000 = 450毫秒
// 450 / 10 = 45百分秒
完整格式化函数:
typescript
formatTime(ms: number): string {
// 提取分钟
const minutes = Math.floor(ms / 60000);
// 提取秒(取余后除以1000)
const seconds = Math.floor((ms % 60000) / 1000);
// 提取百分秒(取余后除以10)
const milliseconds = Math.floor((ms % 1000) / 10);
// 格式化为两位数
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(2, '0')}`;
}
6. fontFamily - 字体设置详解
fontFamily 属性用于设置字体,使用等宽字体可以让数字显示更整齐。
等宽字体的优势:
- 每个字符占据相同宽度
- 数字对齐整齐
- 时间变化时不会产生抖动
字体设置示例:
typescript
// 使用等宽字体
Text(this.formatTime(this.time))
.fontFamily('monospace')
// 使用系统字体
Text('系统字体')
.fontFamily('sans-serif')
// 使用特定字体
Text('特定字体')
.fontFamily('HarmonyOS Sans')
在计时器中的应用:
typescript
Text(this.formatTime(this.time))
.fontSize(64)
.fontWeight(FontWeight.Bold)
.fontColor('#1E293B')
.fontFamily('monospace') // 等宽字体,数字对齐
7. 边框样式 - border详解
border 属性用于设置组件的边框样式,支持宽度、颜色、样式等。
基本语法:
typescript
.border({
width: 边框宽度,
color: 边框颜色,
style: 边框样式
})
边框样式选项:
typescript
// 实线(默认)
.style(BorderStyle.Solid)
// 虚线
.style(BorderStyle.Dashed)
// 点线
.style(BorderStyle.Dotted)
在计时器中的应用:
typescript
Column() {
// 内容
}
.width(280)
.height(280)
.borderRadius(140)
.backgroundColor('#F8FAFC')
.border({
width: 4,
color: this.isRunning ? '#10B981' : '#E2E8F0'
})
边框动画效果:
typescript
.border({
width: 4,
color: this.isRunning ? '#10B981' : '#E2E8F0'
})
.animation({
duration: 300,
curve: Curve.EaseInOut
})
8. 数组操作 - push和unshift详解
push - 添加到末尾:
typescript
// 添加到数组末尾
this.laps.push(this.time);
// 添加多个元素
this.laps.push(time1, time2, time3);
unshift - 添加到开头:
typescript
// 添加到数组开头
this.laps.unshift(this.time);
在计时器中的应用:
typescript
// 记录当前时间到计次列表
if (this.isRunning) {
this.laps.push(this.time); // 添加到末尾
}
其他常用数组方法:
typescript
// 删除最后一个元素
this.laps.pop();
// 删除第一个元素
this.laps.shift();
// 删除指定位置的元素
this.laps.splice(index, 1);
// 清空数组
this.laps = [];
9. 空数组检查详解
检查数组是否为空,显示不同的内容。
检查方法:
typescript
// 方法1:检查length
if (this.laps.length > 0) {
// 有内容
}
// 方法2:检查length === 0
if (this.laps.length === 0) {
// 空数组
}
// 方法3:直接判断
if (this.laps.length) {
// 有内容
}
在计时器中的应用:
typescript
if (this.laps.length > 0) {
// 显示计次列表
Column() {
Text('计次记录')
List() {
ForEach(this.laps, ...)
}
}
} else {
// 显示空状态
Text('暂无计次记录')
}
10. 奇偶行样式详解
根据索引设置不同的背景颜色,实现斑马纹效果,提高可读性。
基本实现:
typescript
.backgroundColor(index % 2 === 0 ? '#FFFFFF' : '#F8FAFC')
原理说明:
index % 2 === 0:偶数行index % 2 !== 0:奇数行- 交替使用不同的背景颜色
在计时器中的应用:
typescript
ForEach(this.laps, (lap: number, index: number) => {
ListItem() {
Row() {
Text(`第 ${index + 1} 次`)
Blank()
Text(this.formatTime(lap))
}
.width('100%')
.padding(16)
.backgroundColor(index % 2 === 0 ? '#FFFFFF' : '#F8FAFC')
.borderRadius(8)
}
})
扩展应用:
typescript
// 更多样式变化
.backgroundColor(index % 2 === 0 ? '#FFFFFF' : '#F8FAFC')
.borderRadius(index % 2 === 0 ? 8 : 12)
完整代码解析
页面组件定义
typescript
@Entry
@Component
struct Index {
@State time: number = 0; // 当前时间(毫秒)
@State isRunning: boolean = false; // 是否正在运行
@State timer: number = 0; // 定时器ID
@State laps: number[] = []; // 计次记录数组
build() {
Column() {
// 头部
this.HeaderSection()
// 计时器显示
this.TimerDisplay()
// 控制按钮
this.ControlButtons()
// 计次列表
this.LapList()
}
.padding(16)
.width('100%')
.height('100%')
.backgroundColor('#F8FAFC')
}
}
头部区域
typescript
@Builder HeaderSection() {
Text('计时器')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#1E293B')
.width('100%')
.padding({ bottom: 32 })
}
计时器显示区域
typescript
@Builder TimerDisplay() {
Column() {
// 时间显示
Text(this.formatTime(this.time))
.fontSize(64)
.fontWeight(FontWeight.Bold)
.fontColor('#1E293B')
.fontFamily('monospace') // 等宽字体
// 运行状态提示
if (this.isRunning) {
Text('计时中...')
.fontSize(12)
.fontColor('#10B981')
.margin({ top: 8 })
}
}
.alignItems(HorizontalAlign.Center)
.width(280)
.height(280)
.borderRadius(140) // 圆形
.backgroundColor('#F8FAFC')
.border({
width: 4,
color: this.isRunning ? '#10B981' : '#E2E8F0' // 根据状态改变边框颜色
})
.margin({ bottom: 32 })
}
控制按钮
typescript
@Builder ControlButtons() {
Row() {
// 重置按钮
Button('重置')
.width(100)
.height(56)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.backgroundColor('#FEE2E2')
.fontColor('#EF4444')
.borderRadius(16)
.onClick(() => {
this.resetTimer();
})
// 开始/暂停按钮
Button(this.isRunning ? '暂停' : '开始')
.width(140)
.height(56)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.backgroundColor(this.isRunning ? '#F59E0B' : '#10B981')
.fontColor('#FFFFFF')
.borderRadius(16)
.margin({ left: 16, right: 16 })
.onClick(() => {
if (this.isRunning) {
this.stopTimer();
} else {
this.startTimer();
}
})
// 计次按钮
Button('计次')
.width(100)
.height(56)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.backgroundColor('#FEF3C7')
.fontColor('#F59E0B')
.borderRadius(16)
.onClick(() => {
if (this.isRunning) {
this.laps.push(this.time); // 记录当前时间
}
})
}
}
计次列表
typescript
@Builder LapList() {
if (this.laps.length > 0) {
Column() {
// 列表头部
Row() {
Text('计次记录')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#1E293B')
Blank()
Text(`${this.laps.length} 次`)
.fontSize(14)
.fontColor('#64748B')
}
.width('100%')
.margin({ bottom: 16 })
// 计次列表
List() {
ForEach(this.laps, (lap: number, index: number) => {
ListItem() {
Row() {
Text(`第 ${index + 1} 次`)
.fontSize(16)
.fontColor('#64748B')
Blank()
Text(this.formatTime(lap))
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#1E293B')
.fontFamily('monospace')
}
.width('100%')
.padding(16)
.backgroundColor(index % 2 === 0 ? '#FFFFFF' : '#F8FAFC')
.borderRadius(8)
}
}, (lap: number, index: number) => index.toString())
}
.layoutWeight(1)
.width('100%')
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(24)
}
}
定时器方法
typescript
// 开始计时
startTimer(): void {
this.isRunning = true;
// 创建定时器,每10毫秒执行一次
this.timer = setInterval(() => {
this.time += 10; // 增加10毫秒
}, 10);
}
// 暂停计时
stopTimer(): void {
this.isRunning = false;
clearInterval(this.timer); // 清除定时器
}
// 重置计时器
resetTimer(): void {
this.stopTimer(); // 先暂停
this.time = 0; // 重置时间
this.laps = []; // 清空计次记录
}
时间格式化方法
typescript
// 格式化时间
formatTime(ms: number): string {
// 计算分钟
const minutes = Math.floor(ms / 60000);
// 计算秒
const seconds = Math.floor((ms % 60000) / 1000);
// 计算百分秒(10毫秒为单位)
const milliseconds = Math.floor((ms % 1000) / 10);
// 格式化为两位数
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(2, '0')}`;
}
常见问题与解决方案
问题1:定时器精度问题
问题描述:定时器可能不完全准确,存在微小误差。
解决方案:
typescript
// 使用更精确的时间计算
const startTime = Date.now();
setInterval(() => {
this.time = Date.now() - startTime;
}, 10);
问题2:内存泄漏
问题描述:组件销毁时定时器仍在运行。
解决方案:
typescript
// 在组件销毁时清除定时器
aboutToDisappear(): void {
this.stopTimer();
}
问题3:快速点击导致多个定时器
问题描述:快速点击开始按钮会创建多个定时器。
解决方案:
typescript
startTimer(): void {
this.stopTimer(); // 先清除旧的定时器
this.isRunning = true;
this.timer = setInterval(() => {
this.time += 10;
}, 10);
}
扩展学习
1. 添加更多功能
- 倒计时功能
- 多个计时器
- 历史记录保存
- 声音提醒
2. 优化用户体验
- 动画效果
- 触觉反馈
- 全屏模式
- 悬浮窗
3. 数据持久化
- 保存计次记录
- 导出数据
- 数据统计分析
总结
通过本教程,你学习了:
- setInterval/clearInterval - 定时器的创建和清除
- 条件渲染 - 根据状态显示不同内容
- padStart - 字符串填充
- Math.floor - 数学计算
- fontFamily - 字体设置
- border - 边框样式
- 数组操作 - push 添加元素
- 奇偶行样式 - 斑马纹效果
- 时间格式化 - 毫秒转换为分秒毫秒
- 状态管理 - 定时器状态控制
这些知识点构成了 HarmonyOS NEXT 中定时器类应用开发的基础,掌握它们后,你将能够构建闹钟、倒计时、番茄钟等时间相关的应用。