一、引言
在鸿蒙(HarmonyOS)应用开发中,选择器组件是用户选择特定数据的核心UI元素。日期选择器用于日程安排,时间选择器用于闹钟设置,城市多级联动选择器用于地址填写。这些组件需支持范围限制、自定义格式、多级联动和主题适配。本文将系统讲解鸿蒙原生选择器的实现原理、代码封装及跨设备适配方案,提供可直接集成的完整代码。
二、技术背景
1. 鸿蒙UI框架核心组件
|
|
|
|
|
|
ohos.agp.components.DatePicker
|
支持年/月/日选择,可设置最小/最大日期,自定义日期格式
|
|
|
ohos.agp.components.TimePicker
|
支持时/分/秒选择,12/24小时制切换,时间间隔设置
|
|
|
|
多级联动(省-市-区),动态数据加载,搜索过滤功能
|
|
|
ohos.agp.components.Picker
|
|
2. 关键特性对比
三、应用场景
|
|
|
|
|
|
|
|
|
设置minDate=today,maxDate=today+365天,监听日期变化计算住宿天数
|
|
|
|
|
设置startHour=7, endHour=22, minuteStep=15
|
|
|
|
|
|
|
|
|
|
|
四、核心原理与流程图
1. 日期/时间选择器原理
graph TD
A[用户打开选择器] --> B[初始化滚轮数据]
B --> C[年滚轮:1900-2100]
B --> D[月滚轮:1-12]
B --> E[日滚轮:动态计算当月天数]
C --> F[用户滚动选择]
D --> F
E --> F
F --> G[组合日期值]
G --> H[触发onChange回调]
2. 城市多级联动原理
graph TD
A[用户打开选择器] --> B[加载省级数据]
B --> C[用户选择省份]
C --> D[加载该省市级数据]
D --> E[用户选择城市]
E --> F[加载该市区级数据]
F --> G[用户选择区县]
G --> H[组合完整地址]
H --> I[触发onChange回调]
I --> J[支持搜索过滤]
五、核心特性
-
范围限制:日期/时间设置最小/最大值(如不能选择过去日期)
-
自定义格式:支持多种显示格式(yyyy-MM-dd、MM/dd/yyyy等)
-
联动机制:多级数据自动关联(年→月→日,省→市→区)
-
-
事件回调:提供
onAccept/onCancel事件监听选择结果
六、环境准备
1. 开发环境
-
-
HarmonyOS SDK:API 9+(支持ArkUI声明式开发)
-
设备:真机/模拟器(Phone/Tablet/智慧屏)
2. 项目配置
-
{
"module": {
"reqPermissions": [
{"name": "ohos.permission.INTERNET"} // 如需联网加载城市数据
]
}
}
-
添加城市数据文件
city-data.json到resources/rawfile/目录
七、详细代码实现
以下分日期选择器、时间选择器、城市多级联动三个场景实现完整功能。
场景1:日期选择器封装(DatePickerComponent)
功能:支持范围限制、自定义格式、最小/最大日期设置。
1. 组件代码(DatePickerComponent.ets)
// 日期选择器组件
@Component
export struct DatePickerComponent {
@Link selectedDate: Date // 双向绑定的日期值
@Prop minDate: Date = new Date('1900-01-01') // 最小日期
@Prop maxDate: Date = new Date('2100-12-31') // 最大日期
@Prop format: string = 'yyyy-MM-dd' // 日期格式
private isShow: boolean = false // 控制对话框显示
build() {
Column() {
Button('选择日期')
.onClick(() => this.isShow = true)
Text(this.formatDate(this.selectedDate))
.fontSize(18)
.margin(10)
}
.bindPopup(this.isShow, {
builder: this.buildDatePickerDialog.bind(this),
placement: Placement.Bottom,
maskColor: '#80000000'
})
}
// 构建日期选择对话框
private buildDatePickerDialog() {
DatePickerDialog.show({
start: this.minDate,
end: this.maxDate,
selected: this.selectedDate,
lunar: false, // 公历模式
onAccept: (value: Date) => {
this.selectedDate = value
this.isShow = false
},
onCancel: () => this.isShow = false
})
}
// 格式化日期
private formatDate(date: Date): string {
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
switch(this.format) {
case 'MM/dd/yyyy':
return `${month}/${day}/${year}`
case 'dd-MM-yyyy':
return `${day}-${month}-${year}`
default: // yyyy-MM-dd
return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`
}
}
}
场景2:时间选择器封装(TimePickerComponent)
功能:支持12/24小时制、时间间隔设置、AM/PM指示。
1. 组件代码(TimePickerComponent.ets)
// 时间选择器组件
@Component
export struct TimePickerComponent {
@Link selectedTime: Date // 双向绑定的时间值
@Prop use24HourFormat: boolean = true // 是否24小时制
@Prop minuteStep: number = 1 // 分钟间隔
@Prop startHour: number = 0 // 起始小时
@Prop endHour: number = 23 // 结束小时
private isShow: boolean = false
build() {
Column() {
Button('选择时间')
.onClick(() => this.isShow = true)
Text(this.formatTime(this.selectedTime))
.fontSize(18)
.margin(10)
}
.bindPopup(this.isShow, {
builder: this.buildTimePickerDialog.bind(this),
placement: Placement.Bottom
})
}
// 构建时间选择对话框
private buildTimePickerDialog() {
TimePickerDialog.show({
hour: this.selectedTime.getHours(),
minute: this.selectedTime.getMinutes(),
useMilitaryTime: this.use24HourFormat,
minuteStep: this.minuteStep,
onAccept: (value: TimePickerResult) => {
this.selectedTime.setHours(value.hour, value.minute)
this.isShow = false
},
onCancel: () => this.isShow = false
})
}
// 格式化时间
private formatTime(date: Date): string {
let hours = date.getHours()
const minutes = date.getMinutes()
const ampm = hours >= 12 ? 'PM' : 'AM'
if (!this.use24HourFormat) {
hours = hours % 12 || 12
return `${hours}:${minutes.toString().padStart(2, '0')} ${ampm}`
}
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`
}
}
场景3:城市多级联动封装(CityPickerComponent)
功能:支持省-市-区三级联动、搜索过滤、最近选择记录。
1. 城市数据模型(CityModel.ts)
// 城市数据模型
export interface District {
id: string
name: string
}
export interface City {
id: string
name: string
districts: District[]
}
export interface Province {
id: string
name: string
cities: City[]
}
2. 组件代码(CityPickerComponent.ets)
// 城市选择器组件
@Component
export struct CityPickerComponent {
@Link @Watch('onCityChange') selectedCity: string = ''
@State provinces: Province[] = []
@State cities: City[] = []
@State districts: District[] = []
@State selectedProvince: string = ''
@State selectedCityId: string = ''
@State searchKeyword: string = ''
@State recentCities: string[] = []
private isShow: boolean = false
// 加载城市数据
async loadCityData() {
try {
const context = getContext(this)
const resourceMgr = context.resourceManager
const rawFile = await resourceMgr.getRawFileContent('city-data.json')
const jsonStr = String.fromCharCode.apply(null, rawFile)
this.provinces = JSON.parse(jsonStr) as Province[]
this.cities = this.provinces[0]?.cities || []
this.districts = this.cities[0]?.districts || []
} catch (error) {
console.error('加载城市数据失败:', error)
}
}
build() {
Column() {
Button('选择城市')
.onClick(() => {
this.loadCityData()
this.isShow = true
})
Text(this.selectedCity || '未选择')
.fontSize(18)
.margin(10)
}
.bindPopup(this.isShow, {
builder: this.buildCityPickerDialog.bind(this),
placement: Placement.Bottom
})
}
// 构建城市选择对话框
private buildCityPickerDialog() {
Dialog.show({
title: '选择城市',
content: this.buildCityPickerContent(),
buttons: [
{ text: '取消', action: () => this.isShow = false },
{ text: '确定', action: () => {
this.selectedCity = `${this.selectedProvince} · ${this.cities.find(c => c.id === this.selectedCityId)?.name} · ${this.districts[0]?.name || ''}`
this.addToRecentCities(this.selectedCity)
this.isShow = false
}}
]
})
}
// 构建城市选择器内容
private buildCityPickerContent() {
return Column() {
// 搜索框
Search({ placeholder: '搜索城市...' })
.onChange((value: string) => this.searchKeyword = value)
.margin(10)
// 最近选择
if (this.recentCities.length > 0) {
Text('最近选择').fontSize(16).margin(5)
Scroll() {
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.recentCities, (city: string) => {
Button(city)
.onClick(() => {
this.selectedCity = city
this.isShow = false
})
.margin(5)
})
}
}.margin(5)
}
// 三级联动选择器
Row() {
// 省份选择
Picker({ range: this.provinces.map(p => p.name) })
.value(this.getProvinceIndex())
.onChange((index: number) => {
this.selectedProvince = this.provinces[index].name
this.cities = this.provinces[index].cities
this.districts = this.cities[0]?.districts || []
})
.layoutWeight(1)
// 城市选择
Picker({ range: this.cities.map(c => c.name) })
.value(this.getCityIndex())
.onChange((index: number) => {
this.selectedCityId = this.cities[index].id
this.districts = this.cities[index].districts
})
.layoutWeight(1)
// 区县选择
Picker({ range: this.districts.map(d => d.name) })
.layoutWeight(1)
}.margin(10)
}
}
// 辅助方法:获取省份索引
private getProvinceIndex(): number {
return this.provinces.findIndex(p => p.name === this.selectedProvince)
}
// 辅助方法:获取城市索引
private getCityIndex(): number {
return this.cities.findIndex(c => c.id === this.selectedCityId)
}
// 添加到最近选择
private addToRecentCities(city: string) {
if (!this.recentCities.includes(city)) {
this.recentCities.unshift(city)
if (this.recentCities.length > 3) {
this.recentCities.pop()
}
}
}
// 城市变化响应
private onCityChange() {
console.log('城市已更新:', this.selectedCity)
}
}
场景4:综合应用示例(ReservationPage)
1. 页面代码(ReservationPage.ets)
import { DatePickerComponent } from '../components/DatePickerComponent'
import { CityPickerComponent } from '../components/CityPickerComponent'
@Entry
@Component
struct ReservationPage {
@State checkInDate: Date = new Date()
@State checkOutDate: Date = new Date(Date.now() + 86400000) // 明天
@State destination: string = ''
@State guestCount: number = 2
build() {
Column({ space: 20 }) {
// 标题
Text('酒店预订')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ top: 20 })
// 目的地选择
CityPickerComponent({ selectedCity: $destination })
.margin(10)
// 入住日期
DatePickerComponent({
selectedDate: $checkInDate,
minDate: new Date(),
maxDate: new Date(Date.now() + 365 * 86400000)
})
.margin(10)
// 离店日期
DatePickerComponent({
selectedDate: $checkOutDate,
minDate: new Date(this.checkInDate.getTime() + 86400000),
maxDate: new Date(Date.now() + 365 * 86400000)
})
.margin(10)
// 客人数量
StepperComponent({
value: $guestCount,
min: 1,
max: 6,
step: 1
})
.margin(10)
// 提交按钮
Button('查询可用酒店', { type: ButtonType.Normal })
.width('90%')
.height(50)
.backgroundColor('#007DFF')
.onClick(() => this.submitReservation())
.margin(20)
}
.padding(20)
.width('100%')
.alignItems(HorizontalAlign.Center)
}
// 提交预订
private submitReservation() {
// 计算住宿天数
const diffTime = Math.abs(this.checkOutDate.getTime() - this.checkInDate.getTime())
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
AlertDialog.show({
title: '预订信息',
message: `目的地: ${this.destination}\n入住: ${this.formatDate(this.checkInDate)}\n离店: ${this.formatDate(this.checkOutDate)}\n天数: ${diffDays}天\n客人: ${this.guestCount}人`,
confirm: { value: '确定', action: () => {} }
})
}
// 日期格式化辅助方法
private formatDate(date: Date): string {
return `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`
}
}
八、运行结果与测试步骤
1. 预期效果
-
日期选择器:弹出滚轮选择界面,选择后显示为"yyyy-MM-dd"格式
-
时间选择器:支持12/24小时制切换,按步长选择分钟
-
-
综合页面:日期范围自动校验(离店≥入住),城市选择后显示完整地址
2. 测试步骤
-
-
安装DevEco Studio 3.1+,创建"Empty Ability"项目(语言选择TS)
-
添加城市数据文件
city-data.json到resources/rawfile/
-
-
九、部署场景
|
|
|
|
|
|
|
|
增大字体和触摸区域(按钮最小48×48dp),支持遥控器操作
|
|
|
|
|
|
|
十、疑难解答
|
|
|
|
|
|
未正确调用DatePickerDialog.show()
|
检查参数格式,确保minDate < maxDate
|
|
|
|
使用resourceManager.getRawFileContent()验证文件读取
|
|
|
|
使用getProvinceIndex()等方法确保索引正确
|
|
|
|
使用系统资源@color/text_color_primary
|
十一、未来展望与技术趋势
1. 趋势
-
AI智能推荐:根据用户历史选择推荐常用城市/时间段
-
-
-
2. 挑战
-
多端一致性:手机/车机/手表等不同形态设备的交互适配
-
-
数据安全:城市数据脱敏处理,遵守GDPR等隐私法规
十二、总结
-
日期/时间选择器:利用原生
DatePickerDialog和TimePickerDialog,重点处理范围限制和格式转换
-
城市多级联动:基于
Picker组件实现三级联动,动态加载数据并支持搜索
-
-
通过本文封装的组件,开发者可快速实现符合鸿蒙设计规范的专业级选择器功能,提升应用交互体验。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
评论(0)