在这里插入图片描述

引言

如果说示例 41「世界时钟」关注的是「每个时刻的当前时间」,那么示例 44「节日倒计时」关注的就是「从当前时刻到目标日期的剩余时间」。用户可以在列表中看到 6 个节日的剩余天数,点击某个节日后,顶部卡片会以大号字体展示到该节日的精确倒计时(天/小时/分/秒)。

这个应用的实现核心是每秒定时器驱动倒计时刷新——nowTick 状态每秒 +1 触发 UI 重绘,重绘时动态计算到各节日的剩余时间。设计上的亮点包括:节日列表与主显示区的联动(点击列表项切换主显示)、日期跨年处理(当年已过则顺延明年)、以及秒级精度的倒计时展示。

1. 应用概述与功能

「节日倒计时」页面自上而下分为:顶部返回栏、主显示倒计时卡片、全部节日剩余天数列表。

1.1 核心功能清单

  • 6 个预置节日:元旦、春节、劳动节、国庆节、圣诞节、七夕。
  • 秒级倒计时:实时展示到目标节日的精确剩余时间(天/小时/分/秒)。
  • 主显示切换:点击列表中的节日条目,顶部卡片切换显示该节日的倒计时。
  • 自动跨年:如果当年节日已过,自动顺延到明年。
  • 春节特殊处理:春节固定为 2027-02-06(示例),到期后顺延到 2028。
  • 列表剩余天数:列表中每个节日显示剩余整天数。

1.2 技术要点一览

  1. Festival 接口:定义节日数据结构(名称、月份、日期)。
  2. 定时器驱动刷新setInterval 每秒更新 nowTick 状态,触发所有倒计时重新计算。
  3. 动态日期计算getTarget() 方法处理节日已过/未过的逻辑,返回正确的目标日期。
  4. 剩余时间格式化remainText() 将毫秒差格式化为「天 小时 分 秒」的文本。
  5. ForEach 列表渲染:节日列表与主显示区联动,点击切换当前节日。

2. 核心知识点

2.1 Festival 接口

interface Festival {
  name: string;
  month: number;
  day: number;
}
  • name:节日中文名。
  • month:月份(1-12)。
  • day:日期(1-31)。

6 个节日的预置数据覆盖了全年主要节日。

2.2 @State 状态变量

@State festivals: Festival[] = [...]
@State currentName: string = '元旦';
@State nowTick: number = 0;
  • festivals:节日数据数组,标记为 @State 是因为需要 ForEach 遍历渲染(尽管数据不变,但 ForEach 要求数据源是响应式的)。
  • currentName:当前主显示的节日名称,初始为「元旦」。
  • nowTick:每秒 +1 的计数器,用于触发 UI 刷新(因为倒计时数据是在渲染时动态计算的,不需要存储为状态)。

2.3 定时器驱动

aboutToAppear(): void {
  this.timer = setInterval(() => {
    this.nowTick = this.nowTick + 1;
  }, 1000);
}

与示例 41 不同,这里定时器只做一件事:将 nowTick 状态 +1。UI 上显示的倒计时不是存储在 @State 中,而是在渲染时通过 remainText()remainDays() 动态计算。这种设计的好处是:不需要在定时器回调中计算和存储大量状态,只需触发一次重绘即可。

2.4 getTarget:动态目标日期

getTarget(f: Festival): Date {
  const now: Date = new Date();
  if (f.name === '春节') {
    const d: Date = new Date(2027, 1, 6);
    if (d.getTime() > now.getTime()) {
      return d;
    }
    return new Date(2028, 1, 6);
  }
  const year: number = now.getFullYear();
  const d: Date = new Date(year, f.month - 1, f.day);
  if (d.getTime() > now.getTime()) {
    return d;
  }
  return new Date(year + 1, f.month - 1, f.day);
}

日期处理逻辑:

  • 春节特殊处理:因为春节是农历节日,公历日期每年不同,所以用固定示例日期(2027-02-06),过期后顺延到 2028-02-06。
  • 其他节日:默认使用当年的日期。如果当年节日已过(d.getTime() > now.getTime()false),则顺延到明年。

month - 1 是因为 Date 构造函数中月份从 0 开始(0=一月,11=十二月)。

2.5 remainText:剩余时间格式化

remainText(f: Festival): string {
  const diff: number = this.getTarget(f).getTime() - new Date().getTime();
  const sec: number = Math.max(0, Math.floor(diff / 1000));
  const days: number = Math.floor(sec / 86400);
  const hours: number = Math.floor((sec % 86400) / 3600);
  const minutes: number = Math.floor((sec % 3600) / 60);
  const seconds: number = sec % 60;
  return days + ' 天 ' + hours + ' 小时 ' + minutes + ' 分 ' + seconds + ' 秒';
}

计算步骤:

  1. 毫秒差:目标日期时间戳 - 当前时间戳 = 剩余毫秒数。
  2. 转秒Math.floor(diff / 1000),用 Math.max(0, ...) 防止负数。
  3. 分解为天/小时/分/秒
    • 天:Math.floor(sec / 86400)(86400 = 24 × 3600)
    • 小时:Math.floor((sec % 86400) / 3600)
    • 分钟:Math.floor((sec % 3600) / 60)
    • 秒:sec % 60
  4. 拼接字符串'X 天 X 小时 X 分 X 秒'

2.6 remainDays:剩余整天数

