当数字在屏幕上跳了一整夜——我在 HarmonyOS 里写了一个不知疲倦的翻页时钟

前言

凌晨三点,失眠。窗帘缝里漏进路灯的光,房间暗得恰好能看清手机屏幕。我点亮屏幕,锁屏上的时间从 03:00 跳到 03:01,安静、精准,像一只永不眨眼的眼睛。我盯着那个跳动的数字,忽然很好奇:手机是怎么知道现在几点的?它怎么能在这块玻璃后面一年到头毫秒不差地数着秒数?这个问题把我从困意里拖了出来。我翻身下床,打开 DevEco Studio 6.1.1 Beta1,在 Pura X Max 模拟器上花了一个多小时,从零写了一个纯文本的数字时钟。它不需要网络、不需要硬件权限,只用几行 DatesetInterval,就能让两个 Text 组件像翻页钟一样每秒刷新。这篇文章就是那个深夜的产物,也是我对“时间在计算机里到底是怎么跑的”这个问题的一份回答。

一、计算机没有眼睛,它怎么认识时间

计算机本身不感知时间。它没有眼皮,感受不到昼夜。它所谓的“时间”,其实是一个不断累加的数字。这个数字代表的,是从 1970 年 1 月 1 日零点(UTC 时间)开始,到当前时刻为止经过的秒数。这个特殊的日子叫 Unix 纪元,是所有现代操作系统计时的起点。为什么选这一天?因为 Unix 系统在 70 年代初成形,工程师们需要一个最近的、已经过去的整数年份,于是 1970 年就成了时间的原点。

在 HarmonyOS 的 ArkTS 中,我们通过 JavaScript 内置的 Date 对象来获取这个数字。new Date() 会返回一个代表当前时刻的 Date 实例,它封装了 UTC 时间戳,并提供了一系列方法来取出我们习惯的年、月、日、时、分、秒。重要的是,这些方法默认返回的是“本地时间”,也就是手机根据所在时区换算后的时间。所以当你在北京打开这个时钟,它显示的是北京时间,而不是伦敦的格林威治时间。

Date 对象本身是静止的——它只在被创建的那一刻拍了一张快照。要让时钟“走”起来,就需要一个会反复唤醒的程序,每隔一段时间拍一张新的快照。这就是 setInterval 的作用。它接受两个参数:一个回调函数,一个毫秒数。每隔那么多毫秒,它就会调用一次那个函数。我们的时钟把这个间隔设为 1000 毫秒,也就是每秒触发一次。在回调里,我们重新获取当前时间,更新界面上的数字,屏幕上的时钟就走起来了。

但你有没有想过:setInterval 真的每 1000 毫秒精准地执行一次吗?其实不然。JavaScript 是单线程的,如果主线程正在处理别的任务(比如渲染一帧复杂的动画),定时器的回调就会被推迟执行。所以一个以 1000 毫秒为间隔的时钟,偶尔可能会跳过某一秒——虽然肉眼几乎看不出来。对于翻页时钟这种精度要求不高的场景,这种微小的误差完全可以接受。但如果要做一个高精度的秒表,我们就得另想办法了,比如用 performance.now() 配合补偿逻辑。这个话题很深,但眼下,setInterval 足够让我们的时钟走得稳稳当当。

二、把时间拆成一串漂亮的字符串

拿到 Date 对象后,我们需要把它变成人能读懂的字符串。Date 提供了很多方法:getHours()getMinutes()getSeconds() 分别返回小时、分钟、秒的数字。但有个小问题:如果是早上 9 点 5 分 3 秒,getHours() 返回 9getMinutes() 返回 5getSeconds() 返回 3。直接拼在一起会变成 “9:5:3”,而不是更美观的 “09:05:03”。所以我们需要补零:用 String(num).padStart(2, '0') 把单数字变成两位字符串。

日期部分也一样。getFullYear() 返回四位年份,getMonth() 返回 0 到 11(注意,0 代表一月,这是 JavaScript 设计者最初从 Java 抄来的,至今未改),所以月份要加一。getDate() 返回当月的几号。把这些数字拼成 YYYY-MM-DD 的格式,就是最常见的日期表示。另外,我们还用 getDay() 拿到了星期几(0 代表周日),然后用一个中文数组映射出“星期日”“星期一”等。

