一、引言

倒计时是移动端应用中最常见的功能之一。电商 App 的秒杀倒计时、外卖 App 的配送倒计时、验证码页面的 60 秒重发倒计时、番茄钟的 25 分钟专注倒计时、运动 App 的跑步计时——这些场景都有一个共同的核心需求:一个可控的、格式化显示的、精确到秒的计时器。

传统实现计时器需要组合 setInterval + 格式化函数 + 自动刷新 UI + 开始/暂停/重置状态管理。代码量不小,而且有几个容易出错的点:定时器的清除时机(防止内存泄漏)、计时精度(setInterval 的累积偏差)、跨页面的时间同步(切到后台再回来时间不准)。如果还要支持正计时和倒计时两种模式的切换,状态管理和控制逻辑会更加复杂。

HarmonyOS 提供了 TextTimer 组件——一个将计时逻辑和文本显示合二为一的组件。它内置了精确的计时引擎(不受 setInterval 累积偏差影响),自动处理时间格式化(mm:ssHH:mm:ss),通过 TextTimerController 提供标准的开始/暂停/重置控制接口。开发者不需要管理定时器生命周期,不需要编写格式化函数——只需配置参数和绑定控制器。

本文通过一个倒计时器 Demo 深入讲解 TextTimer 组件的核心用法:如何设置倒计时和正计时?如何使用 TextTimerController 控制计时?如何通过 format 定制时间显示格式?如何构建预设时长选择器?

阅读完本文,你将能够:

  • 使用 TextTimer 组件实现倒计时和正计时两种模式
  • 使用 TextTimerControllerstart()/pause()/reset() 方法控制计时流程
  • 使用 format() 方法定制时间显示格式
  • 结合 onTimer 回调实现进度条、颜色变化等联动效果
  • 构建预设时长选择和自定义计时持续时间的交互

二、TextTimer 组件 API 总览

2.1 构造函数

TextTimer(options?: TextTimerOptions)

TextTimer 的构造函数接收一个可选配置对象,定义计时模式、时长和控制器:

interface TextTimerOptions {
  isCountDown?: boolean;  // 是否倒计时模式,默认 false(正计时)
  count?: number;          // 计时范围,单位毫秒,最大 86400000(24小时),默认 60000
  controller?: TextTimerController;  // 控制器,用于开始/暂停/重置
}

最简用法——默认 60 秒倒计时:

TextTimer({ isCountDown: true, count: 60000 })

2.2 TextTimerController —— 计时控制器

TextTimerController 是控制 TextTimer 行为的核心接口,提供三个方法:

方法 用途 说明
start() 开始/继续计时 首次调用开始计时,暂停后调用继续计时
pause() 暂停计时 保持当前时间,再次 start() 后继续
reset() 重置计时 回到初始状态(倒计时回到 count 值,正计时回到 0)

使用模式:

private timerController: TextTimerController = new TextTimerController();

TextTimer({
  isCountDown: true,
  count: 300000,
  controller: this.timerController
})

// 控制按钮
Button('开始').onClick(() => { this.timerController.start(); })
Button('暂停').onClick(() => { this.timerController.pause(); })
Button('重置').onClick(() => { this.timerController.reset(); })

注意:reset() 后需要重新调用 start() 才开始计时。pause() 后调用 start() 会从暂停位置继续,不会从初始值重新开始。

2.3 format —— 时间显示格式

format() 方法控制时间的显示格式:

TextTimer({ isCountDown: true, count: 3600000 })
  .format('HH:mm:ss')  // 显示为 "01:00:00"
格式字符串 显示示例 适用场景
'mm:ss' 05:30 短时间(<1h),如验证码、番茄钟
'HH:mm:ss' 01:05:30 长时间(>=1h),如赛事计时、配送计时
'mm:ss.SS' 05:30.50 需要毫秒精度,如运动计时
'HH:mm:ss.SS' 01:05:30.50 完整格式

默认格式为 'mm:ss'

2.4 核心样式方法

