# 鸿蒙自定义时间选择器 **Repository Path**: wangweihao111111/harmonyos-custom-time-selector ## Basic Information - **Project Name**: 鸿蒙自定义时间选择器 - **Description**: 鸿蒙自定义时间选择器,专为HarmonyOS应用打造的时间选择组件,支持多种样式和功能定制,提升用户体验。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-09-12 - **Last Updated**: 2025-09-13 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 鸿蒙自定义组件(自定义时间选择器) gh-md-toc README.md > README_toc.md --- # 前言 鸿蒙官方提供了大量组件,使用起来很方便,但是在开发时为了统一样式自定义样式是必不可少的,今天在开发时遇到了需要自定义组件的时间选择器,因为博主使用自定义组件比较少所以不太熟练,在查阅官方文档中废了不少时间,所有编写了这个博客来记录一下如何使用和使用中踩到的坑,希望可以帮助到你 **实现效果图:** ![输入图片说明](https://foruda.gitee.com/images/1757693591060928958/c6c6b786_15636452.png "Snipaste_2025-09-12_23-24-57.png") ```mermaid flowchart TD A[初始化 TimeRangeDialog] --> B[初始化状态变量(currentIndex、currentnuber 等)] A --> C[初始化时间相关状态(startTime、endTime 等)] A --> D[初始化控制器(swiperController、controller)] A --> E[构建界面] E --> F[构建标题栏] F --> F1[显示“选择预约时段”标题] F --> F2[关闭按钮,点击关闭对话框] E --> G[构建时间选择区域] G --> G1[“开始时间”“结束时间”按钮,点击切换滑动视图] G --> G2[滑动视图(Swiper),包含开始、结束时间界面] G2 --> G2a[滑动时更新 currentnuber] G --> G3[选项卡(Tabs)与时间选择器(TimePicker)] G3 --> G3a[选项卡切换时更新 currentIndex,同步日期到 startTime/endTime] G3 --> G3b[TimePicker 选择时间时,更新 startTime/endTime 的时分] E --> H[构建底部按钮区域] H --> H1["确定按钮,点击触发验证"] H1 --> I{验证时间范围} I -->|结束时间 <= 开始时间| J[弹出“结束时间必须晚于开始时间”提示] I -->|结束时间 > 开始时间| K[调用 confirm 回调,传递 startTime 和 endTime] K --> L[关闭对话框] ``` ## 流程图讲解 # 一、初始化阶段:组件启动的基础准备 流程起点为 **进入 TimeRangeDialog 组件**,首先执行 **数据与控制器初始化(init_data)**,这是组件功能正常运行的前提,具体包含 4 个关键子步骤: 1. **控制器初始化(init_controller)**:创建 `SwiperController`,用于后续控制“开始时间/结束时间”滑动视图的切换(如 `showNext()`、`showPrevious()` 方法)。 2. **状态变量初始化(init_state)**:定义两个核心状态变量: - `currentIndex`:默认值 0,用于标记 Tabs 组件(日期选择)当前选中的索引(0 = 今天、1 = 明天、2 = 后天)。 - `currentnuber`:默认值 0,用于标记 Swiper 组件(时间视图)当前显示的页面(0 = 开始时间视图、1 = 结束时间视图)。 3. **时间状态初始化(init_time)**:预设 4 个时间变量,为用户提供合理初始值: - `startTime`:默认当前时间,记录用户最终选择的开始时间。 - `endTime`:默认当前时间 + 1 小时,记录用户最终选择的结束时间。 - `oneTomorrow`:当前时间 + 1 天,对应“明天” Tab 的日期。 - `dayAfterTomorrow`:当前时间 + 2 天,对应“后天” Tab 的日期(显示为具体“X 月 X 日”文本)。 4. **回调初始化(init_callback)**:定义 `confirm` 回调函数,用于将用户最终选择的 `startTime` 和 `endTime` 传递给父组件,实现数据回传。 --- # 二、UI 构建阶段:分层搭建交互界面 初始化完成后进入 **整体 UI 构建(build_ui)**,采用鸿蒙常用的 `Column` 纵向布局,将界面分为“标题栏”“时间选择区域”“底部按钮区” 3 个核心模块: ## 1. 标题栏模块(build_title):基础导航与关闭功能 - **标题显示(show_title_text)**:展示“选择预约时段”文本,配置字体大小 18、中等粗细、颜色 #18181B,并居中对齐。 - **关闭按钮(build_close_btn)**:使用透明背景按钮包裹“删除图标”(`homedelete`),点击后调用 `controller.close()` 直接关闭对话框(`close_dialog`)。 ## 2. 时间选择区域(build_time_area):核心交互区,分 3 层设计 该区域通过“按钮切换 + 滑动视图 + 日期 Tabs + 时间选择器”实现功能: ### (1)顶层:时间视图切换按钮(build_time_btn_row) 采用 `Row` 均匀布局,包含 3 个元素: - **开始时间按钮(build_start_btn)**:显示“开始时间”标签(蓝色背景、圆角样式)和当前 `startTime`(如“3 月 20 日 14:30”),点击后触发 `btn_click_start`:将 `currentnuber` 设为 0,并调用 `swiperController.showNext()` 切换到开始时间视图。 - **分隔箭头(show_arrow)**:使用“右半箭头图标”(`homeRight_half_arrow`)分隔两个按钮。 - **结束时间按钮(build_end_btn)**:显示 `endTime`,点击后触发 `btn_click_end`:将 `currentnuber` 设为 1,调用 `swiperController.showPrevious()` 切换到结束时间视图。 ### (2)中层:Swiper 滑动视图(build_swiper) 绑定初始化的 `SwiperController`: - **滑动页内容**:两个页面均通过 `timeSelection()` 方法构建(参数 `id` 用于区分开始/结束)。 - **滑动响应(swiper_change)**:用户手动滑动时触发 `onChange` 事件,更新 `currentnuber` 为当前索引。 - **指示器(set_swiper_indicator)**:使用 `DotIndicator` 显示位置(选中/未选中状态均为白色)。 **滑动效果:** ![输入图片说明](https://foruda.gitee.com/images/1757693628560539823/9096adbb_15636452.png "Snipaste_2025-09-12_23-28-02.png") ### (3)底层:日期 Tabs 与时间选择器(build_time_selection) 通过 `Tabs` 纵向布局(`vertical=true`)实现: - **Tabs 基础配置(tabs_vertical)**:设置 Tab 栏在左侧(`barPosition=Start`)、高度 250、宽度 80、背景色 #f0f3f8。 - **3 个日期 Tab**: - `tab_today`(今天):文本为“今天”,内容调用 `timeSelectionutensil()` 构建时间选择器。 - `tab_tomorrow`(明天):文本为“明天”,内容同上。 - `tab_day_after`(后天):文本为 `dayAfterTomorrow` 的“X 月 X 日”格式,内容同上。 ![输入图片说明](https://foruda.gitee.com/images/1757693805595059280/1f519172_15636452.png "Snipaste_2025-09-12_23-29-52.png") - **Tab 样式(build_tab_name)**:根据 `currentIndex` 切换背景色(选中为 #ffffff,未选中为透明)。 - **Tab 切换响应(tabs_change)**:用户点击 Tab 时触发 `onChange` 事件,更新 `currentIndex`,并进入 `update_time_by_tab` 逻辑: - 若 `currentIndex=0`(今天):调用 `copyDateTime(startDate, 布尔值)`,赋值给 `startTime` 或 `endTime`(基于 `currentnuber`)。 - 若 `currentIndex=1`(明天):使用 `oneTomorrow` 赋值。 - 若 `currentIndex=2`(后天):使用 `dayAfterTomorrow` 赋值。 - **copyDateTime 方法(copy_date_time)**:复制源日期的年、月、日、时、分到目标时间,确保时间修改独立性。 ### (4)时间选择器(build_time_picker) 每个 Tab 内容区通过 `timeSelectionutensil()` 构建: - **基础配置(time_picker_config)**:默认选中时间为 `startDate`(当前时间),启用 24 小时制(`useMilitaryTime=true`)。 - **选择响应(time_picker_change)**:用户调整时间时触发 `onChange` 事件: - `currentnuber=0`:更新 `startTime` 的小时(`setHours`)和分钟(`setMinutes`)。 - `currentnuber=1`:更新 `endTime` 的小时和分钟。 ![输入图片说明](https://foruda.gitee.com/images/1757693785667060747/4483d719_15636452.png "Snipaste_2025-09-12_23-30-26.png") ## 3. 底部按钮区(build_bottom_btn):确认选择与时间验证 采用 `Row` 布局: - **按钮构建(build_confirm_btn)**:复用自定义组件 `black_button`,设置宽度 100%、文本“确定”。 - **点击响应(btn_click_confirm)**:点击后执行 `validateTimeRange()` 验证: - **验证失败**:若 `startTime` 时间戳 ≥ `endTime` 时间戳,调用 `promptAction.showToast()` 弹出提示“结束时间必须晚于开始时间”(时长 2000ms)。 - **验证成功**:调用 `confirm` 回调函数传递数据,随后调用 `controller.close()` 关闭对话框(`close_after_confirm`)。 **验证成功:** ![输入图片说明](https://foruda.gitee.com/images/1757693693336142592/41c71287_15636452.png "Snipaste_2025-09-12_23-31-01.png") **验证失败:** ![输入图片说明](https://foruda.gitee.com/images/1757693711456261516/8229e06c_15636452.png "Snipaste_2025-09-12_23-31-52.png") --- # 三、核心逻辑总结:状态同步与交互闭环 整个流程的核心是“状态变量驱动视图,视图交互更新状态”,关键闭环包括: 1.**视图切换闭环**:`currentnuber` 关联 Swiper 视图,通过按钮点击或手动滑动更新状态。 2. **日期选择闭环**:`currentIndex` 关联 Tabs 选中项,切换时通过 `copyDateTime` 同步更新日期。 3. **时间验证闭环**:确认前强制验证时间范围,失败时提示修正,成功后才回传数据并关闭组件。 1. 初始化 TimeRangeDialog # 四、TimeRangeDialog(时间范围选择弹窗)使用步骤 ## 一、前置准备:确认依赖与环境 在使用组件前,需确保项目环境和依赖满足以下条件,避免出现引用错误: - **API 版本适配**:组件基于鸿蒙 ArkTS 开发,需确保项目的 `minSdkVersion` 支持 `@ohos.promptAction`(弹窗提示)、`CustomDialog`(自定义弹窗)、`Swiper`(滑动容器)等组件,建议使用 API Version 9 及以上。 - **依赖组件引入**: - 组件依赖 `@ohos.promptAction`(用于时间校验失败的 Toast 提示),无需额外安装,直接在页面头部导入即可。 - 组件依赖自定义按钮组件 `black_button`(路径为 `./ButtonuUI`),需确保项目中存在该组件文件,且组件接收 `width1`(宽度)和 `text`(按钮文本)两个参数。 - **资源文件确认**:组件中引用了两张图片资源(`app.media.homedelete` 关闭图标、`app.media.homeRight_half_arrow` 箭头图标),需在项目的 `main_pages.json` 对应的资源目录中添加这两张图片,或替换为项目中已有的同类资源。 ## 二、步骤1:导入 TimeRangeDialog 组件 在需要使用“时间范围选择弹窗”的页面(如预约页面)中,首先导入 `TimeRangeDialog` 组件和 `promptAction`(若页面未导入),代码如下: ```typescript // 导入弹窗提示工具(用于时间校验失败提示) import promptAction from '@ohos.promptAction'; // 导入自定义时间范围弹窗组件(路径需与组件实际存放路径一致) import { TimeRangeDialog } from './TimeRangeDialog'; // 假设 TimeRangeDialog 存放在当前页面同级目录 ``` ## 三、步骤2:创建 CustomDialogController 实例 通过 `CustomDialogController` 控制 `TimeRangeDialog` 的显示/隐藏、弹窗样式(位置、背景、圆角等),并绑定“确认选择”后的回调函数。 ### 核心代码示例: 在页面的结构体(`struct`)中定义 `dialogController` 变量,作为弹窗的控制器: ```typescript @Entry @Component struct ReservationPage { // 示例:预约页面 // 1. 定义弹窗控制器,配置弹窗样式与回调 dialogController: CustomDialogController = new CustomDialogController({ // 绑定 TimeRangeDialog 组件,传入 confirm 回调(核心:接收选择的开始/结束时间) builder: TimeRangeDialog({ confirm: (startTime: Date, endTime: Date) => { // 这里是“用户点击确定按钮后”的逻辑,startTime 和 endTime 为用户选择的时间 this.handleConfirm(startTime, endTime); } }), // 弹窗位置:底部弹出(符合常见选择器交互) alignment: DialogAlignment.Bottom, // 是否允许点击弹窗外部关闭:false(需用户主动点击关闭按钮或确定按钮) autoCancel: false, // 弹窗背景蒙版颜色:半透明黑色(遮罩页面,突出弹窗) maskColor: 'rgba(0, 0, 0, 0.3)', // 弹窗宽度:占满屏幕宽度 width: '100%', // 弹窗圆角:仅顶部左右圆角(底部直角,贴合底部弹窗样式) cornerRadius: { topLeft: 10, topRight: 10, bottomLeft: 0, bottomRight: 0 } }); // 2. 页面构建(示例:添加“打开时间选择弹窗”的按钮) build() { Column({ space: 20 }) { // 触发弹窗的按钮(可根据实际页面样式调整) Button('选择预约时间') .width(200) .height(45) .backgroundColor('#3092f3') .fontColor(Color.White) .borderRadius(8) .onClick(() => { // 点击按钮,打开时间选择弹窗 this.dialogController.open(); }); // 其他页面内容... } .width('100%') .height('100%') .justifyContent(FlexAlign.Center) } // 3. 定义“确认选择”的回调处理函数(用户点击弹窗“确定”后执行) private handleConfirm(startTime: Date, endTime: Date) { // 示例1:打印选择的时间(便于调试) console.log(`用户选择的开始时间:${startTime.toLocaleString()}`); console.log(`用户选择的结束时间:${endTime.toLocaleString()}`); // 示例2:后续业务逻辑(如调用“预约电桩”接口) // this.callReservationApi(startTime, endTime); // 需自行实现接口调用逻辑 } } ``` ## 四、步骤3:使用弹窗进行时间选择(用户操作流程) 用户点击“选择预约时间”按钮后,弹窗会从页面底部弹出,用户可按以下流程完成时间选择: ### 1. 切换“开始时间/结束时间”选择页 弹窗中部通过 **Swiper(滑动容器)** 分为两个页面: - 左侧:开始时间选择页(默认显示) - 右侧:结束时间选择页 - 切换方式: - 点击弹窗中部的“开始时间”或“结束时间”按钮,自动滑动到对应页面; - 直接用手指左右滑动弹窗中部区域,切换页面。 ### 2. 选择日期(今天/明天/后天) 弹窗时间选择区顶部有 **Tabs(标签页)**,提供3个日期选项: - Tab1:今天(默认选中) - Tab2:明天 - Tab3:后天(显示为“X月X日”格式,自动计算当前日期后2天) - 操作:点击对应 Tab,即可切换到目标日期(切换后时间选择器会同步更新为该日期)。 ### 3. 选择具体时间(小时/分钟) 日期切换后,下方会显示 **TimePicker(时间选择器)**,支持: - 24小时制(组件已通过 `useMilitaryTime(true)` 开启); - 滑动选择“小时”和“分钟”(选择后实时更新弹窗中部的时间显示)。 ### 4. 确认/取消选择 - **确认选择**:点击弹窗底部的“确定”按钮,组件会先校验时间有效性(结束时间必须晚于开始时间): - 校验通过:关闭弹窗,并通过 `confirm` 回调将选择的 `startTime`(开始时间)和 `endTime`(结束时间)传递给页面; - 校验失败:不关闭弹窗,弹出 Toast 提示“结束时间必须晚于开始时间”。 - **取消选择**:点击弹窗顶部右侧的“关闭图标”(`homedelete` 图片),直接关闭弹窗,不触发任何回调。 ## 五、关键注意事项 1. **时间格式处理**:`confirm` 回调返回的 `startTime` 和 `endTime` 是 `Date` 类型,若需传递给接口(如转为“yyyy-MM-dd HH:mm”字符串),需自行处理格式,示例代码: ```typescript private formatDate(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从0开始,补0为两位数 const day = String(date.getDate()).padStart(2, '0'); const hour = String(date.getHours()).padStart(2, '0'); const minute = String(date.getMinutes()).padStart(2, '0'); return `${year}-${month}-${day} ${hour}:${minute}`; } ``` 2. **默认时间配置**:组件默认“结束时间为当前时间+1小时”,若需修改默认时间(如默认结束时间为开始时间+2小时),可调整 `TimeRangeDialog` 中 `endDate` 的初始化代码: ```typescript // 原代码:默认结束时间=当前时间+1小时 @State endDate: Date = new Date(Date.now() + 3600000); // 修改为:默认结束时间=当前时间+2小时(7200000毫秒) @State endDate: Date = new Date(Date.now() + 7200000); ``` 3. **样式自定义**:若需修改弹窗颜色、字体大小、按钮样式等,可直接修改 `TimeRangeDialog` 组件内部的样式代码(如标题字体颜色 `#18181B`、按钮背景色 `#3092f3` 等)。 源码拉取下来即可使用