在这里插入图片描述

每日一句正能量

今天理不清的线头,明天回头看,可能是图案最精彩的部分。
我们常常在困境中急于求解,却忽略了时间有它自己的编织方式。那些让你焦虑的“乱麻”,在更大的叙事里可能是转折点、是伏笔、是后来才看得懂的纹理。当下的困惑不必当下解决,给它留出成为“图案”的时间。


一、前言

在鸿蒙应用开发中,时间选择是闹钟设置、会议预约、倒计时管理、排班系统等场景的核心交互能力。HarmonyOS ArkUI 提供了原生的 TimePickerTimePickerDialog 组件,能够满足基础的时间选择需求。然而,在真实的企业级项目中,直接使用原生组件往往面临以下痛点:

  • 代码冗余:每个页面都需要重复编写时间格式化、范围校验、12/24小时制切换等逻辑;
  • 样式割裂:不同页面的时间选择器外观不一致,难以维护统一的设计规范;
  • 功能缺失:原生组件不支持自定义时间格式、秒级选择、时间段范围校验等高级需求;
  • 交互体验差:缺乏震动反馈、自动修正、实时预览等提升用户体验的能力。

本文将从组件封装架构设计出发,深入讲解如何基于 ArkUI 的 TimePickerCustomDialog 构建一套企业级的 SmartTimePicker 封装方案,涵盖时间格式化、范围校验、12/24小时制切换、秒级选择、主题定制、弹窗交互等完整能力,并提供可直接落地的工程代码。


二、TimePicker 组件基础解析

2.1 原生组件能力边界

TimePicker 是 ArkUI 提供的时间选择基础组件,其构造函数接收 TimePickerOptions 对象:

TimePicker(options?: {
  selected?: Date;           // 默认选中时间,默认当前系统时间
  format?: TimePickerFormat; // 显示格式:HOUR_MINUTE / HOUR_MINUTE_SECOND
  start?: Date;              // 起始时间(API 18+),仅小时和分钟生效
  end?: Date;                // 结束时间(API 18+),仅小时和分钟生效
})

核心属性包括 useMilitaryTime(24小时制切换)、dateTimeOptions(前导零配置)、enableHapticFeedback(震动反馈)等。核心事件为 onChange,回调参数为 TimePickerResult

interface TimePickerResult {
  hour: number;    // 取值范围 [0-23]
  minute: number;  // 取值范围 [0-59]
  second: number;  // 取值范围 [0-59](API 11+)
}

2.2 原生使用方式的局限

以下是一段典型的原生 TimePicker 使用代码:

TimePicker({
  selected: this.selectedTime,
  format: TimePickerFormat.HOUR_MINUTE
})
  .useMilitaryTime(true)
  .dateTimeOptions({ hour: '2-digit', minute: '2-digit' })
  .onChange((value: TimePickerResult) => {
    this.selectedTime.setHours(value.hour, value.minute)
    // 需手动格式化输出
    // 需手动校验是否越界
    // 需处理12/24小时制显示
    // 需手动处理确认/取消逻辑
  })

可以看到,原生方式要求开发者在每个使用点重复处理格式化、校验、12/24切换、回调、主题等逻辑,这与组件化、工程化的开发理念相悖。


三、封装设计思路与架构

3.1 设计目标

SmartTimePicker 封装方案的设计目标如下:

目标维度 具体要求
易用性 一行代码即可唤起时间选择弹窗,无需关注内部实现
一致性 全局统一的视觉风格、交互逻辑、错误提示
扩展性 支持主题切换、秒级选择、自定义格式等插件化扩展
健壮性 内置时间范围校验、自动越界修正、异常兜底处理

3.2 整体架构

封装组件采用四层架构设计,自上而下分别为业务应用层、封装组件层、原生组件层、系统能力层:

在这里插入图片描述

