时间显示是移动应用中最基础的需求之一。HarmonyOS NEXT ArkUI 提供了 TextClock(文本时钟)和 TextTimer(文本计时器)两个组件,分别覆盖"看时间"和"计时"两大场景。上一篇文章我们深入探讨了 TextClock 的用法,本文聚焦 TextTimer——一个专为倒计时和正计时设计的文本计时组件。

我们将通过一个完整的"专注计时器"(番茄钟)实战案例,展示 TextTimer 在倒计时场景中的完整用法,包括控制器操作、格式切换、预设选择和状态管理。

关键词:HarmonyOS、ArkUI、TextTimer、计时器、倒计时、番茄钟

一、TextTimer 组件概览

TextTimer 是 ArkUI 中专门用于计时显示的基础组件。它与 TextClock 的核心区别在于:

特性 TextClock TextTimer
数据来源 系统时间 应用层控制
主要用途 显示当前时间 倒计时 / 正计时
控制方式 被动更新 TextTimerController 主动控制
典型场景 时钟展示 番茄钟、验证码、运动计时

TextTimer 通过 TextTimerController 提供三个核心操作:start()(开始计时)、pause()(暂停计时)、reset()(重置计时)。配合 isCountDown 参数,可灵活切换正计时和倒计时两种模式。

二、TextTimer 核心 API

2.1 构造函数

TextTimer({
  isCountDown: boolean,   // true=倒计时, false=正计时
  count: number,          // 计时总时长,单位毫秒
  controller: TextTimerController  // 计时器控制器
})

其中 isCountDowncount 决定了 TextTimer 的核心行为:

  • isCountDown = true 时,TextTimer 从 count 表示的时长开始倒数,直到 0
  • isCountDown = false 时,TextTimer 从 0 开始递增

2.2 TextTimerController

TextTimerController 是控制计时器行为的核心对象,提供三个方法:

class TextTimerController {
  start(): void;   // 开始计时(从当前值继续)
  pause(): void;   // 暂停计时(保留当前值)
  reset(): void;   // 重置计时(回到 count 值)
}

这三个方法的语义非常直观:

  • start:在空闲或暂停状态下启动计时。如果之前已暂停,调用 start 会从暂停时的剩余时间继续
  • pause:在运行状态下暂停。暂停后当前剩余时间被保留
  • reset:在任何状态下将计时器重置到初始的 count

2.3 format 属性

format 属性控制 TextTimer 的文本显示格式,支持以下模式:

格式字符串 显示效果 适用场景
'mm:ss' 25:00 分钟级倒计时(推荐)
'mm:ss.SS' 25:00.00 需要显示百分秒
'HH:mm:ss' 00:25:00 小时级计时

选择合适的格式很重要。对于番茄钟这类 25-60 分钟的场景,mm:ss 最为清晰;对于超过 60 分钟的长时间计时,HH:mm:ss 能更好地表达总时长。
在这里插入图片描述

三、实战:专注计时器(番茄钟)

番茄工作法是一种经典的时间管理技术:设定 25 分钟专注时间,期间不受干扰地工作,完成后休息 5 分钟。我们将实现一个完整的番茄钟计时器,包含以下功能点:

  1. 预设时长选择:25 分钟、45 分钟、60 分钟三档
  2. 开始 / 暂停 / 重置:完整的计时器操作
  3. 格式切换:mm:ss 与 HH:mm:ss 之间切换
  4. 完成计数:记录今日完成的番茄数

3.1 状态设计

计时器页面涉及多个维度的状态,需要清晰的状态设计:

@State timerStatus: number = 0;
// 0 = idle(就绪,等待开始)
// 1 = running(专注中)
// 2 = paused(已暂停)
// 3 = finished(完成)

@State totalSeconds: number = 25 * 60;   // 倒计时总秒数
@State showHours: boolean = false;       // 是否显示小时位
@State sessionCount: number = 0;         // 完成的番茄数
@State currentPreset: number = 25;       // 当前预设(分钟)
private timerCtrl: TextTimerController = new TextTimerController();

