# 鸿蒙 HarmonyOS 应用开发实战(第31期)|模拟时钟(Analog Clock)— Stack 布局与旋转动画精讲

一、应用概述

模拟时钟(Analog Clock) 是一款经典的桌面时钟应用,它以传统钟表的指针式设计展示当前系统时间。应用通过时针、分针、秒针三根独立指针的旋转角度来直观呈现时间信息,同时辅以数字时间显示,兼顾美观与功能性。项目基于 HarmonyOS 的 ArkTS 语言和声明式 UI 框架构建,完美展示了 Stack 层叠布局、Circle 圆形组件、rotate 旋转变换以及 setInterval 定时器等核心技术的综合运用。

对于鸿蒙开发者而言,模拟时钟是一个极佳的技术练兵场------它让你在极少的代码量中,同时掌握坐标定位中心旋转实时刷新三大关键能力,这些能力广泛适用于仪表盘、进度指示器、游戏等场景。

二、技术架构与组件树

2.1 整体架构

复制代码
@Entry @Component struct index31
├── Column (根容器)
│   ├── Row (顶部导航栏)
│   │   ├── Text ("‹ 返回")
│   │   ├── Text ("🕐 模拟时钟")
│   │   └── Blank (弹性空白)
│   └── Column (主内容区, layoutWeight=1, 居中)
│       └── Stack (钟表盘, 280×280)
│           ├── Circle (白色表盘背景)
│           ├── Text×12 (刻度数字 I ~ XII)
│           ├── Column (时针------旋转)
│           ├── Column (分针------旋转)
│           ├── Column (秒针------旋转)
│           └── Circle (中心圆点)

2.2 @Entry 与 @Component

arkts 复制代码
@Entry
@Component
struct index31 {

@Entry 装饰器标识该组件为页面的入口组件,允许通过路由直接跳转。@Component 声明这是一个可复用的自定义组件。在 HarmonyOS 中,每个页面通常由一个 @Entry @Component 结构体承载。

2.3 @State 驱动 UI 刷新

arkts 复制代码
@State time: string = '00:00:00';
intervalId: number = -1;

@State 是 ArkTS 最核心的响应式装饰器。当 time 值发生变化时,框架自动重新渲染所有依赖 time 的 UI 部分。intervalId 是非响应式的普通变量,用于保存定时器句柄,避免不必要的渲染。

三、生命周期管理

3.1 aboutToAppear --- 页面初始化

arkts 复制代码
aboutToAppear(): void {
  this.updateTime();
  this.intervalId = setInterval((): void => { this.updateTime(); }, 1000);
}

aboutToAppear 在组件即将挂载时调用,等效于传统 Android 的 onStart 或 iOS 的 viewDidLoad。这里做了两件事:

  1. 立即调用 updateTime() 获取当前时间,避免首次渲染时显示 00:00:00 的默认值。
  2. 启动定时器,每秒更新一次时间。

3.2 aboutToDisappear --- 资源清理

arkts 复制代码
aboutToDisappear(): void {
  clearInterval(this.intervalId);
}

当页面被销毁(如通过 router.back() 返回上一页)时,aboutToDisappear 被触发。务必在此清除定时器,否则定时器会持续运行,导致内存泄漏甚至崩溃。这是 HarmonyOS 开发中容易被忽视但非常重要的最佳实践。

四、核心代码深度解析

4.1 时间字符串的获取

arkts 复制代码
updateTime(): void {
  const now = new Date();
  const hh = now.getHours().toString().padStart(2, '0');
  const mm = now.getMinutes().toString().padStart(2, '0');
  const ss = now.getSeconds().toString().padStart(2, '0');
  this.time = hh + ':' + mm + ':' + ss;
}

通过标准 JavaScript Date 对象获取时、分、秒,并使用 padStart(2, '0') 确保始终为两位数(如 09:05:03)。然后将格式化后的字符串存入 @State time,驱动 UI 更新。

4.2 指针角度的计算

arkts 复制代码
get hourDeg(): number {
  const h = parseInt(this.time.slice(0, 2));
  return (h % 12) * 30 + parseInt(this.time.slice(3, 5)) * 0.5;
}

get minDeg(): number {
  return parseInt(this.time.slice(3, 5)) * 6;
}

get secDeg(): number {
  return parseInt(this.time.slice(6, 8)) * 6;
}

这三个计算属性(getter) 是时钟的核心逻辑:

  • 秒针角度:每秒 6°(360° ÷ 60 秒)
  • 分针角度:每分钟 6°(360° ÷ 60 分钟)
  • 时针角度:每小时 30°(360° ÷ 12 小时)+ 每分钟 0.5°(30° ÷ 60 分钟)

时针角度叠加了分钟偏移,使得指针能够在两小时之间平滑过渡,这是区分专业时钟与简陋时钟的关键细节。

4.3 Stack 层叠布局与刻度定位

arkts 复制代码
Stack() {
  Circle().width(280).height(280).fill('#fff')
    .shadow({ radius: 12, color: '#30000000', offsetY: 4 })

  Text('XII').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333')
    .position({ x: 128, y: 12 })
  Text('III').fontSize(16).fontWeight(FontWeight.Bold).fontColor('#333')
    .position({ x: 258, y: 130 })
  // ... 其余刻度

Stack 是一个层叠容器,所有子组件按声明顺序从下至上堆叠。position({ x, y }) 使用相对于 Stack 容器左上角的绝对坐标定位,坐标原点在容器的 (0,0) 位置。

这里有一个精妙的设计:钟表盘尺寸为 280×280,中心点为 (140, 140)。12 点刻度 Text("XII") 的位置为 (128, 12),即距离顶部 12px,水平居中。每个刻度数字的坐标都经过手工计算,确保它们均匀分布在圆周上。

4.4 rotate 旋转实现指针转动

arkts 复制代码
// 时针
Column() {
  Text('').width(5).height(70).backgroundColor('#333').borderRadius(3)
}
.rotate({ angle: this.hourDeg, centerX: 140, centerY: 140 })
.position({ x: 137.5, y: 75 })

// 分针
Column() {
  Text('').width(4).height(100).backgroundColor('#555').borderRadius(2)
}
.rotate({ angle: this.minDeg, centerX: 140, centerY: 140 })
.position({ x: 138, y: 45 })

// 秒针
Column() {
  Text('').width(2).height(110).backgroundColor('#F44336').borderRadius(1)
}
.rotate({ angle: this.secDeg, centerX: 140, centerY: 140 })
.position({ x: 139, y: 35 })

指针的实现方式非常巧妙------三根指针实际上是三个空的 Column 容器 ,内部只有一个 Text('') 空文本元素,通过设置 widthheight 形成一个矩形条。然后通过 borderRadius 将矩形条两端变圆,模拟指针外观。

.rotate() 是实现指针旋转的核心 API:

  • angle: 旋转角度(度数),正值为顺时针旋转
  • centerX/centerY: 旋转中心,这里设置为 (140, 140),即表盘中心

指针的 position 坐标需要精心计算。以时针为例:时针长 70px,中心在 (140,140),因此时针的顶部起点 y 坐标为 140 - 70 + 5 = 75(加了 5px 的偏移让指针看起来更自然),x 坐标为 140 - (5/2) = 137.5(宽度为 5px,需要减去一半来居中)。秒针和分针的计算逻辑相同。

4.5 中心圆点与数字时间

arkts 复制代码
Circle().width(10).height(10).fill('#333')
  .position({ x: 135, y: 135 })

一个直径 10px 的小圆覆盖在三根指针的交汇处,遮盖指针底部,使视觉效果更加整洁。

底部的数字时间使用 Courier New 等宽字体,与指针式钟表形成有趣的对比------既有传统的模拟显示,又有精确的数字显示。

五、HarmonyOS 特性分析

5.1 Stack 层叠布局

Stack 是 HarmonyOS 中最强大的布局容器之一,适用于:

  • 重叠 UI:如本例中的表盘 + 刻度 + 指针
  • 徽标(Badge):在头像右上角显示未读消息数
  • 遮罩层:在图片上方叠加半透明文字

使用 Stack 时,子组件通过 position() 进行绝对定位,或通过 alignContent 进行相对对齐。在本项目中,所有子组件都使用 position() 精确定位。

5.2 Circle 圆形组件

Circle 是 ArkTS 内置的形状组件,用于绘制正圆或椭圆。相较于通过 CSS borderRadius: 50% 模拟圆形的方式,Circle 更加语义化且性能更优。常用 API:

  • width/height:设置直径
  • fill:填充颜色
  • stroke/strokeWidth:描边颜色与宽度
  • shadow:阴影效果

5.3 setInterval / clearInterval 定时器

arkts 复制代码
this.intervalId = setInterval((): void => { this.updateTime(); }, 1000);
clearInterval(this.intervalId);

setInterval 是 HarmonyOS 提供的全局定时器函数,返回一个数值类型的 id。执行周期性任务时应当注意:

  1. 将 id 保存在普通成员变量中(非 @State
  2. aboutToDisappear 中通过 clearInterval 清除
  3. 避免在定时器回调中执行耗时操作(超过间隔时间会导致任务堆积)

六、UI/UX 设计与最佳实践

6.1 视觉设计亮点

  1. 干净的白色表盘 :白色背景配合柔和阴影 (rgba(0,0,0,0.19)) 营造出浮动的立体感。
  2. 指针差异化:时针最粗 (5px) 且颜色最深 (#333),分针适中 (4px, #555),秒针最细 (2px) 且使用醒目的红色 (#F44336)。这种设计让用户能一眼区分三根指针。
  3. 刻度精确标注:12 个主要刻度全部标注,没有使用简化的四点标注法,提升了时钟的专业感。

6.2 最佳实践

实践 说明
定时器生命周期管理 aboutToDisappearclearInterval,防止页面销毁后定时器继续运行
计算属性分离逻辑 使用 getter 替代在 build 中计算角度,使构建函数更简洁且易于测试
不可变数据更新 每次更新 @State time 时赋予全新字符串,而非修改现有字符串
弹性居中 layoutWeight(1) + justifyContent(FlexAlign.Center) 确保内容垂直居中
返回导航 统一的 ‹ 返回 样式和 router.back() 逻辑,保持全应用导航一致性

6.3 响应式适配建议

当前应用固定了 280×280 的表盘尺寸,若要适配不同屏幕,建议:

arkts 复制代码
private clockSize: number = Math.min(this.screenWidth, this.screenHeight) * 0.7;

然后所有坐标和尺寸都基于 this.clockSize 进行等比缩放。对于文本刻度,可以使用 this.clockSize * 0.05 等比例方式计算字体大小。

七、可扩展思路

  1. 主题切换:支持深色模式、自定义表盘颜色和背景图片。
  2. 闹钟功能 :结合 @ohos.notification@ohos.permission 实现闹钟提醒。
  3. 秒表/计时器:在页面内增加 Tab 切换,集成秒表和倒计时功能。
  4. 世界时钟:同时显示多个时区的时间,适合旅行者使用。
  5. 指针动画平滑化 :当前是每秒跳转一次(秒针的跳秒效果),可以改为使用 animateTo 实现平滑过渡。

八、总结

模拟时钟应用麻雀虽小五脏俱全,仅用 95 行代码就实现了一个功能完整的钟表应用。它完美示范了 ArkTS 声明式 UI 的几个核心范式:

  • 响应式状态管理@State 自动追踪数据变化并更新 UI
  • 声明式布局Stack + position 实现精确的层叠定位
  • 属性级动画rotate 的 angle 属性实时变化驱动指针转动
  • 生命周期钩子aboutToAppear 启动定时器,aboutToDisappear 清理资源

这些知识模块可以无缝迁移到更复杂的应用场景中。无论你是刚接触 HarmonyOS 开发的新手,还是想要巩固 ArkTS 基础的有经验开发者,模拟时钟都是一个绝佳的练手项目。

在下一期的实战文章中,我们将探讨一个更具实用价值的工具------间隔计时器(Interval Timer),看它如何利用同样的定时器机制,为健身训练提供精确的间歇计时功能。


本文是 HarmonyOS 应用开发实战系列第 31 期,更多内容请关注专栏。

相关推荐
独守一片天1 小时前
HarmonyOS 新生态 从原生应用到 AI Agent 的全场景智能底座
人工智能·安全·harmonyos
hqzing1 小时前
鸿蒙 PC 底层开发技术详解(八):鸿蒙 PC 上的问题定位手段
华为·harmonyos
爱写代码的阿森11 小时前
鸿蒙三方库 | harmony-utils之EncryptUtil AES对称加密详解
华为·harmonyos·鸿蒙·huawei
爱写代码的阿森11 小时前
鸿蒙三方库 | harmony-utils之FileUtil文件管理与目录详解
华为·harmonyos·鸿蒙·huawei
爱写代码的森12 小时前
鸿蒙三方库 | harmony-utils之DateUtil日期计算详解
华为·harmonyos·鸿蒙·huawei
youtootech13 小时前
HarmonyOS 6.0 长按菜单与拖拽预览
华为·harmonyos
特立独行的猫a14 小时前
鸿蒙PC Node.js三方库移植的AI移植框架与社区贡献完整流程
华为·node.js·harmonyos·三方库移植·鸿蒙pc
FF2501_9402285815 小时前
HarmonyOS应用《玄象》开发实战:FortuneTimelinePage 运势时间线:Timeline 自定义组件 + List 虚拟滚动
harmonyos·鸿蒙