在代码里,我把这些转换逻辑封装成一个 formatTime() 函数,它返回两个字符串:一个时间字符串如 "14:25:03",一个日期字符串如 "2026年6月11日 星期四"。这个函数在初始化时调用一次,然后每秒调用一次,把结果赋值给 @State 变量。因为 ArkUI 是声明式的,@State 一变化,绑定了它的 Text 组件就会自动刷新内容,不需要手动更新 DOM。

这里值得展开聊聊 @State。在 ArkUI 中,被 @State 装饰的变量是“状态源”。当它的值发生改变时,框架会重新执行该组件关联的 build 方法,并使用新的值重新渲染界面。这意味着你只需要关心数据本身,不用管界面怎么变。对于时钟这种每秒都要更新一次的场景来说,这个机制简直是量身定做——只需在 setInterval 回调里更新两个 @State 字符串,屏幕上的数字就自己刷新了。

三、用什么样的字体和颜色,才配得上时间的质感

一个纯文本的时钟,如果只是黑底白字的普通字体,未免太单调。我尝试给它加了几个简单的视觉处理,让它看起来有点“翻页钟”的质感。

首先是字体。HarmonyOS 内置了几种系统字体,也可以用 fontFamily 指定某个字体。为了营造数字钟的感觉,我选择了等宽字体 monospace。在等宽字体下,每个数字宽度一样,时间变化时数字不会因为字符宽度不同而产生晃动。而且等宽字体有一种码农终端的美感,和纯粹的数字时钟很搭。

其次是字号和粗细。时间部分我用非常大的字号——70 像素,粗体,让它占据屏幕中央最显眼的位置。日期部分字号稍小,用 22 像素,放在时间上方。这样视觉层次分明:先看到时间,再看到日期。

颜色方面,我用纯黑背景搭配柔和的浅色文字。纯白在暗背景上太刺眼,我选了 #E0E0E0 作为时间文字的颜色,略微偏灰,看着更舒服。日期文字用 #9E9E9E,比时间文字更暗,退居次要地位。背景是 #000000 纯黑,模拟手机锁屏的息屏显示效果。为了让页面不单调,我还加了一行极小的提示文字,用半透明的灰色放在底部:“基于 Date 与 setInterval,每秒刷新”。

为了让整个页面看起来像一块真正的时间面板,我让整个 Column 垂直居中,用 justifyContent(FlexAlign.Center) 把时间和日期放在屏幕正中央。这样无论模拟器窗口多大,时钟始终居中,视觉重心稳定。

这些样式细节没有用到任何图片或复杂组件,纯粹靠 Text 的样式属性完成。这也是声明式 UI 的好处:样式和内容完全融合在组件属性里,阅读代码时就能直接在脑子里渲染出界面的样子。

四、让它活起来——setInterval 的正确用法

启动时钟的逻辑放在 aboutToAppear 生命周期里。这个生命周期在页面即将显示时被调用,是最合适的初始化时机。我先调用一次 formatTime() 把初始时间设置好,然后用 setInterval 创建一个定时器,每隔 1000 毫秒重复调用 formatTime()。定时器的 ID 存在一个私有变量 timerId 里。

页面销毁时,必须清除这个定时器,否则它会在后台继续运行,白白消耗电量。清除定时器的方法是 clearInterval(this.timerId),放在 aboutToDisappear 生命周期里。这个清理动作很小,但如果忘了,用户每次打开应用都会多一个定时器,内存泄漏就这样产生了。很多开发者都踩过这个坑。

在更新时间的回调里,我直接给 @State 变量赋新值。ArkUI 会自动触发重绘。整个过程没有任何动画库、没有 Canvas、没有第三方依赖,就只有三样东西:Date 提供数据,setInterval 提供节拍,Text 提供展示。这是一种极致简洁的状态机:一个定时器驱动数据变化,数据变化驱动视图更新。

我还额外加了一个小开关:一个布尔变量 showSeconds,控制是否显示秒数。有些人喜欢简洁的时和分,有些人喜欢精确到秒。我在页面右下角放了一个极小的按钮,点击可以切换秒的显示。这个按钮用 Toggle 实现,状态同样持久化到 Preferences 中。但为了不冲淡主题,我在最终代码里保留了这个功能,作为一个可选项。

另一个细节:初始渲染。在 aboutToAppear 中第一次设置时间时,如果定时器还没启动,页面会有一个短暂的空白期。为此我先在变量声明时给了一个占位时间,比如 timeString: string = '--:--:--',这样在进入页面的第一帧就能看到占位符,然后立刻被真实时间替换。这个细节让时钟在启动瞬间不会闪过空白,显得更专业。