方法 用途 示例
fontSize(value) 字体大小 .fontSize(64)
fontColor(value) 字体颜色 .fontColor('#1a1a2e')
fontWeight(value) 字体粗细 .fontWeight(FontWeight.Bold)
fontFamily(value) 字体族(等宽字体推荐) .fontFamily('monospace')

使用 monospace 等宽字体可以避免数字变化时文字左右跳动——每个数字字符在等宽字体中占据相同宽度。

2.5 onTimer —— 计时回调

onTimer 在每次计时更新时触发(每秒一次),提供两个参数:

TextTimer({ isCountDown: true, count: 300000, controller: this.timerController })
  .onTimer((utc: number, elapsedTime: number) => {
    // utc: 当前显示的绝对时间戳(较少使用)
    // elapsedTime: 已经过的时间,单位毫秒
    this.progress = Math.round((elapsedTime / this.totalTime) * 100);
  })

elapsedTime 是计时器开始后累计的时间:

  • 倒计时模式:从 0 递增到 count(计时结束)
  • 正计时模式:从 0 开始持续递增

利用 elapsedTime 可以计算进度百分比,驱动进度条、颜色变化、振动提醒等联动效果。

2.6 倒计时 vs 正计时

TextTimer 通过 isCountDown 参数切换两种模式:

模式 isCountDown count 含义 显示内容
倒计时 true 倒计时的初始时长(ms) 从 count 递减到 0
正计时 false 无效(正计时没有上限) 从 0 开始递增

倒计时模式下,count 必须在 0 ~ 86400000(24 小时)之间。超出范围的值会被替换为默认值 60000(1 分钟)。

正计时模式下,count 参数无效——计时器从 0 开始持续增加,没有上限。
在这里插入图片描述

三、Demo 设计:倒计时器

3.1 功能概述

Demo 是一个倒计时器页面,支持倒计时和正计时两种模式:

  1. 大号时间显示:64sp 的等宽字体,醒目展示剩余/已过时间
  2. 6 种预设时长:1 分钟、3 分钟、5 分钟、10 分钟、25 分钟(番茄钟)、60 分钟
  3. 开始/暂停/继续/重置控制:三种状态(未开始、运行中、已暂停)对应不同的按钮组合
  4. 倒计时/正计时模式切换:分段按钮切换两种模式
  5. 显示格式切换:Toggle 控制是否显示小时位(mm:ss ↔ HH:mm:ss)
  6. 进度可视化:倒计时模式下,彩色进度条实时反映剩余时间比例(蓝→橙→红三级变色)

3.2 状态管理

@State isCountDown: boolean = true;     // 倒计时/正计时
@State timerCount: number = 300000;      // 当前时长(ms)
@State isRunning: boolean = false;       // 是否运行中
@State isPaused: boolean = false;        // 是否已暂停
@State showHours: boolean = false;       // 是否显示小时
@State elapsedPercent: number = 0;       // 已过时间百分比
private timerController: TextTimerController = new TextTimerController();

三个布尔状态(isRunningisPaused)组合出三种 UI 状态:

  • !isRunning && !isPaused:未开始 —— 显示"开始"按钮
  • isRunning:运行中 —— 显示"暂停"和"重置"按钮
  • !isRunning && isPaused:已暂停 —— 显示"继续"和"重置"按钮

3.3 TextTimer 核心配置

TextTimer({
  isCountDown: this.isCountDown,
  count: this.timerCount,
  controller: this.timerController
})
  .format(this.showHours ? 'HH:mm:ss' : 'mm:ss')
  .fontSize(64)
  .fontColor('#1a1a2e')
  .fontWeight(FontWeight.Bold)
  .fontFamily('monospace')
  .onTimer((utc: number, elapsedTime: number) => {
    if (this.isCountDown && this.timerCount > 0) {
      this.elapsedPercent = Math.min(100,
        Math.round((elapsedTime / this.timerCount) * 100));
    }
  })

关键细节:

  • format() 通过 showHours 布尔值动态切换——不重新创建组件,只改变显示格式
  • fontFamily('monospace') 确保数字不跳动——这在计时器中是重要的 UI 细节
  • onTimer 中计算 elapsedPercent 用于驱动进度条,注意使用 Math.min(100, ...) 防止超过 100%