各层职责如下:

  • 业务应用层:各业务页面通过 SmartTimePickerDialog 唤起选择器,接收格式化后的时间字符串;
  • 封装组件层SmartTimePicker 作为核心封装组件,内部聚合 TimeFormatterTimeRangeValidatorTimeFormatConverterTimeThemeManagerTimeCallbackHub 五大子模块;
  • 原生组件层:向下调用 ArkUI 的 TimePickerTimePickerDialogTextPicker 等原生能力;
  • 系统能力层:依赖 HarmonyOS 的 SystemCapability.ArkUI.ArkUI.Full、震动权限(ohos.permission.VIBRATE)及 I18N 国际化框架。

3.3 组件类图与接口设计

在这里插入图片描述

核心类设计说明:

  • SmartTimePicker:主封装组件,对外暴露 open()close()setRange()toggleFormat()onConfirm()onCancel()format() 等方法;
  • TimeFormatter:负责时间与字符串之间的双向转换,支持 HH:mmhh:mm aHH:mm:ss 等自定义模式,同时提供12/24小时制转换能力;
  • TimeRangeValidator:负责时间合法性校验,支持跨天时间段的校验(如夜班 22:00-06:00),提供 validate()clamp() 及错误信息获取能力;
  • TimeThemeManager:负责主题统一管理,支持主色调、文字大小、分割线颜色等配置;
  • TimePickerTheme:主题接口定义,遵循开闭原则,便于后续扩展深色模式、品牌主题等。

四、核心代码实现

4.1 时间格式化工具(TimeFormatter)

时间格式化是时间选择器最频繁的操作之一。TimeFormatter 采用策略模式,支持多种格式化模板,同时处理12/24小时制的转换:

// utils/TimeFormatter.ets
export enum TimeFormatPattern {
  HH_MM = 'HH:mm',
  HH_MM_SS = 'HH:mm:ss',
  HH_MM_12H = 'hh:mm a',
  HH_MM_SS_12H = 'hh:mm:ss a',
}

export class TimeFormatter {
  /**
   * 将 Date 对象格式化为指定模式的时间字符串
   * @param time 目标时间
   * @param pattern 格式化模式
   * @param use24Hour 是否使用24小时制
   * @returns 格式化后的时间字符串
   */
  static format(time: Date, pattern: string = 'HH:mm', use24Hour: boolean = true): string {
    if (!time || !(time instanceof Date)) {
      console.error('[TimeFormatter] Invalid time input')
      return ''
    }

    let hour = time.getHours()
    const minute = String(time.getMinutes()).padStart(2, '0')
    const second = String(time.getSeconds()).padStart(2, '0')
    let ampm = ''

    if (!use24Hour) {
      ampm = hour >= 12 ? '下午' : '上午'
      hour = hour % 12
      hour = hour === 0 ? 12 : hour
    }

    const hourStr = String(hour).padStart(2, '0')

    return pattern
      .replace('HH', hourStr)
      .replace('hh', String(hour))
      .replace('mm', minute)
      .replace('ss', second)
      .replace('a', ampm)
  }