计时器状态机包含 4 个状态,转换逻辑如下:

  • idle → running:点击"开始",调用 timerCtrl.start()
  • running → paused:点击"暂停",调用 timerCtrl.pause()
  • paused → running:点击"继续",调用 timerCtrl.start()
  • running → finished:计时结束或手动点击"完成",调用 timerCtrl.reset()
  • finished → idle:点击中心按钮重新开始,选择新预设并重置

3.2 预设选择

预设选择器使用 ForEach 渲染三个时长选项,通过视觉高亮标识当前选中项:

Row() {
  Text('时长')
    .fontSize(14).fontColor('#1a1a2e')
    .fontWeight(FontWeight.Medium).margin({ right: 12 })

  ForEach(this.presets, (min: number) => {
    Row() {
      Text(min.toString())
        .fontSize(14)
        .fontColor(
          this.currentPreset === min && this.timerStatus === 0
            ? '#FFFFFF' : '#1a1a2e'
        )
        .fontWeight(
          this.currentPreset === min && this.timerStatus === 0
            ? FontWeight.Bold : FontWeight.Normal
        )
      Text(' min')
        .fontSize(11)
        .fontColor(
          this.currentPreset === min && this.timerStatus === 0
            ? '#FFFFFFCC' : '#9999AA'
        )
    }
    .padding({ top: 8, bottom: 8, left: 16, right: 16 })
    .borderRadius(20)
    .backgroundColor(
      this.currentPreset === min && this.timerStatus === 0
        ? '#1677FF' : '#F2F3F5'
    )
    .margin({ left: 8 })
    .onClick(() => {
      if (this.timerStatus === 0 || this.timerStatus === 3) {
        this.selectPreset(min);
      }
    })
  }, (min: number) => min.toString())
}
.width('100%')
.padding({ left: Spacing.LG, right: Spacing.LG, top: 10, bottom: 16 })

