【鸿蒙原生应用开发--ArkUI--007】TimerApp - 计时器应用教程

TimerApp - 计时器应用教程

项目介绍

项目背景

计时器/秒表是移动设备上最常用的功能之一。它展示了如何处理时间相关的逻辑,包括定时器的创建、暂停、恢复和清除。通过构建一个计时器应用,你将深入理解 HarmonyOS NEXT 中的定时器、时间格式化、状态管理等核心概念。

应用场景

计时器应用在日常生活中有着广泛的应用场景:

  • 运动健身:记录跑步、游泳等运动时间
  • 烹饪计时:控制烹饪时间,避免食物过熟或不熟
  • 学习计时:番茄工作法,专注学习时间管理
  • 工作计时:记录工作任务耗时,提高工作效率
  • 实验计时:科学实验中的精确计时

功能特性

本计时器应用实现了以下功能:

  1. 开始/暂停:启动和暂停计时器
  2. 计次记录:记录多个时间点,用于对比分析
  3. 重置功能:清空所有数据,重新开始
  4. 精确显示:显示分钟、秒和百分秒
  5. 状态指示:通过颜色和文字指示当前状态

最终效果

应用界面分为以下区域:

  • 头部区域:显示应用标题
  • 计时器显示:圆形设计,显示当前时间
  • 控制按钮:重置、开始/暂停、计次三个按钮
  • 计次列表:显示所有计次记录

技术栈

  • 开发框架:HarmonyOS NEXT API 23
  • 编程语言:ArkTS
  • UI 框架:ArkUI 声明式 UI
  • 定时器:setInterval/clearInterval
  • 时间处理 :Math.floor、padStart


开发环境准备

1. 创建项目

创建一个新的 HarmonyOS NEXT 项目:

方式一:使用 DevEco Studio 创建

  1. 打开 DevEco Studio
  2. 选择 "Create HarmonyOS Project"
  3. 选择 "Empty Ability" 模板
  4. 设置项目名称为 "TimerApp"
  5. 选择 API 版本为 23

方式二:复制模板项目

  1. 复制 project-template 目录
  2. 重命名为 "TimerApp"
  3. 修改配置文件

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);

定时器的工作原理:

  1. 调用 setInterval 创建定时器
  2. 每隔指定时间执行一次回调函数
  3. 回调函数更新状态变量
  4. 状态变量改变触发 UI 更新
  5. 用户看到时间变化

注意事项:

  • 定时器会一直执行,直到被清除
  • 每个定时器都有唯一的 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. 数据持久化

  • 保存计次记录
  • 导出数据
  • 数据统计分析

总结

通过本教程,你学习了:

  1. setInterval/clearInterval - 定时器的创建和清除
  2. 条件渲染 - 根据状态显示不同内容
  3. padStart - 字符串填充
  4. Math.floor - 数学计算
  5. fontFamily - 字体设置
  6. border - 边框样式
  7. 数组操作 - push 添加元素
  8. 奇偶行样式 - 斑马纹效果
  9. 时间格式化 - 毫秒转换为分秒毫秒
  10. 状态管理 - 定时器状态控制

这些知识点构成了 HarmonyOS NEXT 中定时器类应用开发的基础,掌握它们后,你将能够构建闹钟、倒计时、番茄钟等时间相关的应用。

相关推荐
nashane1 小时前
HarmonyOS 6学习:保存图片预览空白?沙箱路径转URI的“视觉修复”术
学习·华为·harmonyos
芒鸽2 小时前
HarmonyOS ArkTS 状态管理深度解析:@State、@Prop、@Link、@Provide/@Consume 实战指南
华为·harmonyos·arkts·状态管理
大雷神2 小时前
HarmonyOS APP<玩转React>开源教程三十一:示例项目下载功能
react.js·开源·harmonyos
想你依然心痛3 小时前
HarmonyOS 6(API 23)智能体驱动的沉浸式AR量子计算实验室
ar·harmonyos·量子计算·智能体
技术路线图3 小时前
鸿蒙系统小红书应用分身设置教程(2026详细版)
华为·harmonyos
科技与数码3 小时前
鸿蒙智能体框架HMAF与智能化升级全解析
华为·harmonyos
不羁的木木3 小时前
HarmonyOS文件基础服务(Core File Kit)实战演练01-核心概念与架构设计
华为·harmonyos
大雷神4 小时前
第28篇|相机失败态:没有权限、没有设备、会话失败时如何提示
harmonyos
不羁的木木5 小时前
Form Kit(卡片开发服务)学习笔记05-进阶实战与性能优化
笔记·学习·harmonyos