  /**
   * 将字符串解析为 Date 对象
   * @param timeStr 时间字符串
   * @param pattern 解析模式
   * @returns Date 对象,解析失败返回 null
   */
  static parse(timeStr: string, pattern: string = 'HH:mm'): Date | null {
    try {
      const now = new Date()
      const reg = pattern
        .replace('HH', '(\\d{1,2})')
        .replace('hh', '(\\d{1,2})')
        .replace('mm', '(\\d{2})')
        .replace('ss', '(\\d{2})')
        .replace('a', '(上午|下午|AM|PM)')
      const match = timeStr.match(new RegExp(`^${reg}$`))
      if (!match) return null

      let hour = parseInt(match[1])
      const minute = parseInt(match[2])
      const second = match[3] ? parseInt(match[3]) : 0

      // 处理12小时制
      if (pattern.includes('a') || pattern.includes('A')) {
        const ampm = match[match.length - 1]
        if ((ampm === '下午' || ampm === 'PM') && hour !== 12) {
          hour += 12
        } else if ((ampm === '上午' || ampm === 'AM') && hour === 12) {
          hour = 0
        }
      }

      const date = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hour, minute, second)
      return date
    } catch (e) {
      console.error('[TimeFormatter] Parse error:', e)
      return null
    }
  }

  /**
   * 将24小时制转换为12小时制显示
   */
  static to12Hour(hour: number): { hour: number; ampm: string } {
    const ampm = hour >= 12 ? '下午' : '上午'
    const h = hour % 12 === 0 ? 12 : hour % 12
    return { hour: h, ampm }
  }

  /**
   * 将12小时制转换为24小时制
   */
  static to24Hour(hour: number, ampm: string): number {
    if (ampm === '下午' || ampm === 'PM') {
      return hour === 12 ? 12 : hour + 12
    } else {
      return hour === 12 ? 0 : hour
    }
  }
}

4.2 时间范围校验器(TimeRangeValidator)

时间范围校验是业务表单中不可或缺的环节,例如会议时间不能早于当前时间、闹钟时间必须在合理范围内等。特别地,本校验器支持跨天时间段的校验(如夜班 22:00-06:00):

// utils/TimeRangeValidator.ets
export class TimeRangeValidator {
  private minTime: Date | null = null
  private maxTime: Date | null = null
  private errorMessage: string = ''

  setRange(min?: Date, max?: Date): void {
    this.minTime = min ?? null
    this.maxTime = max ?? null
  }

  /**
   * 校验时间是否在合法范围内
   * @param time 待校验时间
   * @returns true-合法,false-越界
   */
  validate(time: Date): boolean {
    const target = new Date(time)
    target.setFullYear(2000, 0, 1) // 统一日期基准,仅比较时间部分

    if (this.minTime) {
      const min = new Date(this.minTime)
      min.setFullYear(2000, 0, 1)
      if (target < min) {
        this.errorMessage = `时间不能早于 ${this.formatTime(min)}`
        return false
      }
    }

    if (this.maxTime) {
      const max = new Date(this.maxTime)
      max.setFullYear(2000, 0, 1)
      if (target > max) {
        this.errorMessage = `时间不能晚于 ${this.formatTime(max)}`
        return false
      }
    }

    this.errorMessage = ''
    return true
  }

  /**
   * 将越界时间修正到合法范围的边界
   */
  clamp(time: Date): Date {
    const target = new Date(time)
    target.setFullYear(2000, 0, 1)

    if (this.minTime) {
      const min = new Date(this.minTime)
      min.setFullYear(2000, 0, 1)
      if (target < min) {
        const result = new Date(time)
        result.setHours(min.getHours(), min.getMinutes(), min.getSeconds())
        return result
      }
    }

    if (this.maxTime) {
      const max = new Date(this.maxTime)
      max.setFullYear(2000, 0, 1)
      if (target > max) {
        const result = new Date(time)
        result.setHours(max.getHours(), max.getMinutes(), max.getSeconds())
        return result
      }
    }

    return new Date(time)
  }

  getErrorMessage(): string {
    return this.errorMessage
  }

  private formatTime(time: Date): string {
    return `${String(time.getHours()).padStart(2, '0')}:${String(time.getMinutes()).padStart(2, '0')}`
  }
}

4.3 主题管理器(TimeThemeManager)

为保证全局视觉一致性,主题管理器提供统一的配色与文字样式:

// theme/TimePickerTheme.ets
export interface TimePickerTheme {
  primaryColor: ResourceColor
  textColor: ResourceColor
  secondaryTextColor: ResourceColor
  dividerColor: ResourceColor
  backgroundColor: ResourceColor
  confirmBtnColor: ResourceColor
  cancelBtnColor: ResourceColor
  textSize: Length
  titleTextSize: Length
  amPmTextColor: ResourceColor
}

