在鸿蒙应用开发入门阶段,番茄钟工具因功能聚焦、交互逻辑清晰,成为理解组件化开发与状态管理的优质实践项目。本文以一款可直接运行的 ArkTS 番茄钟应用为核心,从状态定义、功能逻辑到界面渲染,逐模块拆解代码细节,确保每部分内容均与实际代码对应,既符合基础开发知识体系,也为开发者提供可复用的完整实现方案。

应用核心定位与代码架构

这款番茄钟应用以 “轻量化时间管理” 为目标,支持多档位专注时长选择(5/10/15/30/45/60 分钟)、实时计时反馈、动态视觉提示及灵活状态控制(开始 / 暂停 / 重置 / 归零)。代码整体围绕TomatoTimer入口组件构建,采用 “状态 - 逻辑 - 视图” 三层结构:

  • 状态层:通过@State装饰器管理计时状态、剩余时间、界面显示控制等核心变量;
  • 逻辑层:将计时控制、动画管理、时间格式化等逻辑封装为独立方法,降低冗余;
  • 视图层:通过 ArkTS 内置组件(Column/Stack/Text/Button等)搭建响应式界面,实现状态与视图的自动联动。

代码核心模块深度解析

1. 状态变量:应用运行的 “数据基石”

代码开篇通过@State装饰器定义 11 个核心状态变量,这些变量直接决定应用的运行状态与界面表现,每个变量的初始值与作用均经过严谨设计,具体说明如下:

状态变量名 类型 初始值 核心作用
currentTime string getFormattedTime()返回值 存储并显示当前系统时间(格式:时:分: 秒)
focusMinutes number 5 存储当前选择的专注时长,默认 5 分钟
remainingSeconds number 5*60 存储倒计时剩余秒数,随计时动态递减
isRunning boolean false 标记计时是否运行,控制 “开始 / 暂停” 按钮文本切换
timerId number | null null 存储计时定时器 ID,用于暂停时释放资源
showCompletion boolean false 标记计时是否结束,控制 “完成提示” 的显示 / 隐藏
progressValue number 0 存储进度值(预留用于后续进度条扩展)
showTimeOptions boolean true 控制 “时间设置按钮组” 显示(计时时隐藏,避免误操作)
isZeroState boolean false 标记是否处于 “归零” 状态,区分正常计时与归零操作
showAllButtons boolean false 控制 “归零”“重置” 按钮显示(仅计时 / 暂停时可见)
dotIndex number 0 控制动态圆点闪烁索引,实现 “轮流亮灭” 效果
dotTimerId number | null null 存储圆点动画定时器 ID,用于同步停止动画

此外,代码通过private readonly定义两个固定颜色常量,确保视觉风格统一:

  • ACTIVE_DOT_COLOR: Color = 0x4DFF9E:圆点 “点亮” 状态(亮绿色);
  • INACTIVE_DOT_COLOR: Color = 0x4DFF9E30:圆点 “熄灭” 状态(半透明绿色)。

2. 工具方法:功能逻辑的 “封装单元”

为提升代码可读性与复用性,代码将重复逻辑封装为独立方法,这些方法是连接 “状态变量” 与 “用户操作” 的关键桥梁:

(1)时间格式化方法
  • getFormattedTime():获取并格式化当前系统时间。通过Date对象获取时、分、秒,使用padStart(2, '0')确保每位为两位数(如 “7:2:4” 转为 “07:02:04”),最终返回格式化字符串,用于顶部时间显示。
    getFormattedTime(): string {
      const now = new Date()
      return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
    }
  • formatTime(seconds: number):将秒数转换为 “分:秒” 格式。通过Math.floor(seconds/60)计算分钟数,seconds%60计算剩余秒数,同样用padStart补零(如 250 秒转为 “04:10”),用于核心区倒计时显示。
(2)动态圆点动画方法
  • startDotAnimation():启动圆点闪烁动画。先判断dotTimerId是否存在(避免重复创建定时器),再通过setInterval每 300 毫秒更新dotIndex(dotIndex + 1) % 3实现 0→1→2→0 循环),使三个圆点轮流 “点亮”。
  • stopDotAnimation():停止圆点动画。通过clearInterval清除dotTimerId对应的定时器,同时重置dotIndex为 0,确保下次启动时从第一个圆点开始。
  • getDotColor(index: number):动态返回圆点颜色。未计时(!isRunning)时返回透明色;计时中,当前索引(index)与dotIndex一致则返回亮绿色,否则返回半透明绿色,实现 “闪烁” 视觉反馈。
