【HarmonyOS 6】时间选择器实战:优雅的时间输入设计
在移动应用开发中,时间输入是一个非常常见的需求。无论是设置闹钟、记录睡眠时间、预约服务,还是填写表单,都需要用户输入时间。传统的键盘输入方式虽然精确,但操作繁琐且容易出错。而TimePicker 组件,配合快捷按钮的设计,可以让用户快速、准确地选择时间,大大提升用户体验。本文将通过一个实际案例——健康管理应用的睡眠记录功能,带你深入理解 TimePicker 组件的使用方法和时间输入的交互设计技巧
前言
在移动应用开发中,时间输入是一个非常常见的需求。无论是设置闹钟、记录睡眠时间、预约服务,还是填写表单,都需要用户输入时间。传统的键盘输入方式虽然精确,但操作繁琐且容易出错。而TimePicker 组件,配合快捷按钮的设计,可以让用户快速、准确地选择时间,大大提升用户体验。
本文将通过一个实际案例——健康管理应用的睡眠记录功能,带你深入理解 TimePicker 组件的使用方法和时间输入的交互设计技巧。
本文适合已经了解 ArkTS 基础语法的初学者阅读。通过学习本文,你将掌握:
- TimePicker 组件的基础用法和属性配置
- 时间数据的格式化与转换
- 快捷时间按钮的设计模式
- 时间选择器的显示与隐藏控制
- 时间计算与验证逻辑
- 对话框中的时间选择器布局
- 响应式时间选择器设计
什么是 TimePicker 组件
TimePicker 是时间选择器组件,用于让用户选择具体的时间(小时和分钟)。它采用滚轮式的交互方式,用户可以通过上下滑动来选择时间,比传统的键盘输入更加直观和便捷。
核心特点:
- 滚轮式交互:上下滑动选择时间,符合移动端操作习惯
- 24小时制:支持 00:00 到 23:59 的时间范围
- 实时预览:选择过程中可以实时看到当前时间
- 精确到分钟:支持小时和分钟的独立选择
- 易于集成:可以嵌入到对话框、表单等任何场景
常见应用场景:
- 闹钟设置:设置起床闹钟、提醒事项
- 睡眠记录:记录就寝时间和起床时间
- 预约服务:选择预约时间、会议时间
- 日程安排:设置事件开始和结束时间
- 提醒功能:设置定时提醒的时间
案例背景
我们要实现一个睡眠记录对话框,包含以下功能:
- 就寝时间选择:用户可以选择晚上几点睡觉
- 起床时间选择:用户可以选择早上几点起床
- 快捷时间按钮:提供常用时间(22:00、22:30、23:00 等)快速选择
- 自定义时间:点击"自定义"按钮展开 TimePicker 进行精确选择
- 时长自动计算:根据就寝和起床时间自动计算睡眠时长
- 数据验证:确保起床时间晚于就寝时间
最终效果如下图所示:
初始状态:
点击"自定义"后:
一、完整代码实现
让我们先看睡眠记录对话框的完整实现代码。
@Component
export struct SleepTabContent {
// 时间相关状态
@State bedTime: string = '23:00'; // 就寝时间(字符串格式)
@State wakeTime: string = '07:00'; // 起床时间(字符串格式)
@State bedTimePicker: Date = new Date(); // TimePicker 的 Date 对象
@State wakeTimePicker: Date = new Date(); // TimePicker 的 Date 对象
@State showBedTimePicker: boolean = false; // 是否显示就寝时间选择器
@State showWakeTimePicker: boolean = false; // 是否显示起床时间选择器
@State sleepDuration: number = 480; // 睡眠时长(分钟)
@State sleepQuality: number = 3; // 睡眠质量(1-5)
@State showRecordDialog: boolean = false; // 是否显示记录对话框
private prefService: PreferencesService | null = null;
aboutToAppear(): void {
const ctx = getContext(this) as common.UIAbilityContext;
this.prefService = PreferencesService.getInstance(ctx);
}
// 将字符串时间转换为 Date 对象
private createTimeDate(time: string): Date {
const now: Date = new Date();
const parts: string[] = time.split(':');
const hour: number = parseInt(parts[0]);
const minute: number = parseInt(parts[1]);
now.setHours(hour, minute, 0, 0);
return now;
}
// 将 Date 对象转换为字符串时间
private formatTime(date: Date): string {
const hour: string = String(date.getHours()).padStart(2, '0');
const minute: string = String(date.getMinutes()).padStart(2, '0');
return `${hour}:${minute}`;
}
// 将 TimePickerResult 转换为字符串时间
private formatPickerTime(result: TimePickerResult): string {
const hour: string = String(result.hour).padStart(2, '0');
const minute: string = String(result.minute).padStart(2, '0');
return `${hour}:${minute}`;
}
// 计算睡眠时长
calculateDuration(): void {
const bedParts: string[] = this.bedTime.split(':');
const wakeParts: string[] = this.wakeTime.split(':');
let bedMinutes: number = parseInt(bedParts[0]) * 60 + parseInt(bedParts[1]);
let wakeMinutes: number = parseInt(wakeParts[0]) * 60 + parseInt(wakeParts[1]);
// 如果起床时间小于就寝时间,说明跨天了
if (wakeMinutes < bedMinutes) {
wakeMinutes += 24 * 60;
}
this.sleepDuration = wakeMinutes - bedMinutes;
}
// 格式化时长显示
formatDuration(minutes: number): string {
const hours: number = Math.floor(minutes / 60);
const mins: number = minutes % 60;
return `${hours}小时${mins}分钟`;
}
// 保存睡眠记录
async saveSleepRecord(): Promise<void> {
if (!this.prefService) return;
this.calculateDuration();
const today: string = getTodayDateString();
const record: SleepRecord = {
id: Date.now(),
date: today,
bedTime: this.bedTime,
wakeTime: this.wakeTime,
duration: this.sleepDuration,
quality: this.sleepQuality
};
await this.prefService.saveSleepRecord(record);
this.showRecordDialog = false;
}
build() {
Stack() {
// 主页面内容
Scroll() {
Column() {
// 标题栏
Row() {
Text('作息管理')
.fontSize(24)
.fontWeight(FontWeight.Bold)
Blank()
Button('手动记录')
.fontSize(14)
.fontColor(Color.White)
.backgroundColor('#9C27B0')
.borderRadius(20)
.onClick((): void => {
// 初始化时间选择器
this.bedTimePicker = this.createTimeDate(this.bedTime);
this.wakeTimePicker = this.createTimeDate(this.wakeTime);
this.showBedTimePicker = false;
this.showWakeTimePicker = false;
this.showRecordDialog = true;
})
}
.width('100%')
.padding(16)
// 其他内容...
}
}
// 记录对话框
if (this.showRecordDialog) {
this.RecordDialog()
}
}
}
@Builder
RecordDialog() {
Column() {
Column() {
// 标题
Text('记录睡眠')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#333333')
// 就寝时间选择
this.TimeSelector(
'就寝时间',
this.bedTime,
['22:00', '22:30', '23:00', '00:00'],
this.showBedTimePicker,
this.bedTimePicker,
(time: string) => {
this.bedTime = time;
this.bedTimePicker = this.createTimeDate(time);
this.calculateDuration();
},
() => {
this.bedTimePicker = this.createTimeDate(this.bedTime);
this.showBedTimePicker = true;
this.showWakeTimePicker = false;
},
(value: TimePickerResult) => {
this.bedTimePicker = this.createTimeDate(this.formatPickerTime(value));
},
() => {
this.showBedTimePicker = false;
},
() => {
this.bedTime = this.formatTime(this.bedTimePicker);
this.calculateDuration();
this.showBedTimePicker = false;
}
)
// 起床时间选择
this.TimeSelector(
'起床时间',
this.wakeTime,
['06:30', '07:00', '07:30', '08:00'],
this.showWakeTimePicker,
this.wakeTimePicker,
(time: string) => {
this.wakeTime = time;
this.wakeTimePicker = this.createTimeDate(time);
this.calculateDuration();
},
() => {
this.wakeTimePicker = this.createTimeDate(this.wakeTime);
this.showWakeTimePicker = true;
this.showBedTimePicker = false;
},
(value: TimePickerResult) => {
this.wakeTimePicker = this.createTimeDate(this.formatPickerTime(value));
},
() => {
this.showWakeTimePicker = false;
},
() => {
this.wakeTime = this.formatTime(this.wakeTimePicker);
this.calculateDuration();
this.showWakeTimePicker = false;
}
)
// 睡眠时长显示
Row() {
Text('睡眠时长')
.fontSize(16)
.fontColor('#333333')
Blank()
Text(this.formatDuration(this.sleepDuration))
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#9C27B0')
}
.width('100%')
.margin({ top: 16 })
// 底部按钮
Row() {
Button('取消')
.layoutWeight(1)
.height(44)
.backgroundColor('#F5F5F5')
.fontColor('#333333')
.onClick(() => {
this.showRecordDialog = false;
})
Button('保存')
.layoutWeight(1)
.height(44)
.backgroundColor('#9C27B0')
.fontColor(Color.White)
.margin({ left: 12 })
.onClick(() => {
this.saveSleepRecord();
})
}
.width('100%')
.margin({ top: 20 })
}
.width('85%')
.padding(24)
.backgroundColor(Color.White)
.borderRadius(16)
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0, 0, 0, 0.5)')
.justifyContent(FlexAlign.Center)
}
@Builder
TimeSelector(
label: string,
currentTime: string,
quickTimes: string[],
showPicker: boolean,
pickerDate: Date,
onQuickSelect: (time: string) => void,
onShowPicker: () => void,
onPickerChange: (value: TimePickerResult) => void,
onPickerCancel: () => void,
onPickerConfirm: () => void
) {
Column() {
// 标签和当前时间
Row() {
Text(label)
.fontSize(16)
.fontColor('#333333')
Blank()
Text(currentTime)
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#9C27B0')
}
.width('100%')
.margin({ top: 16 })
// 快捷时间按钮
Row() {
ForEach(quickTimes, (time: string) => {
Text(time)
.fontSize(12)
.fontColor(currentTime === time ? Color.White : '#333333')
.backgroundColor(currentTime === time ? '#9C27B0' : '#F5F5F5')
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
.borderRadius(8)
.onClick(() => {
onQuickSelect(time);
})
})
Text('自定义')
.fontSize(12)
.fontColor(showPicker ? Color.White : '#333333')
.backgroundColor(showPicker ? '#9C27B0' : '#F5F5F5')
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
.borderRadius(8)
.onClick(() => {
onShowPicker();
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ top: 8 })
// TimePicker(条件显示)
if (showPicker) {
Column() {
TimePicker({ selected: pickerDate })
.onChange((value: TimePickerResult) => {
onPickerChange(value);
})
Row() {
Button('取消')
.fontSize(14)
.backgroundColor('#F5F5F5')
.fontColor('#333333')
.onClick(() => {
onPickerCancel();
})
Button('确定')
.fontSize(14)
.backgroundColor('#9C27B0')
.fontColor(Color.White)
.margin({ left: 8 })
.onClick(() => {
onPickerConfirm();
})
}
.width('100%')
.justifyContent(FlexAlign.End)
.margin({ top: 8 })
}
.width('100%')
.padding(12)
.backgroundColor('#F9F9F9')
.borderRadius(8)
.margin({ top: 8 })
}
}
}
}
二、TimePicker 组件基础知识
2.1 TimePicker 的基本用法
最简单的 TimePicker:
TimePicker({ selected: new Date() })
这会创建一个时间选择器,默认选中当前时间。
核心参数说明:
| 参数 | 类型 | 说明 | 示例 |
|---|---|---|---|
| selected | Date | 当前选中的时间 | selected: new Date() |
注意: TimePicker 只接受 Date 对象作为参数,不能直接传入字符串时间。
2.2 TimePicker 的事件处理
TimePicker 的核心事件是 onChange,当用户滑动选择时间时触发。
基础用法:
@State selectedTime: Date = new Date();
TimePicker({ selected: this.selectedTime })
.onChange((value: TimePickerResult) => {
console.log(`选中的时间:${value.hour}:${value.minute}`);
})
TimePickerResult 接口:
interface TimePickerResult {
hour: number; // 小时(0-23)
minute: number; // 分钟(0-59)
}
实时更新示例:
@State hour: number = 12;
@State minute: number = 0;
Column() {
Text(`当前时间:${this.hour}:${this.minute}`)
.fontSize(18)
TimePicker({ selected: new Date() })
.onChange((value: TimePickerResult) => {
this.hour = value.hour;
this.minute = value.minute;
})
}
2.3 时间格式的转换
在实际开发中,我们通常需要在三种格式之间转换:
- 字符串格式:
"23:00"(用于显示和存储) - Date 对象:
new Date()(用于 TimePicker) - TimePickerResult:
{ hour: 23, minute: 0 }(TimePicker 的回调)
转换方法封装:
// 1. 字符串 → Date 对象
private createTimeDate(time: string): Date {
const now: Date = new Date();
const parts: string[] = time.split(':');
const hour: number = parseInt(parts[0]);
const minute: number = parseInt(parts[1]);
now.setHours(hour, minute, 0, 0);
return now;
}
// 2. Date 对象 → 字符串
private formatTime(date: Date): string {
const hour: string = String(date.getHours()).padStart(2, '0');
const minute: string = String(date.getMinutes()).padStart(2, '0');
return `${hour}:${minute}`;
}
// 3. TimePickerResult → 字符串
private formatPickerTime(result: TimePickerResult): string {
const hour: string = String(result.hour).padStart(2, '0');
const minute: string = String(result.minute).padStart(2, '0');
return `${hour}:${minute}`;
}
为什么需要 padStart(2, ‘0’)?
// ❌ 不使用 padStart
const hour = 9;
const minute = 5;
const time = `${hour}:${minute}`; // "9:5"(不规范)
// ✅ 使用 padStart
const hour = String(9).padStart(2, '0');
const minute = String(5).padStart(2, '0');
const time = `${hour}:${minute}`; // "09:05"(规范)
padStart(2, '0') 确保数字始终是两位数,不足两位时在前面补 0。
2.4 完整的时间选择流程
@Component
struct TimePickerDemo {
@State displayTime: string = '12:00'; // 显示的时间(字符串)
@State pickerTime: Date = new Date(); // TimePicker 的时间(Date)
@State showPicker: boolean = false; // 是否显示 TimePicker
build() {
Column() {
// 1. 显示当前时间
Text(`当前时间:${this.displayTime}`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
// 2. 点击按钮显示 TimePicker
Button('选择时间')
.onClick(() => {
// 将字符串时间转换为 Date 对象
this.pickerTime = this.createTimeDate(this.displayTime);
this.showPicker = true;
})
// 3. TimePicker(条件显示)
if (this.showPicker) {
Column() {
TimePicker({ selected: this.pickerTime })
.onChange((value: TimePickerResult) => {
// 实时更新 pickerTime
const timeStr = this.formatPickerTime(value);
this.pickerTime = this.createTimeDate(timeStr);
})
Row() {
Button('取消')
.onClick(() => {
this.showPicker = false;
})
Button('确定')
.onClick(() => {
// 将 Date 对象转换为字符串
this.displayTime = this.formatTime(this.pickerTime);
this.showPicker = false;
})
}
}
}
}
}
private createTimeDate(time: string): Date {
const now: Date = new Date();
const parts: string[] = time.split(':');
now.setHours(parseInt(parts[0]), parseInt(parts[1]), 0, 0);
return now;
}
private formatTime(date: Date): string {
const hour: string = String(date.getHours()).padStart(2, '0');
const minute: string = String(date.getMinutes()).padStart(2, '0');
return `${hour}:${minute}`;
}
private formatPickerTime(result: TimePickerResult): string {
const hour: string = String(result.hour).padStart(2, '0');
const minute: string = String(result.minute).padStart(2, '0');
return `${hour}:${minute}`;
}
}
流程:
用户点击"选择时间"
↓
将 displayTime(字符串)转换为 pickerTime(Date)
↓
显示 TimePicker
↓
用户滑动选择时间
↓
onChange 回调,更新 pickerTime
↓
用户点击"确定"
↓
将 pickerTime(Date)转换为 displayTime(字符串)
↓
隐藏 TimePicker,显示新时间
三、快捷时间按钮设计
3.1 为什么需要快捷按钮
虽然 TimePicker 可以精确选择任意时间,但在实际使用中,用户往往会选择一些常用的时间点。例如:
- 就寝时间:22:00、22:30、23:00、00:00
- 起床时间:06:30、07:00、07:30、08:00
- 闹钟时间:06:00、07:00、08:00、09:00
提供快捷按钮可以让用户一键选择这些常用时间,无需滑动 TimePicker,大大提升操作效率。
用户体验对比:
❌ 只有 TimePicker:
用户想选择 23:00
→ 打开 TimePicker
→ 滑动小时到 23
→ 滑动分钟到 00
→ 点击确定
(需要 4 步操作)
✅ 有快捷按钮:
用户想选择 23:00
→ 点击 [23:00] 按钮
(只需 1 步操作)
3.2 快捷按钮的实现
基础实现:
@State bedTime: string = '23:00';
Row() {
ForEach(['22:00', '22:30', '23:00', '00:00'], (time: string) => {
Text(time)
.fontSize(12)
.fontColor(this.bedTime === time ? Color.White : '#333333')
.backgroundColor(this.bedTime === time ? '#9C27B0' : '#F5F5F5')
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
.borderRadius(8)
.onClick(() => {
this.bedTime = time;
this.calculateDuration(); // 更新睡眠时长
})
})
}
.justifyContent(FlexAlign.SpaceBetween)
视觉效果:

3.3 快捷时间的选择原则
就寝时间的快捷选项:
const bedTimeOptions = ['22:00', '22:30', '23:00', '00:00'];
选择依据:
- 22:00:早睡派,健康作息
- 22:30:比较常见的就寝时间
- 23:00:大多数人的就寝时间
- 00:00:晚睡派,熬夜党
起床时间的快捷选项:
const wakeTimeOptions = ['06:30', '07:00', '07:30', '08:00'];
选择依据:
- 06:30:早起派,晨练或早课
- 07:00:标准起床时间
- 07:30:稍晚一点的起床时间
- 08:00:周末或假期的起床时间
设计原则:
- 覆盖常见场景:选择大多数用户会用到的时间点
- 间隔合理:通常 30 分钟一个档位
- 数量适中:3-5 个选项,不要太多
- 留有自定义:提供"自定义"按钮满足特殊需求
3.4 "自定义"按钮的设计
"自定义"按钮用于展开 TimePicker,让用户精确选择时间。
实现代码:
@State showBedTimePicker: boolean = false;
Row() {
// 快捷时间按钮
ForEach(['22:00', '22:30', '23:00', '00:00'], (time: string) => {
Text(time)
.fontSize(12)
.fontColor(this.bedTime === time ? Color.White : '#333333')
.backgroundColor(this.bedTime === time ? '#9C27B0' : '#F5F5F5')
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
.borderRadius(8)
.onClick(() => {
this.bedTime = time;
this.bedTimePicker = this.createTimeDate(time);
this.calculateDuration();
})
})
// 自定义按钮
Text('自定义')
.fontSize(12)
.fontColor(this.showBedTimePicker ? Color.White : '#333333')
.backgroundColor(this.showBedTimePicker ? '#9C27B0' : '#F5F5F5')
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
.borderRadius(8)
.onClick(() => {
this.bedTimePicker = this.createTimeDate(this.bedTime);
this.showBedTimePicker = true;
this.showWakeTimePicker = false; // 关闭另一个选择器
})
}
交互逻辑:
-
点击快捷按钮:
- 直接设置时间
- 关闭 TimePicker(如果已打开)
- 更新相关计算(如睡眠时长)
-
点击"自定义"按钮:
- 初始化 TimePicker 为当前时间
- 显示 TimePicker
- 关闭另一个 TimePicker(避免同时显示两个)
四、时间选择器的状态管理
4.1 状态变量的设计
在时间选择器的实现中,我们需要管理多个状态变量:
@State bedTime: string = '23:00'; // 就寝时间(显示用)
@State wakeTime: string = '07:00'; // 起床时间(显示用)
@State bedTimePicker: Date = new Date(); // 就寝时间选择器的 Date
@State wakeTimePicker: Date = new Date(); // 起床时间选择器的 Date
@State showBedTimePicker: boolean = false; // 是否显示就寝时间选择器
@State showWakeTimePicker: boolean = false; // 是否显示起床时间选择器
@State sleepDuration: number = 480; // 睡眠时长(分钟)
为什么需要两套时间变量?
bedTime(字符串) bedTimePicker(Date)
↓ ↓
用于显示和存储 用于 TimePicker 组件
"23:00" Date 对象
原因:
- 显示需要字符串:UI 上显示 “23:00” 比显示 Date 对象更直观
- TimePicker 需要 Date:TimePicker 组件只接受 Date 对象
- 存储需要字符串:保存到数据库时,字符串更方便
4.2 显示与隐藏的控制
单个选择器的控制:
@State showPicker: boolean = false;
// 显示选择器
Button('选择时间')
.onClick(() => {
this.showPicker = true;
})
// 隐藏选择器
if (this.showPicker) {
Column() {
TimePicker({ selected: this.pickerTime })
Button('确定')
.onClick(() => {
this.showPicker = false; // 隐藏
})
}
}
多个选择器的互斥控制:
当页面上有多个时间选择器时(如就寝时间和起床时间),需要确保同时只显示一个。
@State showBedTimePicker: boolean = false;
@State showWakeTimePicker: boolean = false;
// 显示就寝时间选择器
Button('选择就寝时间')
.onClick(() => {
this.showBedTimePicker = true;
this.showWakeTimePicker = false; // 关闭另一个
})
// 显示起床时间选择器
Button('选择起床时间')
.onClick(() => {
this.showWakeTimePicker = true;
this.showBedTimePicker = false; // 关闭另一个
})
4.3 时间数据的同步
时间选择器涉及多个状态变量的同步更新,需要仔细处理。
完整的同步流程:
// 1. 点击快捷按钮
Button('23:00')
.onClick(() => {
this.bedTime = '23:00'; // 更新显示时间
this.bedTimePicker = this.createTimeDate('23:00'); // 同步 Date 对象
this.calculateDuration(); // 重新计算时长
})
// 2. 点击"自定义"按钮
Button('自定义')
.onClick(() => {
this.bedTimePicker = this.createTimeDate(this.bedTime); // 初始化为当前时间
this.showBedTimePicker = true; // 显示选择器
})
// 3. 滑动 TimePicker
TimePicker({ selected: this.bedTimePicker })
.onChange((value: TimePickerResult) => {
// 实时更新 Date 对象
this.bedTimePicker = this.createTimeDate(this.formatPickerTime(value));
})
// 4. 点击"确定"按钮
Button('确定')
.onClick(() => {
this.bedTime = this.formatTime(this.bedTimePicker); // 更新显示时间
this.calculateDuration(); // 重新计算时长
this.showBedTimePicker = false; // 隐藏选择器
})
4.4 初始化时机
时间选择器的初始化时机很重要,需要在正确的时机设置初始值。
错误的初始化:
// ❌ 在组件创建时初始化
@State bedTimePicker: Date = this.createTimeDate(this.bedTime);
// 问题:此时 bedTime 可能还没有值,或者是默认值
正确的初始化:
// ✅ 在打开对话框时初始化
Button('手动记录')
.onClick(() => {
// 此时 bedTime 已经有正确的值
this.bedTimePicker = this.createTimeDate(this.bedTime);
this.wakeTimePicker = this.createTimeDate(this.wakeTime);
this.showBedTimePicker = false;
this.showWakeTimePicker = false;
this.showRecordDialog = true;
})
为什么要在打开对话框时初始化?
- 确保数据正确:此时 bedTime 和 wakeTime 已经从数据库加载
- 避免旧数据:每次打开对话框都重新初始化,不会显示上次的数据
- 重置选择器状态:关闭所有 TimePicker,避免上次的状态残留
五、时间计算与验证
5.1 睡眠时长的计算
睡眠时长的计算需要考虑跨天的情况。
基础计算逻辑:
calculateDuration(): void {
const bedParts: string[] = this.bedTime.split(':');
const wakeParts: string[] = this.wakeTime.split(':');
// 将时间转换为分钟数
let bedMinutes: number = parseInt(bedParts[0]) * 60 + parseInt(bedParts[1]);
let wakeMinutes: number = parseInt(wakeParts[0]) * 60 + parseInt(wakeParts[1]);
// 如果起床时间小于就寝时间,说明跨天了
if (wakeMinutes < bedMinutes) {
wakeMinutes += 24 * 60; // 加上一天的分钟数
}
this.sleepDuration = wakeMinutes - bedMinutes;
}
为什么要判断跨天?
❌ 不判断跨天的问题:
就寝:23:00(1380分钟)
起床:07:00(420分钟)
时长:420 - 1380 = -960 分钟 ❌(负数!)
✅ 判断跨天后:
就寝:23:00(1380分钟)
起床:07:00(420 + 1440 = 1860分钟)
时长:1860 - 1380 = 480 分钟 ✅(8小时)
5.2 时长的格式化显示
计算出的时长是分钟数,需要转换为易读的格式。
格式化方法:
formatDuration(minutes: number): string {
const hours: number = Math.floor(minutes / 60);
const mins: number = minutes % 60;
return `${hours}小时${mins}分钟`;
}
显示示例:
this.formatDuration(480) // "8小时0分钟"
this.formatDuration(495) // "8小时15分钟"
this.formatDuration(90) // "1小时30分钟"
this.formatDuration(45) // "0小时45分钟"
优化版本(更简洁):
formatDuration(minutes: number): string {
const hours: number = Math.floor(minutes / 60);
const mins: number = minutes % 60;
if (mins === 0) {
return `${hours}小时`;
}
return `${hours}小时${mins}分钟`;
}
优化后的显示:
this.formatDuration(480) // "8小时"(更简洁)
this.formatDuration(495) // "8小时15分钟"
this.formatDuration(90) // "1小时30分钟"
this.formatDuration(45) // "0小时45分钟"
5.3 时间的验证
在实际应用中,需要对用户输入的时间进行验证。
常见验证场景:
1. 睡眠时长过短:
validateSleepDuration(): boolean {
if (this.sleepDuration < 60) { // 少于1小时
AlertDialog.show({
title: '提示',
message: '睡眠时长过短,建议至少睡1小时',
confirm: {
value: '知道了',
action: () => {}
}
});
return false;
}
return true;
}
2. 睡眠时长过长:
if (this.sleepDuration > 16 * 60) { // 超过16小时
AlertDialog.show({
title: '提示',
message: '睡眠时长过长,请检查时间是否正确',
confirm: {
value: '知道了',
action: () => {}
}
});
return false;
}
3. 时间合理性检查:
validateTimeRange(): boolean {
const bedHour = parseInt(this.bedTime.split(':')[0]);
const wakeHour = parseInt(this.wakeTime.split(':')[0]);
// 就寝时间应该在晚上(18:00-次日6:00)
if (bedHour >= 6 && bedHour < 18) {
AlertDialog.show({
title: '提示',
message: '就寝时间通常在晚上,请检查是否正确',
confirm: {
value: '确定',
action: () => {}
},
cancel: () => {}
});
return false;
}
return true;
}
完整的保存流程:
async saveSleepRecord(): Promise<void> {
// 1. 计算时长
this.calculateDuration();
// 2. 验证时长
if (!this.validateSleepDuration()) {
return;
}
// 3. 验证时间范围
if (!this.validateTimeRange()) {
return;
}
// 4. 保存数据
if (!this.prefService) return;
const today: string = getTodayDateString();
const record: SleepRecord = {
id: Date.now(),
date: today,
bedTime: this.bedTime,
wakeTime: this.wakeTime,
duration: this.sleepDuration,
quality: this.sleepQuality
};
await this.prefService.saveSleepRecord(record);
this.showRecordDialog = false;
}
5.4 实时计算的触发时机
睡眠时长需要在时间变化时实时更新。
触发时机:
// 1. 点击快捷按钮时
Button('23:00')
.onClick(() => {
this.bedTime = '23:00';
this.calculateDuration(); // ← 触发计算
})
// 2. TimePicker 确定时
Button('确定')
.onClick(() => {
this.bedTime = this.formatTime(this.bedTimePicker);
this.calculateDuration(); // ← 触发计算
this.showBedTimePicker = false;
})
// 3. 使用"现在"按钮时
Button('现在')
.onClick(() => {
const nowTime: string = getCurrentTimeString();
this.wakeTime = nowTime;
this.calculateDuration(); // ← 触发计算
})
实时显示效果:
初始状态:
就寝时间:23:00
起床时间:07:00
睡眠时长:8小时0分钟
点击就寝时间 [22:30]:
就寝时间:22:30 ← 变化
起床时间:07:00
睡眠时长:8小时30分钟 ← 自动更新
点击起床时间 [07:30]:
就寝时间:22:30
起床时间:07:30 ← 变化
睡眠时长:9小时0分钟 ← 自动更新
六、对话框中的时间选择器布局
6.1 对话框的基本结构
时间选择器通常放在对话框中,对话框的布局需要精心设计。
代码实现:
build() {
Stack() {
// 底层:主页面内容
Scroll() {
Column() {
// 页面内容
}
}
// 顶层:对话框
if (this.showRecordDialog) {
Column() {
// 遮罩层(半透明黑色)
Column() {
// 对话框内容(白色卡片)
}
.width('85%')
.padding(24)
.backgroundColor(Color.White)
.borderRadius(16)
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0, 0, 0, 0.5)') // 半透明遮罩
.justifyContent(FlexAlign.Center)
}
}
}
6.2 时间选择区域的布局
每个时间选择区域包含:标签、当前时间、快捷按钮、TimePicker。
代码实现:
Column() {
// 1. 标签和当前时间
Row() {
Text('就寝时间')
.fontSize(16)
.fontColor('#333333')
Blank()
Text(this.bedTime)
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#9C27B0')
}
.width('100%')
// 2. 快捷按钮
Row() {
ForEach(['22:00', '22:30', '23:00', '00:00'], (time: string) => {
Text(time)
.fontSize(12)
.fontColor(this.bedTime === time ? Color.White : '#333333')
.backgroundColor(this.bedTime === time ? '#9C27B0' : '#F5F5F5')
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
.borderRadius(8)
.onClick(() => {
this.bedTime = time;
this.bedTimePicker = this.createTimeDate(time);
this.calculateDuration();
})
})
Text('自定义')
.fontSize(12)
.fontColor(this.showBedTimePicker ? Color.White : '#333333')
.backgroundColor(this.showBedTimePicker ? '#9C27B0' : '#F5F5F5')
.padding({ left: 8, right: 8, top: 6, bottom: 6 })
.borderRadius(8)
.onClick(() => {
this.bedTimePicker = this.createTimeDate(this.bedTime);
this.showBedTimePicker = true;
this.showWakeTimePicker = false;
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.margin({ top: 8 })
// 3. TimePicker(条件显示)
if (this.showBedTimePicker) {
Column() {
TimePicker({ selected: this.bedTimePicker })
.onChange((value: TimePickerResult) => {
this.bedTimePicker = this.createTimeDate(this.formatPickerTime(value));
})
Row() {
Button('取消')
.fontSize(14)
.backgroundColor('#F5F5F5')
.fontColor('#333333')
.onClick(() => {
this.showBedTimePicker = false;
})
Button('确定')
.fontSize(14)
.backgroundColor('#9C27B0')
.fontColor(Color.White)
.margin({ left: 8 })
.onClick(() => {
this.bedTime = this.formatTime(this.bedTimePicker);
this.calculateDuration();
this.showBedTimePicker = false;
})
}
.width('100%')
.justifyContent(FlexAlign.End)
.margin({ top: 8 })
}
.width('100%')
.padding(12)
.backgroundColor('#F9F9F9')
.borderRadius(8)
.margin({ top: 8 })
}
}
6.3 底部按钮的布局
对话框底部通常有两个按钮:取消和确定。
等宽布局:
Row() {
Button('取消')
.layoutWeight(1)
.height(44)
.backgroundColor('#F5F5F5')
.fontColor('#333333')
.onClick(() => {
this.showRecordDialog = false;
})
Button('保存')
.layoutWeight(1)
.height(44)
.backgroundColor('#9C27B0')
.fontColor(Color.White)
.margin({ left: 12 })
.onClick(() => {
this.saveSleepRecord();
})
}
.width('100%')
.margin({ top: 20 })
视觉效果:

按钮样式区分:
| 按钮类型 | 背景色 | 文字色 | 用途 |
|---|---|---|---|
| 取消按钮 | 浅灰色 | 深色 | 次要操作,放弃当前操作 |
| 确定按钮 | 主题色 | 白色 | 主要操作,完成当前任务 |
总结
通过本文的学习,我们深入了解了 TimePicker 组件的使用方法和时间选择器的交互设计。让我们回顾一下核心要点:
TimePicker 组件的核心知识:
- TimePicker 使用 Date 对象作为参数,通过 onChange 回调获取用户选择
- 需要在字符串、Date 对象、TimePickerResult 三种格式之间灵活转换
- 使用 padStart 确保时间格式的规范性(如 “09:05”)
时间选择器的交互设计:
- 快捷按钮 + 自定义选择的组合模式,兼顾效率和灵活性
- 多个选择器需要互斥显示,避免界面混乱
- 实时计算和显示相关数据(如睡眠时长),提升用户体验
时间计算的注意事项:
- 必须处理跨天的情况,避免计算出负数
- 将时间转换为分钟数进行计算,更简单可靠
- 提供友好的时长显示格式(如 “8小时30分钟”)
时间选择器是移动应用中非常常见的组件,掌握其设计和实现技巧,可以大大提升应用的用户体验。希望本文能帮助你在实际项目中灵活运用 TimePicker 组件,打造出优雅、易用的时间输入界面。
如果有任何问题或建议,欢迎交流讨论。
更多推荐



所有评论(0)