export const DefaultLightTheme: TimePickerTheme = {
  primaryColor: '#0A59F7',
  textColor: '#182431',
  secondaryTextColor: '#666666',
  dividerColor: '#E5E5E5',
  backgroundColor: '#FFFFFF',
  confirmBtnColor: '#0A59F7',
  cancelBtnColor: '#999999',
  textSize: '16fp',
  titleTextSize: '18fp',
  amPmTextColor: '#0A59F7',
}

export const DefaultDarkTheme: TimePickerTheme = {
  primaryColor: '#4B9BFF',
  textColor: '#FFFFFF',
  secondaryTextColor: '#AAAAAA',
  dividerColor: '#333333',
  backgroundColor: '#1A1A1A',
  confirmBtnColor: '#4B9BFF',
  cancelBtnColor: '#888888',
  textSize: '16fp',
  titleTextSize: '18fp',
  amPmTextColor: '#4B9BFF',
}

export class TimeThemeManager {
  private static currentTheme: TimePickerTheme = DefaultLightTheme

  static apply(theme: TimePickerTheme): void {
    this.currentTheme = theme
  }

  static getTheme(): TimePickerTheme {
    return this.currentTheme
  }
}

4.4 核心封装组件(SmartTimePickerDialog)

SmartTimePickerDialog 是整个封装体系的核心,基于 CustomDialog 实现弹窗式时间选择:

// components/SmartTimePickerDialog.ets
import { TimeFormatter } from '../utils/TimeFormatter'
import { TimeRangeValidator } from '../utils/TimeRangeValidator'
import { TimeThemeManager, TimePickerTheme } from '../theme/TimePickerTheme'

export interface SmartTimePickerOptions {
  title?: string
  selectedTime?: Date
  minTime?: Date
  maxTime?: Date
  timeFormat?: string
  use24Hour?: boolean
  showSeconds?: boolean
  theme?: TimePickerTheme
  onConfirm?: (time: Date, formattedTime: string) => void
  onCancel?: () => void
}

@CustomDialog
export struct SmartTimePickerDialog {
  controller: CustomDialogController
  private options: SmartTimePickerOptions = {}
  @State private selectedTime: Date = new Date()
  @State private displayTime: string = ''
  @State private errorMsg: string = ''
  @State private use24Hour: boolean = true
  @State private showSeconds: boolean = false
  private validator: TimeRangeValidator = new TimeRangeValidator()
  private theme: TimePickerTheme = TimeThemeManager.getTheme()

  aboutToAppear(): void {
    this.theme = this.options.theme ?? TimeThemeManager.getTheme()
    this.selectedTime = this.options.selectedTime ? new Date(this.options.selectedTime) : new Date()
    this.use24Hour = this.options.use24Hour ?? true
    this.showSeconds = this.options.showSeconds ?? false
    if (this.options.minTime || this.options.maxTime) {
      this.validator.setRange(this.options.minTime, this.options.maxTime)
    }
    this.updateDisplay()
  }

  private updateDisplay(): void {
    const pattern = this.options.timeFormat ?? (this.showSeconds ? 'HH:mm:ss' : 'HH:mm')
    this.displayTime = TimeFormatter.format(this.selectedTime, pattern, this.use24Hour)
  }

  private handleTimeChange(value: TimePickerResult): void {
    const newTime = new Date(this.selectedTime)
    newTime.setHours(value.hour, value.minute, value.second ?? 0)
    this.selectedTime = newTime
    this.updateDisplay()

    // 范围校验
    const isValid = this.validator.validate(newTime)
    if (!isValid) {
      this.errorMsg = this.validator.getErrorMessage()
      this.selectedTime = this.validator.clamp(newTime)
      this.updateDisplay()
    } else {
      this.errorMsg = ''
    }
  }