3.4 控制器操作

startTimer() {
  if (this.isPaused) {
    this.timerController.start();  // 从暂停位置继续
  } else {
    this.timerController.reset();  // 首次开始前先重置
    this.timerController.start();
  }
  this.isRunning = true;
  this.isPaused = false;
}

pauseTimer() {
  this.timerController.pause();
  this.isRunning = false;
  this.isPaused = true;
}

resetTimer() {
  this.timerController.reset();
  this.isRunning = false;
  this.isPaused = false;
  this.elapsedPercent = 0;
}

startTimer() 中的逻辑值得注意:如果之前已暂停,直接 start() 继续;如果是从头开始(更改了时长或切换了模式),先 reset()start()。TextTimerController 本身不跟踪状态——状态管理完全由开发者的 @State 变量负责。

3.5 进度条联动

进度条是倒计时体验的重要组成部分,直观展示剩余时间:

Row() {
  Row()
    .width(`${this.elapsedPercent}%`)
    .height(4)
    .borderRadius(2)
    .backgroundColor(
      this.elapsedPercent > 80 ? '#FF4D4F' :   // 80%+ → 红色(紧急)
      this.elapsedPercent > 50 ? '#FF9800' :   // 50%+ → 橙色(注意)
      '#1677FF'                                 // <50% → 蓝色(正常)
    )
    .animation({ duration: 500, curve: Curve.Linear })
}
.width('80%')
.height(4)
.borderRadius(2)
.backgroundColor('#F2F3F5')

进度条长度表示已过时间的百分比,颜色从蓝到橙到红渐变——接近截止时间时自动变红,提醒用户时间紧迫。

3.6 页面结构

┌──────────────────────────────────────────┐
│ ⏱️ 倒计时器(深色标题栏)                 │
├──────────────────────────────────────────┤
│ 📘 TextTimer 组件说明卡片                │
├──────────────────────────────────────────┤
│ ┌ 倒计时 ──────────────────────────┐    │
│ │                                   │    │
│ │        05:00                      │    │
│ │    ████████░░░░░░░░  50%          │    │
│ │                                   │    │
│ └───────────────────────────────────┘    │
├──────────────────────────────────────────┤
│ ┌ 控制按钮 ────────────────────────┐    │
│ │  [ ▶ 开始 ]                       │    │
│ │  或 [ ⏸ 暂停 ]  [ ↺ 重置 ]       │    │
│ │  或 [ ▶ 继续 ]  [ ↺ 重置 ]       │    │
│ └───────────────────────────────────┘    │
├──────────────────────────────────────────┤
│ ┌ 计时模式 ────────────────────────┐    │
│ │  [ 倒计时 ]  [ 正计时 ]           │    │
│ └───────────────────────────────────┘    │
├──────────────────────────────────────────┤
│ ┌ 预设时长 ────────────────────────┐    │
│ │ [1分钟] [3分钟] [5分钟]           │    │
│ │ [10分钟] [25分钟] [60分钟]        │    │
│ └───────────────────────────────────┘    │
├──────────────────────────────────────────┤
│ ┌ 显示格式 ────────────────────────┐    │
│ │ 显示时               [Toggle]     │    │
│ └───────────────────────────────────┘    │
└──────────────────────────────────────────┘

在这里插入图片描述

四、TextTimer 组件的最佳实践

4.1 控制器与 UI 状态分离

TextTimerController 只负责计时器的底层控制(start/pause/reset),不跟踪"是否在运行"等 UI 状态。这意味着你需要用 @State 变量管理 UI 状态,并在控制器操作中同步更新:

// 好的做法:控制器操作 + @State 同步
this.timerController.pause();
this.isRunning = false;
this.isPaused = true;

// 不好的做法:只操作控制器,@State 不同步
// 结果:UI 不更新,按钮显示错误
this.timerController.pause();

三个 UI 状态(未开始/运行中/已暂停)通过 isRunningisPaused 的布尔组合表达,在 build() 中用条件渲染来显示不同的按钮。

4.2 预设时长的选择

对于倒计时功能,预设时长选择可以提升用户体验——用户不需要手动输入时间,一键点击即可:

private presets: PresetItem[] = [
  { label: '1 分钟', ms: 60000 },
  { label: '3 分钟', ms: 180000 },
  { label: '5 分钟', ms: 300000 },
  { label: '10 分钟', ms: 600000 },
  { label: '25 分钟', ms: 1500000 },   // 番茄钟标准时长
  { label: '60 分钟', ms: 3600000 }
];

预设的选择原则:

  • 覆盖常用场景:1/3/5 分钟用于短操作,25 分钟用于专注,60 分钟用于长时间
  • 有清晰的标签(“分钟"而非"min”)——面向中文用户
  • 切换预设时自动重置计时器——防止用户困惑(旧计时还在跑,新时长已设置)

4.3 format 的动态切换

format() 在计时过程中可以动态切换——TextTimer 会自动调整显示格式,不影响计时的内部状态。这意味着:

  • 倒计时跑到一半时切换 mm:ssHH:mm:ss,计时不会中断
  • 当时长 < 1 小时时,HH:mm:ss 会显示为 00:mm:ss——这是预期行为,而非 bug

建议:根据时长自动选择格式——时长 >= 1 小时默认用 HH:mm:ss,< 1 小时默认用 mm:ss。Demo 中将选择权交给用户(通过 Toggle 切换),但也可以自动化。

4.4 等宽字体的重要性

计时器的数字每秒变化一次。如果使用比例字体(如默认的系统字体),数字 “1” 和 “5” 的宽度不同,导致数字变化时整个文本左右微微跳动。这个跳动在 64sp 大字体下尤为明显。

解决方式:设置 fontFamily('monospace')。等宽字体中每个字符宽度相同,数字变化不会产生布局抖动。

4.5 count 参数的限制

倒计时模式下的 count 参数有硬限制:

  • 范围:0 ~ 86400000(24 小时)
  • 超出范围的值会被替换为 60000(1 分钟)
  • 这意味着 TextTimer 不适合超过 24 小时的计时场景(如"距离生日还有 X 天")

对于超过 24 小时的倒计时(如活动倒计时),需要结合 Date 计算 + 自定义文本显示,或使用多个 TextTimer 分别显示天/时/分/秒。

4.6 页面生命周期与计时器

TextTimer 在页面不可见时(如切换到其他 Tab 或应用退到后台)继续计时——其内部使用系统时间差来保证精度,而非依赖 setInterval 的累加。

这意味着:

  • 用户切到后台几分钟再回来,计时器显示的时间是正确的(基于实际时间差,而非前台运行时间)
  • 不需要额外处理 onPageShow/onPageHide 生命周期
  • 不需要手动暂停/恢复——TextTimer 内部自行处理

但注意:如果倒计时在后台走到了 0,回到前台时 onTimer 不会收到到期的回调。如果需要"计时结束"的精确触发(如播放音效、弹出通知),应该在 onTimer 中检查 elapsedTime >= count 而非依赖"恰好触发一次"。

五、完整代码结构

TextTimerPage (~295 行)
├── 接口
│   └── PresetItem — 预设时长数据接口
├── 状态变量
│   ├── @State isCountDown — 倒计时/正计时
│   ├── @State timerCount — 当前时长(ms)
│   ├── @State isRunning / isPaused — 控制器状态
│   ├── @State showHours — 格式切换
│   ├── @State elapsedPercent — 进度百分比
│   └── @State selectedPreset — 当前选中的预设
├── 实例
│   ├── timerController: TextTimerController
│   └── presets: PresetItem[] — 6 种预设时长
├── 方法
│   ├── startTimer() / pauseTimer() / resetTimer()
│   └── selectPreset() / formatString()
├── 视图
│   ├── 标题栏 — ⏱️ 倒计时器
│   ├── 说明卡片 — TextTimer 组件介绍
│   ├── 计时显示区(TextTimer + 进度条)
│   ├── 控制按钮(开始/暂停/继续/重置)
│   ├── 模式切换(倒计时/正计时)
│   ├── 预设时长选择
│   └── 格式切换 Toggle
└── (无 @Builder——所有布局内联)

