HarmonyOS NEXT 实战:打造功能完整的秒表应用

API版本:6.1.1(24)
目标设备:Phone
开发工具:DevEco Studio


一、前言:为什么选择秒表作为练手项目?

秒表(Stopwatch)是移动开发中最经典的入门项目之一。看似简单,实则涵盖了定时器管理、状态控制、列表渲染、数据处理等多个核心知识点。对于刚接触 HarmonyOS NEXT 的开发者来说,这是一个绝佳的进阶练手项目。

通过这个项目,你将学到:

知识点 应用场景
setInterval 定时器 实现毫秒级计时
状态变量 @State 控制计时器的启动/暂停/重置
条件渲染 if 动态显示运行状态
ForEach 列表渲染 展示计次记录
数据接口定义 封装圈数、单圈时间、总时间
算法逻辑 计算最快/最慢圈数

本文将从项目创建开始,逐步拆解每个功能模块的实现,最终完成一个功能完整、界面美观的秒表应用。


二、项目创建与配置

2.1 新建项目

打开 DevEco Studio,选择 File → New → Create Project

  1. 选择 Application → Empty Ability 模板
  2. 填写项目信息:
    • Project name:MyApplication
    • Bundle name:com.example.myapplication
    • Compile SDK:API 6.1.1(24)
    • Model:Stage 模型

点击 Finish,等待项目初始化完成。

2.2 项目结构一览

MyApplication/
├── AppScope/
│   ├── app.json5                    # 应用全局配置
│   └── resources/base/
│       ├── element/string.json      # 应用名称
│       └── media/                   # 应用图标
├── entry/
│   ├── src/main/
│   │   ├── ets/
│   │   │   ├── entryability/
│   │   │   │   └── EntryAbility.ets  # 应用入口
│   │   │   └── pages/
│   │   │       └── Index.ets         # 主页面 ⭐
│   │   ├── resources/
│   │   │   ├── base/
│   │   │   │   ├── element/          # 字符串、颜色
│   │   │   │   ├── media/            # 图片资源
│   │   │   │   └── profile/
│   │   │   │       └── main_pages.json
│   │   │   └── rawfile/
│   │   └── module.json5              # 模块配置
│   └── build-profile.json5
└── build-profile.json5               # 构建配置

三、功能需求分析

3.1 核心功能

一个完整的秒表应用需要具备以下功能:

功能 描述
开始计时 点击开始按钮,时间以 20ms 间隔更新
暂停计时 暂停后保留已计时间,可继续计时
重置计时 清零时间,清除所有计次记录
计次(Lap) 记录当前单圈时间和累计总时间
最快/最慢标记 在计次列表中标记最快圈(🏆)和最慢圈(🐢)

3.2 UI 设计

  • 时间显示区:圆形表盘设计,居中显示 MM:SS.cs 格式时间
  • 控制按钮区:三个按钮横排(计次、开始/停止、重置)
  • 计次列表区:滚动列表展示所有计次记录
  • 状态提示:运行中显示红色"计时中",暂停时显示橙色"已暂停"

3.3 时间格式说明

采用 分:秒.厘秒 格式:

MM:SS.cs
│  │  │
│  │  └── 厘秒 (百分之一秒,00-99)
│  └───── 秒 (00-59)
└──────── 分钟 (00-99)

例如:02:34.56 表示 2 分 34 秒 56 厘秒。


四、数据模型设计

4.1 计次记录接口

每次点击"计次",需要记录以下信息:

interface LapRecord {
  lapNumber: number    // 圈数编号(1, 2, 3...)
  lapTime: string      // 单圈时间(如 "00:12.34")
  totalTime: string    // 累计总时间(如 "01:23.45")
}

4.2 页面状态变量

@Entry
@Component
struct Index {
  // UI 显示状态
  @State displayTime: string = '00:00.00'      // 当前显示时间
  @State isRunning: boolean = false            // 是否正在计时
  @State lapCount: number = 0                  // 计次次数
  @State laps: LapRecord[] = []                // 计次记录列表
  
  // 最快/最慢圈索引
  @State bestLapIndex: number = -1
  @State worstLapIndex: number = -1

  // 计时器相关(非响应式)
  private startTime: number = 0                // 本次启动时间戳
  private elapsedBeforePause: number = 0       // 暂停前累计时间
  private lastLapTime: number = 0              // 上次计次时的总时间
  private timerId: number = -1                 // 定时器 ID
}