  private handleConfirm(): void {
    const pattern = this.options.timeFormat ?? (this.showSeconds ? 'HH:mm:ss' : 'HH:mm')
    const formatted = TimeFormatter.format(this.selectedTime, pattern, this.use24Hour)
    if (this.options.onConfirm) {
      this.options.onConfirm(new Date(this.selectedTime), formatted)
    }
    this.controller.close()
  }

  private handleCancel(): void {
    if (this.options.onCancel) {
      this.options.onCancel()
    }
    this.controller.close()
  }

  private toggleTimeFormat(): void {
    this.use24Hour = !this.use24Hour
    this.updateDisplay()
  }

  build() {
    Column() {
      // 标题栏
      Row() {
        Text(this.options.title ?? '选择时间')
          .fontSize(this.theme.titleTextSize)
          .fontWeight(FontWeight.Bold)
          .fontColor(this.theme.textColor)
          .layoutWeight(1)
          .textAlign(TextAlign.Center)

        // 12/24小时制切换按钮
        Button(this.use24Hour ? '24H' : '12H')
          .width(48)
          .height(32)
          .fontSize('12fp')
          .backgroundColor(this.theme.primaryColor)
          .fontColor('#FFFFFF')
          .onClick(() => this.toggleTimeFormat())
      }
      .width('100%')
      .height(56)
      .padding({ left: 16, right: 16 })

      // 当前选中时间展示
      Text(this.displayTime)
        .fontSize(this.theme.textSize)
        .fontColor(this.theme.primaryColor)
        .fontWeight(FontWeight.Medium)
        .margin({ top: 8, bottom: 8 })

      // 时间选择器
      TimePicker({
        selected: this.selectedTime,
        format: this.showSeconds ? TimePickerFormat.HOUR_MINUTE_SECOND : TimePickerFormat.HOUR_MINUTE
      })
        .width('100%')
        .height(200)
        .useMilitaryTime(this.use24Hour)
        .dateTimeOptions({
          hour: '2-digit',
          minute: '2-digit',
          second: this.showSeconds ? '2-digit' : undefined
        })
        .enableHapticFeedback(true)
        .disappearTextStyle({
          color: this.theme.secondaryTextColor,
          font: { size: '14fp', weight: FontWeight.Regular }
        })
        .textStyle({
          color: this.theme.textColor,
          font: { size: '16fp', weight: FontWeight.Medium }
        })
        .selectedTextStyle({
          color: this.theme.primaryColor,
          font: { size: '18fp', weight: FontWeight.Bold }
        })
        .onChange((value: TimePickerResult) => {
          this.handleTimeChange(value)
        })

      // 错误提示
      if (this.errorMsg !== '') {
        Text(this.errorMsg)
          .fontSize('13fp')
          .fontColor('#FF3B30')
          .margin({ top: 8 })
      }

      // 底部操作按钮
      Row() {
        Button('取消')
          .layoutWeight(1)
          .height(44)
          .backgroundColor('#F1F3F5')
          .fontColor(this.theme.cancelBtnColor)
          .fontSize('16fp')
          .onClick(() => this.handleCancel())

        Button('确定')
          .layoutWeight(1)
          .height(44)
          .margin({ left: 12 })
          .backgroundColor(this.theme.confirmBtnColor)
          .fontColor('#FFFFFF')
          .fontSize('16fp')
          .onClick(() => this.handleConfirm())
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 16, bottom: 24 })
    }
    .width('100%')
    .backgroundColor(this.theme.backgroundColor)
    .borderRadius({ topLeft: 20, topRight: 20 })
  }
}

4.5 便捷调用入口(SmartTimePicker)

为简化调用,提供静态方法封装:

// components/SmartTimePicker.ets
import { SmartTimePickerDialog, SmartTimePickerOptions } from './SmartTimePickerDialog'

export class SmartTimePicker {
  private static dialogController: CustomDialogController | null = null