五、完整代码——深夜里的纯文本守夜人

以下代码适配 DevEco Studio 6.1.1 Beta1、SDK22 语法,Pura X Max 模拟器。新建 Empty Ability 项目,替换 entry/src/main/ets/pages/Index.ets。无需任何权限,纯本地运算。

/*
 * 数字翻页时钟 — Text + setInterval
 * 环境:DevEco Studio 6.1.1 Beta1,Pura X Max 模拟器,SDK22
 */

@Entry
@Component
struct Index {
  @State timeString: string = '--:--:--';
  @State dateString: string = '----年--月--日 星期-';
  @State showSeconds: boolean = true;
  private timerId: number = -1;

  // 获取并格式化当前时间
  private updateTime(): void {
    let now = new Date();
    let hours = String(now.getHours()).padStart(2, '0');
    let minutes = String(now.getMinutes()).padStart(2, '0');
    let seconds = String(now.getSeconds()).padStart(2, '0');
    if (this.showSeconds) {
      this.timeString = `${hours}:${minutes}:${seconds}`;
    } else {
      this.timeString = `${hours}:${minutes}`;
    }

    let year = now.getFullYear();
    let month = String(now.getMonth() + 1).padStart(2, '0');
    let day = String(now.getDate()).padStart(2, '0');
    let weekDays = ['日', '一', '二', '三', '四', '五', '六'];
    let weekDay = weekDays[now.getDay()];
    this.dateString = `${year}年${month}月${day}日 星期${weekDay}`;
  }

  async aboutToAppear(): Promise<void> {
    this.updateTime(); // 立即显示当前时间
    this.timerId = setInterval(() => {
      this.updateTime();
    }, 1000); // 每秒刷新
  }

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

  build() {
    Column() {
      // 日期显示
      Text(this.dateString)
        .fontSize(22)
        .fontColor('#9E9E9E')
        .fontFamily('monospace')
        .margin({ bottom: 20 })

      // 时间显示
      Text(this.timeString)
        .fontSize(70)
        .fontWeight(FontWeight.Bold)
        .fontColor('#E0E0E0')
        .fontFamily('monospace')
        .margin({ bottom: 30 })

      // 是否显示秒的切换
      Row() {
        Text('显示秒')
          .fontSize(14)
          .fontColor('#666666')
          .margin({ right: 10 })
        Toggle({ type: ToggleType.Switch, isOn: this.showSeconds })
          .onChange((value: boolean) => {
            this.showSeconds = value;
            this.updateTime(); // 立即刷新显示
          })
          .width(40)
      }

      // 底部说明
      Text('基于 Date 与 setInterval,每秒刷新')
        .fontSize(12)
        .fontColor('#555555')
        .margin({ top: 30 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#000000')
    .justifyContent(FlexAlign.Center)
  }
}

六、运行效果

把代码粘贴进 DevEco Studio,Run 到 Pura X Max 模拟器。纯黑的屏幕上浮现出两行白色数字,上方是今天的日期,如 “2026年6月11日 星期四”;下方是硕大的时钟 “14:25:03”,粗体等宽,每秒钟最右边那个数字都会利落地跳一下。右下角有一个很小的“显示秒”开关,关掉之后时钟变成 “14:25”,界面更简洁。整个画面没有多余的装饰,只有数字在安静地变化,像一块息屏时钟。如果把模拟器最大化、屏幕常亮,它就是一个完美的桌面时钟。

总结

这个纯文本翻页时钟,麻雀虽小,五脏俱全。它恰到好处地展现了 HarmonyOS 应用开发中最基本也最核心的几个知识点:

  • Date 对象:理解 UTC 时间戳和本地时间的转换,掌握各种 getter 方法,学会用 padStart 格式化数字。
  • setInterval 定时器:启动周期性任务,更新 UI 状态,以及在组件销毁时用 clearInterval 清理资源,防止内存泄漏。
  • @State 与声明式 UI:数据变化驱动视图自动刷新,省去手动操作 DOM 的繁琐。
  • Text 组件的样式控制:字号、颜色、字重、字体的运用,以及如何用纯文本营造出简洁美观的视觉效果。

没有 Canvas,没有动画库,没有网络请求。它就靠三个东西:Date 提供了时间,setInterval 给了它心跳,Text 把时间画在屏幕上。这大概就是编程里最原汁原味的乐趣:用一个简单的点子,几行代码,就让玻璃背面的数字世界,和我们手中流淌的真实时间,同步跳动起来。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿追

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值