HarmonyOS ArkTS 倒计时组件实战:从需求分析到基础实现
本文是《HarmonyOS ArkTS 企业级倒计时组件设计与实现》系列的第一篇,将从零开始,带你实现一个功能完整的倒计时组件。适合 HarmonyOS 开发初学者,通过实际案例学习组件开发的核心技能。
📋 前言
倒计时组件是移动应用中的常见需求,无论是电商秒杀、活动倒计时,还是限时优惠,都需要一个稳定可靠的倒计时组件。本文将带你从需求分析开始,逐步实现一个企业级的倒计时组件。
🎯 需求分析
核心需求
一个倒计时组件需要满足以下基本需求:
- 时间显示:能够显示剩余时间(天、时、分、秒)
- 实时更新:时间需要实时递减
- 多种格式:支持不同的时间显示格式
- 生命周期管理:页面切换时正确处理
- 倒计时结束:时间到0时的处理
时间格式需求
根据业务场景,我们需要支持以下8种时间格式:
| 格式类型 | 示例 | 使用场景 |
|---|---|---|
hh:mm:ss | 12:30:45 | 标准格式,最常用 |
D天hh小时 | 2天12小时 | 超过48小时时使用 |
mm:ss | 30:45 | 低于1小时时隐藏小时 |
hh:mm:ss.ms | 12:30:45.5 | 需要毫秒精度 |
mm:ss.ms | 30:45.5 | 毫秒+隐藏小时 |
ss.ms | 45.5 | 仅显示秒和毫秒 |
ss | 45 | 仅显示秒 |
自定义文本 | 即将开始 | 完全自定义文本 |
功能需求
- 定时器管理:每100ms更新一次,保证流畅度
- 状态管理:支持运行、暂停、停止等状态
- 页面生命周期:页面隐藏时暂停,显示时恢复
- 倒计时结束处理:支持关闭弹窗、跳转页面等操作
🏗️ 基础架构设计
组件结构
使用 HarmonyOS 的 @ComponentV2 装饰器创建组件:
@ComponentV2
export struct CountdownView {
// 组件属性
@Require @Param viewBuilderParam: ComponentParams
// 状态变量
@Local remainingTime: number = 0
@Local status: TimerStatus = TimerStatus.IDLE
// 时间分段
@Local D: number = 0 // 天
@Local hh: number = 0 // 小时
@Local mm: number = 0 // 分钟
@Local ss: number = 0 // 秒
@Local ms: number = 0 // 毫秒
// 定时器
@Local timerId: number | null = null
private readonly interval: number = 100 // 100ms刷新间隔
}
状态管理
使用枚举管理倒计时状态:
enum TimerStatus {
IDLE = 0, // 空闲
RUNNING = 1, // 运行中
PAUSED = 2 // 暂停
}
状态转换流程:
IDLE → RUNNING → PAUSED → RUNNING → IDLE
数据流设计
配置参数 → init() → 初始化状态
↓
aboutToAppear() → 启动定时器
↓
定时器回调 → 更新剩余时间 → 刷新UI
↓
时间到0 → 停止定时器 → 执行结束操作
💻 核心功能实现
1. 初始化方法
组件初始化时,需要从配置中读取参数:
init() {
// 获取配置
const element = this.viewBuilderParam.element
const config = element?.config as ElementConfig
const dataPath = element?.dataPath as DataPath
// 设置总时间
this.totalTime = DataUtil.handleArg(
dataPath.countDown,
this.viewBuilderParam?.args?.state
) as number
this.remainingTime = this.totalTime
// 读取配置
this.fontSize = sizeAdapt(config?.fontInfo?.size)
this.shouldShowMS = (config?.showMS === 1) ?? false
this.shouldHideHour = (
(this.totalTime < 60 * 60) && (config?.disableHour === 1)
) ?? false
// 颜色配置
this.numberFontColor = parseColor(config?.color)
this.numberBgColor = parseColor(config?.numberBgColor)
}
2. 时间计算
将总秒数转换为天、时、分、秒、毫秒:
refreshTimeNumber(seconds: number) {
// 计算天
this.D = Math.floor(seconds / (24 * 3600))
// 计算小时(需要考虑是否在hh:mm:ss模式)
const hhAsTop = this.keyList?.startsWith('hh')
this.hh = Math.floor(
(hhAsTop ? seconds : (seconds % (24 * 3600))) / 3600
)
// 计算分钟
this.mm = Math.floor((seconds % 3600) / 60)
// 计算秒(需要考虑是否在ss模式)
const ssAsTop = this.keyList?.startsWith('ss')
this.ss = Math.floor(ssAsTop ? seconds : (seconds % 60))
// 计算毫秒(取小数部分的第一位)
this.ms = Math.floor((seconds % 1) * 10)
}
关键点:
- 在
hh:mm:ss模式中,需要把天数折算成小时叠加 - 在
ss模式中,直接使用总秒数作为秒数 - 毫秒只显示1位,通过
(seconds % 1) * 10计算
3. 定时器管理
使用 setInterval 实现定时更新:
startCountdown(): void {
// 如果正在运行,直接返回
if (this.status === TimerStatus.RUNNING) {
return
}
this.status = TimerStatus.RUNNING
// 清除之前的定时器
if (this.timerId !== 0) {
clearInterval(this.timerId)
}
// 设置新的定时器,每100ms更新一次
this.timerId = setInterval(() => {
this.refreshTimeNumber(this.remainingTime)
if (this.remainingTime > 0) {
// 递减0.1秒,保留1位小数
this.remainingTime = Math.max(0, this.remainingTime - 0.1)
this.remainingTime = Math.round(this.remainingTime * 10) / 10
// 时间到0
if (this.remainingTime <= 0) {
this.stopCountdown()
this.timeOutAction()
}
}
}, this.interval)
}
关键点:
- 使用100ms间隔,平衡流畅度和性能
- 每次递减0.1秒,保留1位小数精度
- 使用
Math.round避免浮点数精度问题
4. 暂停和恢复
pauseCountdown(): void {
if (this.status === TimerStatus.RUNNING) {
this.status = TimerStatus.PAUSED
if (this.timerId !== 0) {
clearInterval(this.timerId)
this.timerId = 0
}
}
this.refreshTimeNumber(this.remainingTime)
}
stopCountdown(): void {
this.status = TimerStatus.IDLE
if (this.timerId !== 0) {
clearInterval(this.timerId)
this.timerId = 0
}
this.remainingTime = 0
this.refreshTimeNumber(this.remainingTime)
}
5. 生命周期管理
在 aboutToAppear 中启动定时器,在 aboutToDisappear 中清理资源:
aboutToAppear(): void {
this.init()
this.startCountdown()
this.refreshTimeNumber(this.remainingTime)
// 监听页面显示/隐藏事件
if (!this.ignorePause) {
emitter.on(PAGE_SHOW_EVENT, () => this.startCountdown())
emitter.on(PAGE_HIDE_EVENT, () => this.pauseCountdown())
}
}
aboutToDisappear(): void {
this.stopCountdown()
emitter.off(PAGE_HIDE_EVENT)
emitter.off(PAGE_SHOW_EVENT)
}
关键点:
- 页面隐藏时自动暂停,节省资源
- 页面显示时自动恢复,保证用户体验
- 组件销毁时清理定时器和事件监听,防止内存泄漏
6. 基础UI渲染
使用标志位控制显示内容:
@Local timeFormatFlags: Record<string, boolean> = {
'D': false,
'天': false,
'hh': !this.shouldHideHour ?? false,
':1': !this.shouldHideHour ?? false,
'小时': false,
'mm': false,
':2': false,
'ss': false,
'.': this.shouldShowMS ?? false,
'ms': this.shouldShowMS ?? false,
}
build() {
Row() {
// 天
if (this.timeFormatFlags['D']) {
Text(this.D.toString())
.fontSize(this.fontSize)
.fontColor(this.numberFontColor)
}
if (this.timeFormatFlags['天']) {
Text('天').fontSize(this.fontSize)
}
// 小时
if (this.timeFormatFlags['hh']) {
Text(this.padZero(this.hh))
.fontSize(this.fontSize)
.fontColor(this.numberFontColor)
}
if (this.timeFormatFlags[':1']) {
Text(':').fontSize(this.fontSize)
}
// 分钟
if (this.timeFormatFlags['mm']) {
Text(this.padZero(this.mm))
.fontSize(this.fontSize)
.fontColor(this.numberFontColor)
}
if (this.timeFormatFlags[':2']) {
Text(':').fontSize(this.fontSize)
}
// 秒
if (this.timeFormatFlags['ss']) {
Text(this.padZero(this.ss))
.fontSize(this.fontSize)
.fontColor(this.numberFontColor)
}
// 毫秒
if (this.timeFormatFlags['.']) {
Text('.').fontSize(this.fontSize)
}
if (this.timeFormatFlags['ms']) {
Text(this.ms.toString())
.fontSize(this.fontSize)
.fontColor(this.numberFontColor)
}
}
.position({ top: this.y, left: this.x })
}
关键点:
- 使用
Record<string, boolean>标志位控制显示 - 通过
if条件渲染,灵活控制显示内容 - 使用
padZero方法补零,保证两位数显示
7. 辅助方法
// 数字补零
padZero(num: number): string {
return num >= 10 || !this.keyList?.includes(':2ss')
? `${num}`
: `0${num}`
}
📦 完整代码示例
以下是简化版的基础实现(完整版包含更多配置和功能):
@ComponentV2
export struct CountdownView {
@Require @Param viewBuilderParam: ComponentParams
@Local remainingTime: number = 0
@Local status: TimerStatus = TimerStatus.IDLE
@Local timerId: number | null = null
@Local D: number = 0
@Local hh: number = 0
@Local mm: number = 0
@Local ss: number = 0
@Local ms: number = 0
private totalTime: number = 0
private readonly interval: number = 100
init() {
// 初始化配置
this.totalTime = 3600 // 示例:1小时
this.remainingTime = this.totalTime
}
aboutToAppear(): void {
this.startCountdown()
this.refreshTimeNumber(this.remainingTime)
}
refreshTimeNumber(seconds: number) {
this.D = Math.floor(seconds / (24 * 3600))
this.hh = Math.floor((seconds % (24 * 3600)) / 3600)
this.mm = Math.floor((seconds % 3600) / 60)
this.ss = Math.floor(seconds % 60)
this.ms = Math.floor((seconds % 1) * 10)
}
startCountdown(): void {
if (this.status === TimerStatus.RUNNING) {
return
}
this.status = TimerStatus.RUNNING
if (this.timerId !== 0) {
clearInterval(this.timerId)
}
this.timerId = setInterval(() => {
this.refreshTimeNumber(this.remainingTime)
if (this.remainingTime > 0) {
this.remainingTime = Math.max(0, this.remainingTime - 0.1)
this.remainingTime = Math.round(this.remainingTime * 10) / 10
if (this.remainingTime <= 0) {
this.stopCountdown()
}
}
}, this.interval)
}
stopCountdown(): void {
this.status = TimerStatus.IDLE
if (this.timerId !== 0) {
clearInterval(this.timerId)
this.timerId = 0
}
}
aboutToDisappear(): void {
this.stopCountdown()
}
build() {
Row() {
Text(`${this.padZero(this.hh)}:${this.padZero(this.mm)}:${this.padZero(this.ss)}`)
.fontSize(30)
}
}
padZero(num: number): string {
return num < 10 ? `0${num}` : `${num}`
}
}
🎓 关键知识点总结
1. 组件生命周期
init(): 组件初始化,读取配置aboutToAppear(): 组件即将显示,启动定时器aboutToDisappear(): 组件即将销毁,清理资源
2. 状态管理
- 使用
@Local装饰器声明响应式状态 - 使用枚举管理复杂状态
- 状态变化触发UI自动更新
3. 定时器管理
- 使用
setInterval实现定时更新 - 注意清理定时器,防止内存泄漏
- 合理设置刷新间隔(100ms平衡流畅度和性能)
4. 时间计算
- 注意不同格式下的计算逻辑
- 处理浮点数精度问题
- 毫秒显示1位即可
5. 生命周期管理
- 页面隐藏时暂停,节省资源
- 页面显示时恢复,保证体验
- 组件销毁时清理,防止泄漏
🚀 下一步
现在你已经掌握了倒计时组件的基础实现。在下一篇文章中,我们将深入探讨:
- 设计模式实践:如何识别过度设计,选择合适的设计模式
- 标志位驱动渲染:更优雅的条件渲染方式
- 状态机模式:如何管理复杂的状态转换
敬请期待!
系列文章导航:
- [第1篇] 基础篇:从需求分析到基础实现(本文)
- [第2篇] 设计模式反思篇:当AI建议用策略模式时,我选择了质疑
- [第3篇] 设计模式实践篇:标志位驱动渲染与状态机模式
- [第4篇] 性能优化篇:从100ms刷新到流畅体验
- [第5篇] 高级特性篇:时间区间样式切换
- [第6篇] 工程实践篇:测试与质量保证
- [第7篇] 总结篇:最佳实践与思考
相关资源:
讨论: 如果你在实现过程中遇到问题,欢迎在评论区留言讨论!