六、总结

本文通过一个倒计时器 Demo 深入讲解了 HarmonyOS 中的 TextTimer 计时器组件。TextTimer 将精确计时、时间格式化和文本渲染整合为一个声明式组件,通过 TextTimerController 提供标准化的控制接口,通过 format() 灵活配置显示格式,通过 onTimer 回调实现进度联动。

核心要点回顾:

  1. TextTimer 是"计时引擎 + 显示器"的二合一组件:内部使用系统时间差保证精度(不受 setInterval 偏差影响),外部通过声明式参数控制行为和样式。开发者不需要管理定时器生命周期。

  2. TextTimerController 提供三个标准操作start() 开始/继续,pause() 暂停,reset() 回到初始值。控制器不追踪 UI 状态——状态管理由开发者用 @State 变量负责。

  3. isCountDown 切换两种计时模式:倒计时从 count 递减到 0(适合秒杀、验证码、番茄钟),正计时从 0 开始递增(适合运动计时、工作计时)。

  4. format() 自定义显示格式mm:ss 适合短时间,HH:mm:ss 适合长时间。格式可在计时过程中动态切换,不影响计时内部状态。

  5. 等宽字体是计时器的 UI 细节fontFamily('monospace') 避免数字变化时的布局抖动——这个细节在 64sp 大字体下尤为明显。

  6. onTimer 实现进度联动:通过 elapsedTime 参数计算进度百分比,驱动进度条、颜色变化、振动等联动效果——构建丰富的计时体验。

TextTimer 是 ArkUI 中"时间推移的可视化"组件——它不只是一个显示数字的 Text,而是一个集成了计时逻辑和显示控制的完整组件。正是这种"逻辑 + 显示"的一体化设计,使得在 HarmonyOS 中实现一个倒计时器只需几行代码,而不是几十行 setInterval + format 的手动组合。

七、扩展思考

TextTimer 覆盖了基础的计时需求,但在实际项目中,计时功能有更多变化:

验证码倒计时:这是 TextTimer 最常见的实战场景。在发送验证码按钮旁边显示"60s 后重发",倒计时结束后按钮恢复可点击。实现时需要注意:倒计时值(60 秒)和按钮禁用状态需要同步——onTimer 中检查 elapsedTime >= count 时恢复按钮。

全屏番茄钟:TextTimer 可以结合背景色变化、音效、振动等,构建完整的番茄工作法体验。倒计时到 0 时触发全屏提示——这个"到 0"的判断在 onTimer 中进行。

多段计时器:某些场景需要多个计时阶段——如高强度间歇训练(HIIT)在"运动 30s → 休息 10s"之间循环。这需要在 onTimer 中检测当前阶段是否结束,自动切换到下一个阶段并重置控制器。

TextTimer 与 TextClock 的区别:TextClock 显示的是系统当前时间(如 15:30:00),TextTimer 显示的是计时时间(如 05:00)。TextClock 持续更新但不可控制,TextTimer 需要控制器操作但显示的是相对时间。两者在功能上互补——如闹钟页面可以同时用 TextClock 显示当前时间和 TextTimer 显示倒计时。

与后端的时间同步:对于需要精确到秒的活动倒计时(如秒杀、拍卖),前端的 TextTimer 应以后端返回的活动结束时间为准。在创建 TextTimer 前,先计算 后端结束时间 - 当前服务器时间 = 剩余毫秒数,将此差值作为 count 参数。

理解 TextTimer 的定位——可控的、格式化的时间流逝显示组件——是正确使用它的关键。它不是时钟(那是 TextClock 的职责),不是日期选择器(那是 DatePicker/TimePicker 的职责),而正是这种"让我告诉你还剩多少时间"的单一职责,使得 TextTimer 成为了秒杀、验证码、番茄钟等所有需要"可视化时间线"场景的最佳选择。

通过本文的 Demo——倒计时器页面,你将 TextTimer 组件与控制器、预设选择和进度条组合在一起,构建了一个完整的计时工具。这个页面展示了 TextTimer 作为"计时引擎 + 显示器"的核心价值——用最简洁的 API 完成最高频的需求。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