关键设计细节:

  1. 预设切换仅在 空闲(idle)或已完成(finished) 状态下允许——计时器运行中或暂停时不允许切换时长,避免意外中断
  2. 选中项使用蓝色填充(#1677FF)+ 白色文字,未选中项使用灰色背景(#F2F3F5)+ 深色文字
  3. 通过 selectPreset() 方法统一处理选中逻辑,重建 TextTimerController 并重置状态
selectPreset(minutes: number): void {
  this.currentPreset = minutes;
  this.totalSeconds = minutes * 60;
  this.timerStatus = 0;
  this.timerKey++;
  this.timerCtrl = new TextTimerController();
}

每次切换预设时,我们创建一个新的 TextTimerController 并递增 timerKey。这样做可以确保 TextTimer 组件在新参数下完全重建,避免旧控制器状态残留。

3.3 控制按钮

控制区域由三个圆形按钮组成:左侧重置、中央开始/暂停、右侧完成。

Row() {
  // 重置按钮
  Button() {
    Text('重置')
      .fontSize(15).fontColor('#666677')
      .fontWeight(FontWeight.Medium)
  }
  .width(64).height(64).borderRadius(32)
  .backgroundColor('#F2F3F5')
  .onClick(() => { this.selectPreset(this.currentPreset); })

  // 开始 / 暂停(中央主按钮)
  Button() {
    Text(
      this.timerStatus === 1 ? '暂停' :
      this.timerStatus === 2 ? '继续' :
      '开始'
    )
      .fontSize(18).fontColor('#FFFFFF')
      .fontWeight(FontWeight.Bold)
  }
  .width(100).height(100).borderRadius(50)
  .backgroundColor(this.timerStatus === 1 ? '#FF9800' : '#1677FF')
  .margin({ left: 40, right: 40 })
  .onClick(() => {
    if (this.timerStatus === 0) {
      this.timerStatus = 1;
      this.timerCtrl.start();
    } else if (this.timerStatus === 1) {
      this.timerStatus = 2;
      this.timerCtrl.pause();
    } else if (this.timerStatus === 2) {
      this.timerStatus = 1;
      this.timerCtrl.start();
    } else if (this.timerStatus === 3) {
      this.selectPreset(this.currentPreset);
    }
  })

  // 完成按钮
  Button() {
    Text('完成')
      .fontSize(15).fontColor('#666677')
      .fontWeight(FontWeight.Medium)
  }
  .width(64).height(64).borderRadius(32)
  .backgroundColor('#F2F3F5')
  .onClick(() => {
    this.timerCtrl.reset();
    this.timerStatus = 3;
    this.sessionCount += 1;
  })
}

交互逻辑:

  • 开始:从空闲状态(timerStatus = 0)启动,调用 timerCtrl.start(),主按钮变橙色并显示"暂停"
  • 暂停:运行中(timerStatus = 1)点击,调用 timerCtrl.pause(),主按钮变蓝色并显示"继续"
  • 继续:暂停后(timerStatus = 2)点击,调用 timerCtrl.start() 从剩余时间继续
  • 重置:左侧按钮,重建控制器回到初始时长
  • 完成:右侧按钮,提前结束计时并计数

主按钮使用了不同的背景色来区分运行和暂停状态——运行中为橙色(#FF9800),即"暂停"的暗示色;空闲和暂停后重新开始时为蓝色(#1677FF),即"行动"的暗示色。这种颜色变化让用户一眼就能理解当前状态。

3.4 TextTimer 集成

核心倒计时显示区域使用真实的 TextTimer 组件:

TextTimer({
  isCountDown: true,
  count: this.totalSeconds * 1000,   // 转换为毫秒
  controller: this.timerCtrl
})
  .format(this.showHours ? 'HH:mm:ss' : 'mm:ss')
  .fontSize(56)
  .fontColor(this.timerStatus === 3 ? '#52C41A' : '#1a1a2e')
  .fontWeight(FontWeight.Bold)
  .fontFamily('monospace')

这里有几个值得注意的实现细节:

  1. 毫秒转换:TextTimer 的 count 参数以毫秒为单位,而我们的 totalSeconds 以秒为单位,需要乘以 1000
  2. 动态 format:通过 showHours 状态动态切换格式,true 时显示 HH:mm:ssfalse 时显示 mm:ss
  3. 等宽字体:使用 monospace 字体系列避免数字跳动导致布局抖动。不同的数字宽度可能略有不同(如"1"比"0"窄),等宽字体确保每个数字占据相同宽度
  4. 完成状态着色:计时结束时文字变为绿色(#52C41A),给用户正向反馈

3.5 格式切换

格式切换使用 Toggle 开关组件,让用户在 mm:ss 和 HH:mm:ss 之间选择:

Row() {
  Text('显示格式')
    .fontSize(15).fontColor('#1a1a2e')
  Blank()
  Text(this.showHours ? 'HH:mm:ss' : 'mm:ss')
    .fontSize(13).fontColor('#1677FF')
    .fontWeight(FontWeight.Medium)
    .margin({ right: 10 })
  Toggle({ type: ToggleType.Switch, isOn: this.showHours })
    .onChange((on: boolean) => {
      if (this.timerStatus === 0 || this.timerStatus === 3) {
        this.showHours = on;
        this.timerKey++;
      }
    })
}

格式切换同样只在空闲或已完成状态下允许——这是为了避免计时过程中格式变化带来的视觉困惑。
在这里插入图片描述

四、进阶话题

4.1 TextTimerController 的"延迟初始化"模式

在 ArkUI 开发中,某些组件需要在 aboutToAppear 或用户交互后重新创建。TextTimerController 是一个典型的例子:当用户切换预设时长时,旧的控制器可能持有上次计时的残留状态,直接 reset()start() 有时行为不如预期。

最佳实践是"销毁并重建"——创建一个新的 TextTimerController 实例分配给 this.timerCtrl

selectPreset(minutes: number): void {
  this.totalSeconds = minutes * 60;
  this.timerStatus = 0;
  this.timerCtrl = new TextTimerController();
}

配合 @State 计数器的递增,可以强制 ArkUI 框架触发 TextTimer 组件的完全重建,确保新参数和新控制器完美匹配。

4.2 正计时模式

虽然本文的 Demo 聚焦于倒计时(番茄钟),TextTimer 同样支持正计时场景——例如运动计时、通勤计时等。只需将 isCountDown 设置为 false

TextTimer({
  isCountDown: false,
  count: 0,  // 正计时从 0 开始
  controller: this.timerCtrl
})
  .format('HH:mm:ss')

正计时会从 00:00:00 开始递增,非常适合需要记录耗时的场景。

4.3 计时器生命周期管理

当页面销毁(aboutToDisappear)时,运行中的计时器不会自动停止。如果你的应用需要在后台持续计时,这会很有用;但如果需要在离开页面时暂停,应该在 aboutToDisappear 中调用 timerCtrl.pause()

五、完整 Demo 交互流程

梳理一下专注计时器的完整交互流程:

  1. 进入页面:显示 25 分钟计时器,状态为"就绪",计时器圆环显示 25:00
  2. 选择预设:点击"45 min"按钮,计时器显示变为 45:00,预设高亮切换
  3. 开始专注:点击蓝色"开始"按钮,计时器开始倒计时,主按钮变为橙色"暂停",状态文字变为蓝色"专注中 · 保持专注"
  4. 中途暂停:点击橙色"暂停"按钮,倒计时停止,按钮变为蓝色"继续",状态文字变为"已暂停 · 继续努力"
  5. 恢复计时:点击蓝色"继续"按钮,计时器从剩余时间继续倒数
  6. 完成计时:倒计时归零或点击"完成"按钮,计时器变绿,状态显示"太棒了!一个番茄完成",今日完成计数 +1
  7. 格式切换:打开"显示格式"开关,计时器显示从 mm:ss 切换到 HH:mm:ss(仅在空闲/完成状态下可切换)
  8. 开始新计时:选择新的预设或点击中心按钮重新开始

六、设计要点总结

回顾整个计时器 Demo 的设计,有几个值得提炼的要点:

状态驱动的 UI:计时器的 4 个状态(idle / running / paused / finished)决定了所有 UI 元素的呈现——按钮文字、颜色、操作逻辑全部由状态驱动。这种模式让代码逻辑清晰可预测。

控制器的生命周期:TextTimerController 与 TextTimer 组件是紧密耦合的。当 TextTimer 的参数(count、format)发生变化时,最简单可靠的做法是重建控制器,而非在旧控制器上"修复"状态。

操作安全性:预设切换和格式切换仅在 idle 或 finished 状态下允许。这种"操作守卫"避免了计时器运行中修改参数导致的行为混乱,是用户体验中的细节但很重要。

等宽字体的价值:在计时器场景中,等宽字体不仅关乎美观,更关乎可用性——非等宽字体会在数字变化时导致文本区域轻微抖动,分散用户注意力。

七、总结

TextTimer 是 ArkUI 中一个"小而精"的组件。它的 API 只有三个方法(start / pause / reset)和一个属性(format),但配合状态管理和控制器生命周期设计,可以构建出完整且专业的计时器应用。

本文通过"专注计时器"这个完整 Demo,覆盖了:

  1. TextTimer 的构造函数、控制器和 format 属性
  2. 4 状态机驱动的 UI 交互设计
  3. 控制器的"销毁并重建"模式
  4. 预设选择、格式切换的操作守卫设计
  5. 正计时模式的扩展方向

TextTimer 和 TextClock 是 ArkUI 时间相关组件的"双子星"。TextClock 告诉你"现在几点",TextTimer 告诉你"过了多久"或"还剩多久"。将两者灵活运用,就能覆盖应用中所有时间显示场景——从时钟展示到番茄钟,从验证码倒计时到运动计时器,应有尽有。


Logo

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

更多推荐