设计要点:

  • @State 装饰的变量变化会触发 UI 刷新
  • 计时器内部变量不需要响应式,用 private 声明
  • 暂停后再次启动,需要累加之前的时间,所以用 elapsedBeforePause 保存

五、核心逻辑实现

5.1 时间格式化

将毫秒转换为 MM:SS.cs 格式:

formatTime(ms: number): string {
  const totalCs = Math.floor(ms / 10)           // 转为厘秒
  const minutes = Math.floor(totalCs / 6000)    // 分钟
  const seconds = Math.floor((totalCs % 6000) / 100)  // 秒
  const centiseconds = totalCs % 100            // 厘秒
  
  return `${this.pad(minutes)}:${this.pad(seconds)}.${this.pad(centiseconds)}`
}

pad(n: number): string {
  return n < 10 ? '0' + n : '' + n              // 补零
}

计算示例:

毫秒 (ms) 厘秒 (cs) 分钟 厘秒 输出
0 0 0 0 0 00:00.00
1234 123 0 1 23 00:01.23
61789 6178 1 1 78 01:01.78
3600000 360000 60 0 0 60:00.00

5.2 开始计时

start(): void {
  if (this.isRunning) return                    // 防止重复启动
  
  this.startTime = Date.now()                   // 记录启动时间
  this.isRunning = true
  this.lastLapTime = this.elapsedBeforePause    // 记录上次计次时间点

  // 每 20ms 更新一次显示
  this.timerId = setInterval(() => {
    const now = Date.now()
    const elapsed = this.elapsedBeforePause + (now - this.startTime)
    this.displayTime = this.formatTime(elapsed)
  }, 20)
}

为什么要记录 elapsedBeforePause

如果用户点击"暂停"后再点击"开始",我们不应该从零开始计时,而是继续之前的累计时间。

时间线:
├── 开始 ──────────┤ 暂停 ├── 继续 ────────┤
     5000ms                    3000ms
      
总时间 = 5000 + 3000 = 8000ms

5.3 暂停计时

stop(): void {
  if (!this.isRunning) return
  
  clearInterval(this.timerId)                   // 清除定时器
  this.elapsedBeforePause += Date.now() - this.startTime  // 累加时间
  this.isRunning = false
}

5.4 重置计时

reset(): void {
  clearInterval(this.timerId)                   // 清除定时器
  
  // 重置所有状态
  this.isRunning = false
  this.displayTime = '00:00.00'
  this.elapsedBeforePause = 0
  this.lastLapTime = 0
  this.lapCount = 0
  this.laps = []
  this.bestLapIndex = -1
  this.worstLapIndex = -1
}

⚠️ 注意:必须在组件销毁时清除定时器,否则会造成内存泄漏:

aboutToDisappear(): void {
  if (this.timerId !== -1) {
    clearInterval(this.timerId)
  }
}

5.5 计次功能

lap(): void {
  if (!this.isRunning) return                   // 必须在计时中才能计次
  
  const now = Date.now()
  const totalElapsed = this.elapsedBeforePause + (now - this.startTime)
  const thisLapMs = totalElapsed - this.lastLapTime  // 单圈时间 = 总时间 - 上次计次时间
  this.lastLapTime = totalElapsed

  this.lapCount++
  const record: LapRecord = {
    lapNumber: this.lapCount,
    lapTime: this.formatTime(thisLapMs),
    totalTime: this.formatTime(totalElapsed)
  }
  
  // 新记录插入数组开头(最新的在上面)
  this.laps = [record, ...this.laps]

  // 更新最快/最慢标记
  this.updateBestWorst()
}

计算示例:

圈数 总时间 单圈时间
第1圈 00:12.34 00:12.34
第2圈 00:25.67 00:13.33 (25.67 - 12.34)
第3圈 00:38.90 00:13.23 (38.90 - 25.67)

5.6 计算最快/最慢圈

updateBestWorst(): void {
  let bestIdx = 0
  let worstIdx = 0
  const lapTimes = this.laps.map(l => this.parseMs(l.lapTime))

  for (let i = 1; i < lapTimes.length; i++) {
    if (lapTimes[i] < lapTimes[bestIdx]) bestIdx = i
    if (lapTimes[i] > lapTimes[worstIdx]) worstIdx = i
  }
  
  this.bestLapIndex = bestIdx
  this.worstLapIndex = worstIdx
}