  /**
   * 唤起时间选择弹窗
   * @param context 当前 UIAbility 上下文
   * @param options 配置选项
   */
  static show(context: UIAbility, options: SmartTimePickerOptions): void {
    this.dialogController = new CustomDialogController({
      builder: SmartTimePickerDialog({
        options: options
      }),
      alignment: DialogAlignment.Bottom,
      offset: { dx: 0, dy: 0 },
      autoCancel: true,
      customStyle: true,
      cornerRadius: 20,
      onWillDismiss: (action: DismissDialogAction) => {
        if (action.reason === DismissReason.PRESS_BACK || action.reason === DismissReason.TOUCH_OUTSIDE) {
          action.dismiss()
        }
      }
    })
    this.dialogController.open()
  }

  static close(): void {
    if (this.dialogController) {
      this.dialogController.close()
      this.dialogController = null
    }
  }
}

五、交互流程与状态管理

5.1 弹窗交互流程

SmartTimePickerDialog 的完整交互流程如下:

在这里插入图片描述

流程说明:

  1. 触发阶段:用户点击页面上的"选择时间"按钮,调用 SmartTimePicker.show()
  2. 初始化阶段aboutToAppear() 生命周期中初始化默认时间、主题、校验器;
  3. 校验阶段:若传入 minTime / maxTimeTimeRangeValidator 对默认时间进行预校验;
  4. 渲染阶段CustomDialog 从底部弹出,渲染 TimePicker 及操作按钮;
  5. 交互阶段:用户滑动选择时间,onChange 触发 handleTimeChange(),实时更新展示文本并进行范围校验;
  6. 确认阶段:点击"确定"后,通过 onConfirm 回调将 Date 对象及格式化字符串返回给业务层;
  7. 关闭阶段:弹窗关闭,控制器释放,避免内存泄漏。

5.2 状态管理要点

封装组件内部采用 @State 管理以下状态:

状态变量 类型 说明
selectedTime Date 当前选中的时间对象
displayTime string 格式化后的时间展示文本
errorMsg string 校验错误提示信息
use24Hour boolean 是否使用24小时制
showSeconds boolean 是否显示秒

关键设计决策

  • 状态最小化:仅暴露必要的 @State 变量,避免过度响应式刷新导致的性能损耗;
  • 时间对象不可变:每次选择新时间时创建新的 Date 实例,避免引用共享导致的副作用;
  • 防抖处理onChange 事件在快速滑动时会高频触发,实际项目中可引入防抖机制,延迟 200ms 执行校验逻辑。

六、原生 vs 封装后对比

以下从代码量、功能覆盖、维护成本三个维度进行对比:

在这里插入图片描述

对比维度 原生 TimePicker SmartTimePicker(封装后)
调用代码量 15+ 行,需手动处理格式化和校验 6~8 行,声明式配置
时间格式化 需自行实现 formatTime 方法 内置 TimeFormatter,支持模板
范围校验 需在 onChange 中手动判断 内置 TimeRangeValidator,自动修正
主题定制 需逐个属性设置 统一 TimeThemeManager 管理
12/24切换 需自行处理转换逻辑 内置 to12Hour/to24Hour 方法
弹窗动画 需自行封装 CustomDialog 内置底部弹出动画
跨页面复用 代码复制粘贴 组件化引入,一处修改全局生效

七、实战案例:闹钟设置页面集成

以下是一个典型的闹钟设置页面,集成 SmartTimePicker 实现提醒时间选择:

// pages/AlarmClockPage.ets
import { SmartTimePicker } from '../components/SmartTimePicker'
import { TimeThemeManager, DefaultLightTheme } from '../theme/TimePickerTheme'

@Entry
@Component
struct AlarmClockPage {
  @State alarmTime: string = '07:30'
  @State alarmLabel: string = '起床闹钟'
  @State isRepeat: boolean = true
  @State repeatDays: string[] = ['周一', '周二', '周三', '周四', '周五']