remainDays(f: Festival): number {
  const diff: number = this.getTarget(f).getTime() - new Date().getTime();
  return Math.max(0, Math.ceil(diff / (1000 * 3600 * 24)));
}

Math.ceil 向上取整。例如剩余 2 天 1 小时,显示 3 天。这比 Math.floor 更符合「还剩几天」的直觉——用户更愿意看到「还有 3 天」而不是「还有 2 天」。

2.7 current:获取当前节日

current(): Festival {
  let cur: Festival = this.festivals[0];
  for (let i: number = 0; i < this.festivals.length; i++) {
    if (this.festivals[i].name === this.currentName) {
      cur = this.festivals[i];
    }
  }
  return cur;
}

遍历 festivals 找到 name 匹配 currentName 的节日。使用变量而非索引是为了展示遍历查找的模式,实际可以用索引直接访问。

2.8 列表点击切换

.onClick(() => {
  this.currentName = item.name;
})

点击列表项时,将 currentName 更新为点击的节日名称,触发主显示卡片刷新。这是「主从联动」的标准实现。

3. 源码逐段解析

3.1 数据初始化

6 个节日覆盖了公历和农历节日:元旦(1/1)、春节(农历,固定示例)、劳动节(5/1)、国庆节(10/1)、圣诞节(12/25)、七夕(农历,按公历 8/19 示例)。

3.2 主显示卡片

主显示卡片使用白色圆角卡片,居中对齐,顶部显示「距 XXX 还有」,下方是 20fp 蓝色加粗的精确倒计时文本。

3.3 节日列表

列表使用 List + ForEach,每个条目显示节日名和剩余天数(红色强调)。layoutWeight(1) 占据剩余空间。

3.4 整体布局

与系列其他示例一致:顶部返回栏 → 主内容区 → 浅灰背景。

4. 关键实现细节分析

4.1 动态计算 vs 存储状态

倒计时文本是在渲染时通过方法动态计算的,而不是存储在 @State 中。这种设计的优势是:

  • 代码简洁:不需要在定时器回调中更新多个状态变量。
  • 数据一致性:所有倒计时都基于同一个 nowTick 驱动的重绘,不会出现不同步。
  • 缺点:每次重绘都要重新计算所有节日的倒计时,但 6 个节日的计算量很小,性能完全没问题。

4.2 nowTick 的作用

nowTick 本身没有业务含义,它的唯一作用是「触发 UI 重绘」。每当 nowTick +1,@State 系统通知 UI 重新渲染,渲染过程中 remainText()remainDays() 被调用,计算出最新的倒计时数值。

4.3 跨年处理

getTarget() 中的跨年逻辑非常重要:

  • 如果今年的节日还没过 → 用今年的日期。
  • 如果今年的节日已过 → 用明年的日期。

这确保了倒计时永远指向「下一次」该节日,而不是已经过去的那一次。

4.4 春节的农历日期问题

春节是农历节日,公历日期不固定。当前实现使用了固定日期(2027-02-06)作为示例。更精确的实现需要一个农历到公历的转换表或算法。

4.5 性能考虑

每秒调用 remainText()remainDays() 各 7 次(6 个节日 + 1 个当前节日),对于手机来说毫无压力。即使扩展到 100 个节日也不会有性能问题。

5. 运行效果与操作指南

5.1 运行步骤

  1. 从入口页导航到「节日倒计时」页面。
  2. 页面打开后立即显示当前默认节日(元旦)的精确倒计时。
  3. 点击列表中的其他节日,切换主显示。
  4. 倒计时每秒自动刷新。

5.2 使用场景

  • 节日等待:期待某个节日的到来。
  • 活动倒计时:自定义节日用于活动倒计时。
  • 学期/假期倒计时:学生可以用它来期待假期。

6. 可扩展方向

6.1 添加自定义节日

允许用户添加自定义节日(如生日、纪念日、考试日期等)。

6.2 节日持久化

使用 @StorageLink 保存自定义节日列表。

6.3 节日提醒

在节日前 N 天发送通知。

6.4 动画效果

倒计时数字变化时添加过渡动画。

6.5 分类展示

按类别分组节日(传统节日、法定假日、西方节日等)。

7. 常见问题与调试技巧

7.1 倒计时不刷新

检查定时器是否启动。在 aboutToAppear 中添加日志确认。

7.2 倒计时显示负数

检查 remainText()remainDays() 中的 Math.max(0, ...) 是否正确使用。

7.3 日期跨年不正确

检查 getTarget() 的逻辑。确认当年节日已过时正确顺延到明年。

7.4 春节日期不准确

当前使用固定示例日期。对于真实春节日期,需要实现农历转换。

8. 总结

示例 44「节日倒计时」用约 152 行代码,展示了 ArkTS 在「定时刷新 + 动态列表」场景下的实现。重点回顾:

  • 定时器驱动重绘nowTick 每秒 +1 触发 UI 重绘,渲染时动态计算倒计时。
  • 动态日期计算getTarget() 处理节日已过/未过的逻辑。
  • 时间格式化remainText() 将毫秒差格式化为「天/小时/分/秒」。
  • 主从联动:点击列表切换主显示,是 ArkUI 交互的标准模式。
  • 跨年处理:自动将已过节日顺延到明年。

这些技术在后续的番茄钟、计时器等示例中会被复用。

Logo

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

更多推荐