// 将时间字符串转回毫秒(用于比较)
parseMs(time: string): number {
  const parts = time.split(/[:.]/)              // ["00", "12", "34"]
  const m = parseInt(parts[0]) * 60000          // 分钟转毫秒
  const s = parseInt(parts[1]) * 1000           // 秒转毫秒
  const cs = parseInt(parts[2]) * 10            // 厘秒转毫秒
  return m + s + cs
}

六、界面实现

6.1 整体布局结构

build() {
  Column() {
    Scroll() {                                  // 可滚动容器
      Column() {
        // 标题
        Text('⏱ 秒表')
        
        // 时间显示区(圆形表盘)
        Stack() { ... }
        
        // 控制按钮区
        Row() { ... }
        
        // 计次列表区
        if (this.laps.length > 0) { ... }
        else { 空状态提示 }
      }
    }
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#ECEFF1')                   // 灰色背景
}

6.2 圆形表盘设计

使用 Stack 层叠容器实现同心圆效果:

Stack() {
  // 外圈(灰色背景)
  Circle()
    .width(240)
    .height(240)
    .fill('#ECEFF1')

  // 内圈(白色,带阴影)
  Circle()
    .width(210)
    .height(210)
    .fill(Color.White)
    .shadow({ radius: 6, color: '#33000000', offsetY: 3 })

  // 时间文字 + 状态
  Column() {
    Text(this.displayTime)
      .fontSize(46)
      .fontWeight(FontWeight.Bold)
      .fontColor('#263238')
      .fontFamily('Courier New')                // 等宽字体

    // 运行状态指示
    if (this.isRunning) {
      Row() {
        Circle()
          .width(8)
          .height(8)
          .fill('#F44336')                      // 红色圆点
          .margin({ right: 6 })
        Text('计时中')
          .fontSize(13)
          .fontColor('#F44336')
      }
      .margin({ top: 4 })
    } else if (this.elapsedBeforePause > 0) {
      Text('已暂停')
        .fontSize(13)
        .fontColor('#FF9800')                   // 橙色
        .margin({ top: 4 })
    }
  }
  .alignItems(HorizontalAlign.Center)
}
.width(240)
.height(240)
.margin({ top: 20 })

6.3 控制按钮区

三个按钮水平排列,中间的"开始/停止"按钮最大:

Row() {
  // 计次按钮
  Button('⏱ 计次')
    .width(90)
    .height(44)
    .backgroundColor('#78909C')
    .borderRadius(22)
    .fontSize(15)
    .onClick(() => this.lap())

  // 开始/停止按钮(动态切换)
  Button(this.isRunning ? '⏹ 停止' : '▶ 开始')
    .width(130)
    .height(52)
    .backgroundColor(this.isRunning ? '#F44336' : '#4CAF50')  // 红色停止/绿色开始
    .borderRadius(26)
    .fontSize(17)
    .fontWeight(FontWeight.Bold)
    .margin({ left: 16, right: 16 })
    .onClick(() => {
      if (this.isRunning) {
        this.stop()
      } else {
        this.start()
      }
    })

  // 重置按钮
  Button('↺ 重置')
    .width(90)
    .height(44)
    .backgroundColor('#78909C')
    .borderRadius(22)
    .fontSize(15)
    .onClick(() => this.reset())
}
.margin({ top: 24 })

6.4 计次列表

使用 ForEach 渲染列表:

if (this.laps.length > 0) {
  Column() {
    // 标题
    Row() {
      Text(`计次记录 (${this.laps.length})`)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .fontColor('#37474F')
    }
    .width('100%')
    .padding({ bottom: 8 })

    // 表头
    Row() {
      Text('圈数').fontSize(12).fontColor('#999').width(60)
      Text('单圈时间').fontSize(12).fontColor('#999').width(100)
      Text('总时间').fontSize(12).fontColor('#999').width(100)
    }
    .width('100%')
    .padding({ left: 8, right: 8, bottom: 4 })

    // 列表项
    ForEach(this.laps, (lap: LapRecord, index: number) => {
      Row() {
        // 圈数 + 标记
        Row() {
          if (index === this.bestLapIndex && this.laps.length > 1) {
            Text('🏆').fontSize(14).margin({ right: 2 })
          } else if (index === this.worstLapIndex && this.laps.length > 1) {
            Text('🐢').fontSize(14).margin({ right: 2 })
          }
          Text(`${lap.lapNumber}`)
            .fontSize(14)
            .fontColor('#37474F')
        }
        .width(60)

        Text(lap.lapTime)
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#1976D2')
          .width(100)

        Text(lap.totalTime)
          .fontSize(14)
          .fontColor('#666')
          .width(100)
      }
      .width('100%')
      .padding({ left: 8, right: 8, top: 8, bottom: 8 })
      .backgroundColor(index % 2 === 0 ? '#FFFFFF' : '#FAFAFA')  // 斑马纹
      .borderRadius(6)
      .margin({ bottom: 2 })
    }, (lap: LapRecord, index: number) => lap.lapNumber.toString() + index)
  }
  .width('100%')
  .padding(16)
  .backgroundColor(Color.White)
  .borderRadius(12)
  .shadow({ radius: 4, color: '#1A000000', offsetY: 2 })
  .margin({ top: 24, left: 16, right: 16, bottom: 40 })
}

6.5 空状态提示

没有计次记录时显示引导文字:

else {
  Column() {
    Text('👆')
      .fontSize(36)
    Text('点击「开始」启动计时\n点击「计次」记录单圈时间')
      .fontSize(14)
      .fontColor('#BDBDBD')
      .textAlign(TextAlign.Center)
      .margin({ top: 8 })
      .lineHeight(22)
  }
  .margin({ top: 40, bottom: 40 })
}

七、完整代码

Index.ets 完整源码

interface LapRecord {
  lapNumber: number
  lapTime: string
  totalTime: string
}

@Entry
@Component
struct Index {
  @State displayTime: string = '00:00.00'
  @State isRunning: boolean = false
  @State lapCount: number = 0
  @State laps: LapRecord[] = []
  @State bestLapIndex: number = -1
  @State worstLapIndex: number = -1

  private startTime: number = 0
  private elapsedBeforePause: number = 0
  private lastLapTime: number = 0
  private timerId: number = -1

  formatTime(ms: number): string {
    const totalCs = Math.floor(ms / 10)
    const minutes = Math.floor(totalCs / 6000)
    const seconds = Math.floor((totalCs % 6000) / 100)
    const centiseconds = totalCs % 100
    return `${this.pad(minutes)}:${this.pad(seconds)}.${this.pad(centiseconds)}`
  }

  pad(n: number): string {
    return n < 10 ? '0' + n : '' + n
  }

  start(): void {
    if (this.isRunning) return
    this.startTime = Date.now()
    this.isRunning = true
    this.lastLapTime = this.elapsedBeforePause

    this.timerId = setInterval(() => {
      const now = Date.now()
      const elapsed = this.elapsedBeforePause + (now - this.startTime)
      this.displayTime = this.formatTime(elapsed)
    }, 20)
  }

  stop(): void {
    if (!this.isRunning) return
    clearInterval(this.timerId)
    this.elapsedBeforePause += Date.now() - this.startTime
    this.isRunning = false
  }

  reset(): void {
    clearInterval(this.timerId)
    this.isRunning = false
    this.displayTime = '00:00.00'
    this.elapsedBeforePause = 0
    this.lastLapTime = 0
    this.lapCount = 0
    this.laps = []
    this.bestLapIndex = -1
    this.worstLapIndex = -1
  }

  lap(): void {
    if (!this.isRunning) return
    const now = Date.now()
    const totalElapsed = this.elapsedBeforePause + (now - this.startTime)
    const thisLapMs = totalElapsed - this.lastLapTime
    this.lastLapTime = totalElapsed

    this.lapCount++
    const record: LapRecord = {
      lapNumber: this.lapCount,
      lapTime: this.formatTime(thisLapMs),
      totalTime: this.formatTime(totalElapsed)
    }
    this.laps = [record, ...this.laps]
    this.updateBestWorst()
  }

  updateBestWorst(): void {
    let bestIdx = 0
    let worstIdx = 0
    const lapTimes = this.laps.map(l => this.parseMs(l.lapTime))

    for (let i = 1; i < lapTimes.length; i++) {
      if (lapTimes[i] < lapTimes[bestIdx]) bestIdx = i
      if (lapTimes[i] > lapTimes[worstIdx]) worstIdx = i
    }
    this.bestLapIndex = bestIdx
    this.worstLapIndex = worstIdx
  }

  parseMs(time: string): number {
    const parts = time.split(/[:.]/)
    const m = parseInt(parts[0]) * 60000
    const s = parseInt(parts[1]) * 1000
    const cs = parseInt(parts[2]) * 10
    return m + s + cs
  }

  aboutToDisappear(): void {
    if (this.timerId !== -1) {
      clearInterval(this.timerId)
    }
  }

  build() {
    Column() {
      Scroll() {
        Column() {
          Text('⏱ 秒表')
            .fontSize(26)
            .fontWeight(FontWeight.Bold)
            .fontColor('#37474F')
            .margin({ top: 36 })

          Stack() {
            Circle().width(240).height(240).fill('#ECEFF1')
            Circle().width(210).height(210).fill(Color.White)
              .shadow({ radius: 6, color: '#33000000', offsetY: 3 })

            Column() {
              Text(this.displayTime)
                .fontSize(46).fontWeight(FontWeight.Bold)
                .fontColor('#263238').fontFamily('Courier New')

              if (this.isRunning) {
                Row() {
                  Circle().width(8).height(8).fill('#F44336').margin({ right: 6 })
                  Text('计时中').fontSize(13).fontColor('#F44336')
                }.margin({ top: 4 })
              } else if (this.elapsedBeforePause > 0) {
                Text('已暂停').fontSize(13).fontColor('#FF9800').margin({ top: 4 })
              }
            }.alignItems(HorizontalAlign.Center)
          }.width(240).height(240).margin({ top: 20 })

          Row() {
            Button('⏱ 计次').width(90).height(44)
              .backgroundColor('#78909C').borderRadius(22).fontSize(15)
              .onClick(() => this.lap())

            Button(this.isRunning ? '⏹ 停止' : '▶ 开始')
              .width(130).height(52)
              .backgroundColor(this.isRunning ? '#F44336' : '#4CAF50')
              .borderRadius(26).fontSize(17).fontWeight(FontWeight.Bold)
              .margin({ left: 16, right: 16 })
              .onClick(() => { this.isRunning ? this.stop() : this.start() })

            Button('↺ 重置').width(90).height(44)
              .backgroundColor('#78909C').borderRadius(22).fontSize(15)
              .onClick(() => this.reset())
          }.margin({ top: 24 })

          if (this.laps.length > 0) {
            Column() {
              Row() {
                Text(`计次记录 (${this.laps.length})`)
                  .fontSize(16).fontWeight(FontWeight.Medium).fontColor('#37474F')
              }.width('100%').padding({ bottom: 8 })

              Row() {
                Text('圈数').fontSize(12).fontColor('#999').width(60)
                Text('单圈时间').fontSize(12).fontColor('#999').width(100)
                Text('总时间').fontSize(12).fontColor('#999').width(100)
              }.width('100%').padding({ left: 8, right: 8, bottom: 4 })

              ForEach(this.laps, (lap: LapRecord, index: number) => {
                Row() {
                  Row() {
                    if (index === this.bestLapIndex && this.laps.length > 1) {
                      Text('🏆').fontSize(14).margin({ right: 2 })
                    } else if (index === this.worstLapIndex && this.laps.length > 1) {
                      Text('🐢').fontSize(14).margin({ right: 2 })
                    }
                    Text(`${lap.lapNumber}`).fontSize(14).fontColor('#37474F')
                  }.width(60)
                  Text(lap.lapTime).fontSize(15).fontWeight(FontWeight.Bold)
                    .fontColor('#1976D2').width(100)
                  Text(lap.totalTime).fontSize(14).fontColor('#666').width(100)
                }
                .width('100%').padding({ left: 8, right: 8, top: 8, bottom: 8 })
                .backgroundColor(index % 2 === 0 ? '#FFFFFF' : '#FAFAFA')
                .borderRadius(6).margin({ bottom: 2 })
              }, (lap: LapRecord, index: number) => lap.lapNumber.toString() + index)
            }
            .width('100%').padding(16).backgroundColor(Color.White)
            .borderRadius(12).shadow({ radius: 4, color: '#1A000000', offsetY: 2 })
            .margin({ top: 24, left: 16, right: 16, bottom: 40 })
          } else {
            Column() {
              Text('👆').fontSize(36)
              Text('点击「开始」启动计时\n点击「计次」记录单圈时间')
                .fontSize(14).fontColor('#BDBDBD')
                .textAlign(TextAlign.Center).margin({ top: 8 }).lineHeight(22)
            }.margin({ top: 40, bottom: 40 })
          }
        }.width('100%').alignItems(HorizontalAlign.Center)
      }.width('100%').height('100%')
    }
    .width('100%').height('100%')
    .backgroundColor('#ECEFF1')
  }
}

八、开发踩坑记录

8.1 踩坑一:定时器内存泄漏

问题:当用户快速切换页面或关闭应用时,定时器仍在运行,导致内存泄漏。

解决:在 aboutToDisappear() 生命周期中清除定时器:

aboutToDisappear(): void {
  if (this.timerId !== -1) {
    clearInterval(this.timerId)
  }
}

8.2 踩坑二:暂停后时间不准确

问题:简单的计时器实现会在暂停后丢失时间。

// ❌ 错误做法
start(): void {
  this.timerId = setInterval(() => {
    this.elapsed += 20
    this.displayTime = this.formatTime(this.elapsed)
  }, 20)
}

原因setInterval 不精确,每次执行间隔可能超过 20ms。

解决:使用时间戳差值计算:

// ✅ 正确做法
start(): void {
  this.startTime = Date.now()                    // 记录启动时间
  this.timerId = setInterval(() => {
    const now = Date.now()
    const elapsed = this.elapsedBeforePause + (now - this.startTime)
    this.displayTime = this.formatTime(elapsed)
  }, 20)
}

8.3 踩坑三:ForEach key 重复

问题:使用 lap.lapNumber.toString() 作为 key,当重置后重新计次时,key 会重复(都从 “1” 开始)。

解决:将 key 与 index 组合:

ForEach(this.laps, (lap: LapRecord, index: number) => {
  // ...
}, (lap: LapRecord, index: number) => lap.lapNumber.toString() + index)

8.4 踩坑四:按钮状态同步

问题:点击"计次"按钮时,如果计时器未启动,没有反馈。

解决:添加条件判断:

lap(): void {
  if (!this.isRunning) return                    // 未启动时不响应
  // ...
}

九、运行与测试

9.1 运行应用

  1. 连接模拟器或真机
  2. 点击 DevEco Studio 的 Run 按钮
  3. 等待编译安装完成
    在这里插入图片描述

9.2 测试用例

操作步骤 预期结果 测试结果
启动应用 显示 00:00.00,空状态提示
点击"开始" 时间开始计时,按钮变"停止",显示"计时中"
等待 5 秒后点击"计次" 显示第1圈记录,单圈时间约 00:05.00
再等待 3 秒后点击"计次" 显示第2圈记录,单圈时间约 00:03.00
点击"停止" 时间暂停,显示"已暂停"
点击"开始" 从暂停时间继续计时
点击"重置" 所有数据清零
多圈计次后 最快圈显示🏆,最慢圈显示🐢

十、总结与展望

10.1 技术要点回顾

通过这个秒表项目,我们实践了:

技术点 应用场景
setInterval 定时器 实现毫秒级计时更新
时间戳计算 精确计时,支持暂停/继续
@State 状态管理 驱动 UI 响应式更新
Stack 层叠布局 实现同心圆表盘效果
ForEach 列表渲染 展示计次记录
条件渲染 if 动态显示状态和列表
生命周期钩子 防止定时器内存泄漏
数据处理算法 计算最快/最慢圈数

10.2 功能扩展方向

如果你想继续深化,可以尝试:

  1. 本地存储:使用 Preferences 保存历史记录
  2. 振动反馈:计次时触发振动(需权限)
  3. 主题切换:支持深色模式
  4. 语音播报:使用 TTS 读出圈数和时间
  5. 导出数据:生成 CSV/JSON 格式的计次记录
  6. 比较模式:对比两次计时的差异

附录:项目配置

app.json5

{
  "app": {
    "bundleName": "com.example.myapplication",
    "vendor": "example",
    "versionCode": 1000000,
    "versionName": "1.0.0"
  }
}

string.json

{
  "string": [
    { "name": "module_desc", "value": "秒表计时模块" },
    { "name": "EntryAbility_desc", "value": "秒表计时器" },
    { "name": "EntryAbility_label", "value": "秒表" }
  ]
}

开发环境:DevEco Studio + HarmonyOS NEXT API 24

如果这篇文章对你有帮助,欢迎点赞、收藏、评论!有问题欢迎留言讨论~

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