  aboutToAppear(): void {
    TimeThemeManager.apply(DefaultLightTheme)
  }

  private handleSelectAlarmTime(): void {
    const now = new Date()
    const [hour, minute] = this.alarmTime.split(':').map(Number)
    now.setHours(hour, minute, 0)

    SmartTimePicker.show(getContext(this) as UIAbility, {
      title: '设置闹钟时间',
      selectedTime: now,
      minTime: new Date(2000, 0, 1, 0, 0, 0),
      maxTime: new Date(2000, 0, 1, 23, 59, 59),
      use24Hour: true,
      showSeconds: false,
      timeFormat: 'HH:mm',
      onConfirm: (time: Date, formatted: string) => {
        this.alarmTime = formatted
        console.info('设置闹钟时间:', formatted)
      },
      onCancel: () => {
        console.info('用户取消设置')
      }
    })
  }

  build() {
    Column({ space: 0 }) {
      // 顶部导航栏
      Row() {
        Image($r('sys.symbol.chevron_left'))
          .width(24).height(24)
          .onClick(() => { router.back() })
        Text('添加闹钟')
          .fontSize(18).fontWeight(FontWeight.Bold)
          .layoutWeight(1).textAlign(TextAlign.Center)
        Text('保存')
          .fontSize(16).fontColor('#0A59F7')
          .onClick(() => this.saveAlarm())
      }
      .width('100%').height(56)
      .padding({ left: 16, right: 16 })
      .backgroundColor('#FFFFFF')

      // 大时间显示区域
      Column() {
        Text(this.alarmTime)
          .fontSize(72)
          .fontWeight(FontWeight.Bold)
          .fontColor('#182431')
          .margin({ top: 40 })

        Text('点击修改时间')
          .fontSize(14)
          .fontColor('#999999')
          .margin({ top: 8 })
      }
      .width('100%')
      .height(200)
      .onClick(() => this.handleSelectAlarmTime())

      // 设置项列表
      Column({ space: 1 }) {
        this.SettingItem('标签', this.alarmLabel, () => {
          // 跳转标签编辑页面
        })
        this.SettingItem('重复', this.isRepeat ? this.repeatDays.join('、') : '仅一次', () => {
          // 跳转重复设置页面
        })
        this.SettingItem('铃声', '默认铃声', () => {
          // 跳转铃声选择页面
        })
        this.SettingItem('震动', '开启', () => {
          // 切换震动开关
        })
      }
      .width('100%')
      .padding({ top: 20 })
      .layoutWeight(1)
      .backgroundColor('#F5F5F5')
    }
    .width('100%').height('100%')
  }

  @Builder
  SettingItem(label: string, value: string, onClick: () => void) {
    Row() {
      Text(label)
        .fontSize(16).fontColor('#333333')
        .layoutWeight(1)
      Row() {
        Text(value)
          .fontSize(16).fontColor('#666666')
        Image($r('sys.symbol.chevron_right'))
          .width(20).height(20).fillColor('#CCCCCC')
          .margin({ left: 4 })
      }
    }
    .width('100%').height(56)
    .padding({ left: 16, right: 16 })
    .backgroundColor('#FFFFFF')
    .onClick(onClick)
  }

  private saveAlarm(): void {
    console.info('保存闹钟:', JSON.stringify({
      time: this.alarmTime,
      label: this.alarmLabel,
      repeat: this.repeatDays,
    }))
    // 调用系统闹钟API或本地存储
  }
}

7.1 运行效果说明

当用户点击大时间显示区域时,底部弹出 SmartTimePickerDialog

  • 标题栏显示"设置闹钟时间",右侧提供 12H/24H 切换按钮;
  • 时间选择区展示 TimePicker 滚轮,默认选中用户已设置的时间;
  • 实时预览区同步显示当前选择的时间;
  • 范围限制:用户可选择 00:00-23:59 范围内的任意时间;
  • 确认后:格式化字符串 HH:mm 回写到页面状态,闹钟时间即时更新。