(3)生命周期与时间更新

代码仅使用aboutToAppear()生命周期钩子,在组件即将显示时启动秒级定时器,调用updateTime()方法实时更新currentTime,确保顶部时间精准同步系统时间:

aboutToAppear() {
  setInterval(() => {
    this.updateTime()
  }, 1000)
}

updateTime() {
  this.currentTime = this.getFormattedTime()
}

核心功能逻辑实现

1. 计时启停控制:toggleTimer()方法

该方法是应用的核心交互逻辑,通过isRunning状态判断操作类型,同步管理定时器与界面状态:

(1)暂停计时(isRunning为 true)
  • 释放计时资源:若timerId不为 null,通过clearInterval(timerId)停止倒计时,避免资源泄漏;
  • 同步停止动画:调用stopDotAnimation(),确保动画与计时状态一致;
  • 更新界面状态:isRunning设为 false(标记暂停),showAllButtons设为 true(显示 “归零”“重置” 按钮)。
(2)开始计时(isRunning为 false)
  • 重置结束状态:若之前已计时结束(showCompletion为 true),则重置showCompletion为 false,恢复remainingSecondsfocusMinutes*60progressValue设为 0;
  • 处理归零状态:若处于归零状态(isZeroState为 true),同样恢复remainingSecondsprogressValue,并重置isZeroState为 false;
  • 控制界面显示:showTimeOptions设为 false(隐藏时间设置按钮,避免误改时长),isRunning设为 true,showAllButtons设为 true;
  • 启动计时与动画:调用startDotAnimation()开启动画,通过setInterval创建计时定时器,每 1 秒执行一次:
    • remainingSeconds>0:remainingSeconds减 1,同步更新progressValue1 - 剩余秒数/总秒数,确保进度值随时间递增);
    • remainingSeconds=0:清除计时定时器,重置timerIdisRunning设为 false,showCompletion设为 true(显示完成提示),停止动画。

2. 计时重置:resetTimer()方法

点击 “重置” 按钮时,将应用恢复至初始状态:

  • 清除所有定时器:同时清除计时定时器(timerId)与动画定时器(dotTimerId);
  • 重置核心状态:isRunning设为 false,showCompletion设为 false,remainingSeconds恢复为focusMinutes*60progressValue设为 0;
  • 恢复初始显示:showTimeOptions设为 true(显示时间设置按钮),isZeroState设为 false,showAllButtons设为 false。

3. 计时归零:zeroTimer()方法

与 “重置” 不同,“归零” 直接将倒计时设为 0,逻辑如下:

  • 清除定时器:同resetTimer(),确保计时与动画完全停止;
  • 更新状态变量:remainingSeconds设为 0(倒计时显示 “00:00”),progressValue设为 1,isZeroState设为 true;
  • 控制界面显示:showTimeOptions设为 true(允许重新选择时长),showAllButtons设为 false。

4. 专注时长设置:setFocusTime(minutes: number)方法

点击时间设置按钮时,修改专注时长并同步倒计时:

  • 更新时长变量:focusMinutes设为用户选择的分钟数(如 10、15 等);
  • 同步倒计时:若处于归零状态(isZeroState为 true)或未计时(!isRunning),则remainingSeconds设为minutes*60progressValue设为 0;
  • 保留当前计时:若处于计时中(isRunning为 true),仅更新focusMinutes,不改变remainingSeconds,避免计时中断。

界面实现:build方法中的响应式布局

build方法通过ColumnStack嵌套,将界面分为 5 个核心区域,所有组件属性均与状态变量动态绑定,实现 “状态变、视图变” 的效果:

1. 顶部时间显示区

Text组件展示currentTime,通过属性配置确保视觉醒目:

  • 字体样式:fontSize(28)fontWeight(FontWeight.Bold)
  • 间距控制:margin({ top: 30, bottom: 40 })
  • 视觉效果:fontColor("#00FFFF")(青色)、textShadow({ radius: 10, color: "#FF00FF", offsetX: 0, offsetY: 0 })(品红色阴影)。

2. 核心计时区(Stack 嵌套)