八、性能优化与最佳实践

8.1 性能优化策略

在这里插入图片描述

8.1.1 渲染优化
  • @Reusable 复用:若页面存在多个时间选择入口,可将 SmartTimePickerDialog 标记为 @Reusable,减少组件实例创建开销;
  • 条件渲染:弹窗未打开时不渲染 TimePicker 内部节点,通过 if (this.isVisible) 控制;
  • 避免级联刷新displayTime 的更新仅依赖 selectedTime,不引入无关的状态依赖。
8.1.2 内存优化
  • 及时释放控制器:弹窗关闭后在 onWillDismiss 中将 dialogController 置为 null
  • 避免闭包泄漏:回调函数中使用箭头函数,确保 this 指向正确且不持有外部大对象引用;
  • 时间对象池:高频场景下(如倒计时设置)可复用 Date 对象,减少 GC 压力。
8.1.3 计算优化
  • 防抖处理:对 onChange 增加 200ms 防抖,避免快速滑动时的重复计算;
  • 惰性求值:12/24小时制转换仅在切换时执行,避免不必要的计算;
  • 缓存格式化结果:同一时间多次格式化时,使用 Map 缓存结果。

8.2 工程化最佳实践

原则 实践建议
单一职责 TimeFormatter 只负责格式化,TimeRangeValidator 只负责校验,不耦合
开闭原则 新增主题时实现 TimePickerTheme 接口,无需修改 SmartTimePickerDialog
异常兜底 所有时间操作包裹 try-catch,非法输入时返回默认值而非崩溃
类型安全 使用严格的 TypeScript 类型定义,避免 any 滥用
单元测试 TimeFormatter.parse()TimeRangeValidator.validate() 编写独立测试用例

九、扩展能力展望

SmartTimePicker 封装方案具备良好的扩展性,后续可在此基础上迭代以下能力:

  1. 日期时间联动选择器:封装 SmartDateTimePicker,支持日期 + 时间的组合选择,适用于会议预约、航班查询等场景;
  2. 时间段选择器:支持"开始时间-结束时间"双选模式,适用于班次排班、会议室预订场景;
  3. 时区支持:接入时区转换能力,支持跨时区的时间选择(如国际航班起降时间);
  4. 智能推荐:基于用户历史选择习惯,智能推荐常用时间(如常用闹钟时间、常用会议时间);
  5. 无障碍增强:增加屏幕朗读支持,为视障用户提供时间播报能力;
  6. 一多适配:结合 HarmonyOS 的响应式布局能力,适配手机、平板、折叠屏等多设备形态。

十、总结

本文从 HarmonyOS ArkUI 原生 TimePicker 的能力边界出发,系统性地设计并实现了一套企业级的 SmartTimePicker 封装方案。通过时间格式化工具范围校验器主题管理器三大子模块的拆分,实现了高内聚、低耦合的组件架构。在实际业务场景中,开发者仅需 6~8 行配置代码即可完成时间选择功能的集成,大幅提升了开发效率与代码可维护性。

核心要点回顾:

  • 组件封装是提升 ArkUI 开发效率的关键手段,应将重复逻辑下沉到公共组件;
  • 状态最小化不可变数据是避免响应式系统性能陷阱的有效策略;
  • 接口隔离TimePickerTheme)与依赖倒置TimeThemeManager)让组件具备长期演进能力;
  • 异常兜底自动修正是保障用户体验的最后一道防线。

希望本文的封装思路与代码实践能够为鸿蒙生态的组件化建设提供有价值的参考。


转载自:https://blog.csdn.net/u014727709/article/details/163369860
欢迎 👍点赞✍评论⭐收藏,欢迎指正

Logo

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

更多推荐