作为界面视觉中心,采用 “背景 + 内容” 叠加布局:

  • 背景层Column组件width('90%')height(320)backgroundColor("#0F1923")(深灰),borderRadius(16)(圆角),border为 2px 青色虚线(BorderStyle.Dashed),opacity(0.9)
  • 内容层:嵌套Column包含两部分:
    • 倒计时文本:Text(formatTime(remainingSeconds))fontSize(48)fontWeight(FontWeight.Bold)fontColor("#00FFAA")(亮绿),添加蓝色文字阴影;
    • 动态圆点:Row包裹三个Circle组件(通过ForEach([0,1,2])创建),width(14)height(14)fill绑定getDotColor(i)margin(6)opacity绑定isRunning

3. 计时完成提示区

通过if (showCompletion)条件渲染,仅计时结束时显示:

  • 外层Columnwidth('100%')padding(20)backgroundColor("#000000")(黑色),border({ width: 3, color: "#FF00FF" })(品红边框),opacity(0.95)
  • 提示文本:“TIME UP!”(32 号加粗红色)、“Take a break now”(18 号浅蓝色),margin({ top: 10 })

4. 时间设置按钮区

通过if (showTimeOptions)条件渲染,仅未计时 / 重置时显示:

  • 滚动容器:Scroll组件设为水平滚动(ScrollDirection.Horizontal),适配小屏幕;
  • 按钮组:Row通过ForEach([5,10,15,30,45,60])创建 6 个圆形按钮,每个按钮为Column
    • 文本样式:“分钟数”(24 号加粗)、“分钟”(12 号),选中状态(focusMinutes === minutes)为深灰背景(#333333)+ 亮青文字(#00FFCC),未选中为浅灰背景(#111111)+ 灰色文字(#888888);
    • 交互与尺寸:width(70)height(70)borderRadius(35)(圆形),onClick绑定setFocusTime(minutes)margin(8)

5. 操作按钮区

Row容器包裹三个按钮,通过showAllButtons控制显示:

  • 归零按钮:仅showAllButtons为 true 时显示,width(100)height(50),紫色背景(#770077),品红阴影,onClick绑定zeroTimer()
  • 开始 / 暂停按钮:始终显示,width(100)height(50),文本随isRunning切换(“暂停”/“开始”),背景色对应切换(粉红#FF3366/ 绿色#33CC99),阴影颜色同步变化,margin({ left: 10, right: 10 })onClick绑定toggleTimer()
  • 重置按钮:仅showAllButtons为 true 时显示,深蓝背景(#555577),蓝色阴影,onClick绑定resetTimer()
  • 布局控制:Row设为width('100%')justifyContent(FlexAlign.Center)(按钮组居中)。

6. 整体布局

最外层Column设为width('100%')height('100%')padding(20),深色背景(#0A0E17),通过justifyContent(FlexAlign.Center)alignItems(HorizontalAlign.Center)使所有子组件居中,适配不同设备尺寸。

开发问题与解决思路

在代码实现过程中,针对初学者常见问题,总结以下解决方法:

  1. 定时器重复创建:多次点击 “开始” 导致计时加速,通过判断timerId是否为 null,确保每次仅创建一个定时器,暂停时清除并重置timerId
  2. 动画与计时不同步:暂停后圆点仍闪烁,为动画单独设置dotTimerId,在暂停 / 重置 / 归零时同步停止动画;
  3. 小屏幕按钮遮挡:时间设置按钮显示不全,通过Scroll组件实现水平滚动,适配小屏幕设备。

完整代码附录

为方便开发者直接复用与调试,以下为该番茄钟应用的完整 ArkTS 代码,可直接在鸿蒙 DevEco Studio 中创建项目并替换对应文件内容:

@Entry
@Component
struct TomatoTimer {
  @State currentTime: string = this.getFormattedTime()
  @State focusMinutes: number = 5 // 默认改为5分钟
  @State remainingSeconds: number = 5 * 60 // 默认改为5分钟
  @State isRunning: boolean = false
  @State timerId: number | null = null
  @State showCompletion: boolean = false
  @State progressValue: number = 0
  @State showTimeOptions: boolean = true
  @State isZeroState: boolean = false
  @State showAllButtons: boolean = false
  @State dotIndex: number = 0 // 当前闪烁的圆点索引
  @State dotTimerId: number | null = null // 圆点动画计时器ID

  // 圆点颜色定义
  private readonly ACTIVE_DOT_COLOR: Color = 0x4DFF9E  // 亮绿色
  private readonly INACTIVE_DOT_COLOR: Color = 0x4DFF9E30  // 半透明绿色

  // 获取格式化时间
  getFormattedTime(): string {
    const now = new Date()
    return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
  }

  // 更新当前时间
  updateTime() {
    this.currentTime = this.getFormattedTime()
  }

  // 开始圆点动画
  startDotAnimation() {
    if (this.dotTimerId !== null) {
      clearInterval(this.dotTimerId)
    }
    this.dotTimerId = setInterval(() => {
      this.dotIndex = (this.dotIndex + 1) % 3
    }, 300) // 每300毫秒切换一次圆点
  }

  // 停止圆点动画
  stopDotAnimation() {
    if (this.dotTimerId !== null) {
      clearInterval(this.dotTimerId)
      this.dotTimerId = null
    }
    this.dotIndex = 0
  }

  // 获取圆点颜色
  getDotColor(index: number): Color {
    if (!this.isRunning) {
      return Color.Transparent // 非计时时间不显示颜色
    }
    return index === this.dotIndex ? this.ACTIVE_DOT_COLOR : this.INACTIVE_DOT_COLOR
  }

  // 开始/暂停计时器
  toggleTimer() {
    if (this.isRunning) {
      // 暂停计时器
      if (this.timerId !== null) {
        clearInterval(this.timerId)
        this.timerId = null
      }
      this.stopDotAnimation()
      this.isRunning = false
      this.showAllButtons = true
    } else {
      // 开始计时器
      if (this.showCompletion) {
        this.showCompletion = false
        this.remainingSeconds = this.focusMinutes * 60
        this.progressValue = 0
      }

      if (this.isZeroState) {
        this.remainingSeconds = this.focusMinutes * 60
        this.progressValue = 0
        this.isZeroState = false
      }

      this.showTimeOptions = false
      this.isRunning = true
      this.showAllButtons = true
      this.startDotAnimation()

      this.timerId = setInterval(() => {
        if (this.remainingSeconds > 0) {
          this.remainingSeconds--
          this.progressValue = 1 - (this.remainingSeconds / (this.focusMinutes * 60))
        } else {
          clearInterval(this.timerId)
          this.timerId = null
          this.isRunning = false
          this.showCompletion = true
          this.showAllButtons = true
          this.stopDotAnimation()
        }
      }, 1000)
    }
  }

  // 重置计时器
  resetTimer() {
    if (this.timerId !== null) {
      clearInterval(this.timerId)
      this.timerId = null
    }
    this.stopDotAnimation()
    this.isRunning = false
    this.showCompletion = false
    this.remainingSeconds = this.focusMinutes * 60
    this.progressValue = 0
    this.showTimeOptions = true
    this.isZeroState = false
    this.showAllButtons = false
  }

  // 归零功能
  zeroTimer() {
    if (this.timerId !== null) {
      clearInterval(this.timerId)
      this.timerId = null
    }
    this.stopDotAnimation()
    this.isRunning = false
    this.showCompletion = false
    this.remainingSeconds = 0
    this.progressValue = 1
    this.showTimeOptions = true
    this.isZeroState = true
    this.showAllButtons = false
  }

  // 设置专注时间
  setFocusTime(minutes: number) {
    this.focusMinutes = minutes
    if (this.isZeroState || !this.isRunning) {
      this.remainingSeconds = minutes * 60
      this.progressValue = 0
    }
  }

  // 格式化倒计时显示
  formatTime(seconds: number): string {
    const mins = Math.floor(seconds / 60)
    const secs = seconds % 60
    return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
  }

  aboutToAppear() {
    setInterval(() => {
      this.updateTime()
    }, 1000)
  }

  build() {
    Column() {
      // 顶部当前时间显示
      Text(this.currentTime)
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 30, bottom: 40 })
        .fontColor("#00FFFF")
        .textShadow({ radius: 10, color: "#FF00FF", offsetX: 0, offsetY: 0 })

      // 番茄钟主体
      Stack() {
        Column() {}
        .width('90%')
        .height(320)
        .backgroundColor("#0F1923")
        .borderRadius(16)
        .border({
          width: 2,
          color: "#00FFFF",
          style: BorderStyle.Dashed
        })
        .padding(25)
        .opacity(0.9)

        Column() {
          Text(this.formatTime(this.remainingSeconds))
            .fontSize(48)
            .fontWeight(FontWeight.Bold)
            .fontColor("#00FFAA")
            .textShadow({ radius: 8, color: "#0000FF", offsetX: 0, offsetY: 0 })

          // 修改后的圆点显示部分
          Row() {
            ForEach([0, 1, 2], (i: number) => {
              Circle()
                .width(14)
                .height(14)
                .fill(this.getDotColor(i))
                .margin(6)
                .opacity(this.isRunning ? 1 : 0.3)
            })
          }
          .margin({ top: 10 })
          .height(30)
        }
      }
      .margin({ bottom: 40 })

      // 完成提示
      if (this.showCompletion) {
        Column() {
          Text('TIME UP!')
            .fontSize(32)
            .fontWeight(FontWeight.Bold)
            .fontColor("#FF5555")
            .textShadow({ radius: 5, color: Color.White, offsetX: 0, offsetY: 0 })
          Text('Take a break now')
            .fontSize(18)
            .fontColor("#AAAAFF")
            .margin({ top: 10 })
        }
        .width('100%')
        .padding(20)
        .backgroundColor("#000000")
        .border({ width: 3, color: "#FF00FF" })
        .margin({ bottom: 30 })
        .opacity(0.95)
      }

      // 时间设置按钮
      if (this.showTimeOptions) {
        Scroll() {
          Row() {
            ForEach([5, 10, 15, 30, 45, 60], (minutes: number) => {
              Column() {
                Text(`${minutes}`)
                  .fontSize(24)
                  .fontWeight(FontWeight.Bold)
                  .fontColor(this.focusMinutes === minutes ? "#00FFCC" : "#888888")
                Text('分钟')
                  .fontSize(12)
                  .fontColor(this.focusMinutes === minutes ? "#FFFFFF" : "#666666")
              }
              .width(70)
              .height(70)
              .borderRadius(35)
              .backgroundColor(this.focusMinutes === minutes ? "#333333" : "#111111")
              .justifyContent(FlexAlign.Center)
              .onClick(() => this.setFocusTime(minutes))
              .margin(8)
            })
          }
          .padding(10)
        }
        .scrollable(ScrollDirection.Horizontal)
        .margin({ bottom: 30 })
      }

      // 操作按钮区域
      Row() {
        // 左侧按钮 - 归零
        if (this.showAllButtons) {
          Button('归零', { type: ButtonType.Normal })
            .width(100)
            .height(50)
            .backgroundColor("#770077")
            .fontColor(Color.White)
            .fontSize(18)
            .shadow({ radius: 5, color: "#FF00FF", offsetX: 0, offsetY: 3 })
            .onClick(() => this.zeroTimer())
        } else {
          Blank()
            .width(100)
            .height(50)
        }

        // 中间按钮 - 开始/暂停
        Button(this.isRunning ? '暂停' : '开始', { type: ButtonType.Normal })
          .width(100)
          .height(50)
          .backgroundColor(this.isRunning ? "#FF3366" : "#33CC99")
          .fontColor(Color.White)
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .shadow({ radius: 8, color: this.isRunning ? "#FF0000" : "#00FF00", offsetX: 0, offsetY: 5 })
          .onClick(() => this.toggleTimer())
          .margin({ left: 10, right: 10 })

        // 右侧按钮 - 重置
        if (this.showAllButtons) {
          Button('重置', { type: ButtonType.Normal })
            .width(100)
            .height(50)
            .backgroundColor("#555577")
            .fontColor(Color.White)
            .fontSize(18)
            .shadow({ radius: 5, color: "#0000FF", offsetX: 0, offsetY: 3 })
            .onClick(() => this.resetTimer())
        } else {
          Blank()
            .width(100)
            .height(50)
        }
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
      .margin({ top: 20 })
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor("#0A0E17")
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
  }
}

该代码已通过鸿蒙 DevEco Studio 编译与调试,可正常实现所有核心功能。开发者可基于此代码进行二次扩展,如添加休息计时、专注统计、自定义提示音等功能,进一步提升应用实用性。

本文在撰写过程中,参考了 CSDN 博主Rene_ZHK的优质文章《鸿蒙ArkTS打造高效番茄钟》,其清晰的思路和实用的分享为本文提供了重要启发。在此向原作者表示诚挚的感谢!

原文链接:鸿蒙ArkTS打造高效番茄钟_鸿蒙番茄钟案例-CSDN博客

Logo

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

更多推荐