鸿蒙新特性:TextPicker 滚轮选择器——构建三级地址选择器
在移动端表单中,当选项数量众多且具有层级关系时,单选按钮和复选框都不再适用。省市区地址选择、商品规格选择、身高体重选择——这些场景需要一种能容纳大量选项、支持层级联动、操作直观的组件。这就是 TextPicker——滚轮式文本选择器。
HarmonyOS NEXT ArkUI 提供了 TextPicker 组件——一个可滚动的文本选择器,用户通过在滚轮上滑动来选择选项。本文将通过构建一个"省市区三级地址选择器",深入讲解 TextPicker 的 API、多列联动机制和级联数据管理。
关键词:HarmonyOS、ArkUI、TextPicker、滚轮选择器、地址选择、三级联动、级联数据
一、TextPicker 组件 API
1.1 基本用法
TextPicker({ range: ['选项A', '选项B', '选项C'], selected: 0 })
.onChange((value: string | string[], index: number | number[]) => {
// value: 当前选中的文本
// index: 当前选中的索引
console.log('选中:', (value as string), '索引:', (index as number));
})
核心参数与属性:
| 参数/属性 | 类型 | 说明 |
|---|---|---|
range |
string[] | Resource |
数据源数组,TextPicker 展示的选项列表 |
selected |
number |
默认选中的索引(0-based) |
value |
string |
默认选中的值(与 selected 二选一) |
.onChange() |
callback | 选中的值变化时的回调 |
1.2 onChange 回调的类型
TextPicker 的回调签名与其他组件不同——它的 onChange 参数类型是 (value: string | string[], index: number | number[]) => void。这是因为 TextPicker 支持多列模式(通过 CascadePicker 或 MultiPicker),此时 value 和 index 都是数组。
但在单列模式下(如 Demo 中的三个独立 TextPicker),value 始终是 string,index 始终是 number。因此在回调中使用类型断言:
.onChange((value: string | string[], index: number | number[]) => {
this.onProvinceChange(value as string, index as number);
})
这是一个容易忽视的 API 细节——如果你给回调参数标注 (value: string, index: number),编译器会报错,因为形参类型必须兼容 string | string[]。
1.3 动态 range 更新
TextPicker 的 range 是一个构造参数而非属性方法,这意味着你无法直接通过链式调用更新数据源。但当你用 @State 变量驱动 range(通过方法返回值)时,每当 @State 变化,range 会自动重新计算。
Demo 中三个 TextPicker 的 range 都通过方法动态生成:
// 省份 range 固定——直接返回全部省份名
TextPicker({ range: this.getProvinceNames(), selected: this.provinceIndex })
// 城市 range 动态——根据当前选中的省份返回该省的城市
TextPicker({ range: this.getCityNames(), selected: this.cityIndex })
// 区县 range 动态——根据当前选中的城市返回该市的区县
TextPicker({ range: this.getDistrictNames(), selected: this.districtIndex })
当 provinceIndex 变化时,getCityNames() 返回新省份的城市列表,城市 TextPicker 自动更新。同理,当 cityIndex 变化时,getDistrictNames() 返回新城市的区县列表。
1.4 索引越界保护
由于 selected 参数始终使用当前的 cityIndex 和 districtIndex,当上级选项变化时(如切换省份),下级的 selected 可能指向一个不再存在的索引。例如原省份有 3 个城市,cityIndex = 2;新省份只有 2 个城市,cityIndex = 2 就越界了。
解决方案是在上级 onChange 回调中重置下级索引:
onProvinceChange(value: string, index: number): void {
this.provinceIndex = index;
this.cityIndex = 0; // 重置城市索引
this.districtIndex = 0; // 重置区县索引
}
onCityChange(value: string, index: number): void {
this.cityIndex = index;
this.districtIndex = 0; // 重置区县索引
}
每次切换省份,城市和区县都回到第一个选项;每次切换城市,区县回到第一个选项。这种"级联重置"确保了索引用不越界。
二、三级地址选择器的整体设计
2.1 页面架构
AddressPickerPage
├── 标题栏 — "地址选择" + 历史记录数
├── 当前选择预览卡片
│ ├── 完整地址(省 市 区)
│ └── 已保存状态标签
├── 三级滚轮选择区(三个 TextPicker 并排)
│ ├── 省份列
│ ├── 城市列(联动更新)
│ └── 区县列(联动更新)
├── "确认选择"按钮
└── 历史记录列表(含删除按钮)
2.2 数据类设计
Demo 中定义了两个类:
class AddressNode {
name: string;
children: AddressNode[];
constructor(name: string, children: AddressNode[]) {
this.name = name;
this.children = children;
}
}
class SelectedAddress {
province: string;
city: string;
district: string;
constructor(province: string, city: string, district: string) {
this.province = province;
this.city = city;
this.district = district;
}
getFullAddress(): string {
return this.province.concat(' ').concat(this.city).concat(' ').concat(this.district);
}
equals(other: SelectedAddress): boolean {
return this.province === other.province &&
this.city === other.city &&
this.district === other.district;
}
}
AddressNode 是树形数据节点。每个节点有一个 name(名称)和一个 children 数组(子节点)。省份是根节点,城市是子节点,区县是孙节点。区县的 children 为空数组 []。
SelectedAddress 表示用户确认后的完整地址。getFullAddress() 返回空格分隔的完整地址字符串。equals() 用于去重——当用户反复确认同一地址时,只保留一条记录。
2.3 地址数据
Demo 包含 6 个省/直辖市,共计 16 个城市、40+ 个区县,覆盖了中国主要的区域:
| 省份 | 城市 | 区县数 |
|---|---|---|
| 北京市 | 北京市 | 4(朝阳/海淀/西城/东城) |
| 上海市 | 上海市 | 4(浦东/徐汇/静安/黄浦) |
| 广东省 | 广州/深圳/东莞 | 3+3+3 |
| 浙江省 | 杭州/宁波/温州 | 3+3+2 |
| 江苏省 | 南京/苏州/无锡 | 3+3+2 |
| 四川省 | 成都/绵阳 | 4+2 |
直辖市(北京、上海)的数据结构比较特殊——省份和城市同名(“北京市"下的"北京市”)。这种设计真实反映了行政区划的实际情况:直辖市下不设地级市,直接辖区县。
2.4 名称提取方法
三个方法负责从 AddressNode 树中提取当前层级的名称数组:
getProvinceNames() — 总是返回所有省份名:
getProvinceNames(): string[] {
let names: string[] = [];
let data = this.getAddressData();
for (let i = 0; i < data.length; i++) {
names = names.slice().concat(data[i].name);
}
return names;
}
getCityNames() — 返回当前选中省份的城市名:
getCityNames(): string[] {
let names: string[] = [];
let data = this.getAddressData();
let province = data[this.provinceIndex];
if (province) {
for (let i = 0; i < province.children.length; i++) {
names = names.slice().concat(province.children[i].name);
}
}
return names;
}
getDistrictNames() — 返回当前选中城市的区县名:
getDistrictNames(): string[] {
let names: string[] = [];
let data = this.getAddressData();
let province = data[this.provinceIndex];
if (province) {
let city = province.children[this.cityIndex];
if (city) {
for (let i = 0; i < city.children.length; i++) {
names = names.slice().concat(city.children[i].name);
}
}
}
return names;
}
注意所有方法都使用 names.slice().concat(...) 而非 names.push(...)——这是 ArkTS 的不可变数据惯例。虽然 names 是局部变量(非 @State),但保持一致性有助于避免误用。
三、三级联动机制
3.1 联动逻辑
三级联动是地址选择器的核心。Demo 通过 @State 变量链实现联动:
provinceIndex (省份索引)
│
▼ onChange → 更新 cityIndex=0, districtIndex=0
cityIndex (城市索引)
│
▼ onChange → 更新 districtIndex=0
districtIndex (区县索引)
这三个 @State 变量形成了一个简短的依赖链。当上游索引变化时,下游索引自动重置为 0,确保数据一致性。
关键代码:
onProvinceChange(value: string, index: number): void {
this.provinceIndex = index;
this.cityIndex = 0;
this.districtIndex = 0;
this.saved = false;
}
onCityChange(value: string, index: number): void {
this.cityIndex = index;
this.districtIndex = 0;
this.saved = false;
}
onDistrictChange(value: string, index: number): void {
this.districtIndex = index;
this.saved = false;
}
saved = false 的作用:用户切换选项后,之前可能的"已保存"状态失效,预览区回到"请选择省/市/区"的提示状态。
3.2 三列布局
三个 TextPicker 在 Row 中水平并排,每个占等宽:
Row() {
Column() {
Text('省份').fontSize(11).fontColor('#BBBBCC').margin({ bottom: 6 })
TextPicker({ range: this.getProvinceNames(), selected: this.provinceIndex })
.onChange(...)
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('城市').fontSize(11).fontColor('#BBBBCC').margin({ bottom: 6 })
TextPicker({ range: this.getCityNames(), selected: this.cityIndex })
.onChange(...)
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
Column() {
Text('区县').fontSize(11).fontColor('#BBBBCC').margin({ bottom: 6 })
TextPicker({ range: this.getDistrictNames(), selected: this.districtIndex })
.onChange(...)
}
.layoutWeight(1).alignItems(HorizontalAlign.Center)
}
每列包含一个标签(11sp 灰色文字)和一个 TextPicker。.layoutWeight(1) 三等分父容器宽度。每列居中对齐。在典型手机屏幕(360dp)上,每列约 120dp——对 TextPicker 的滚轮来说足够显示中文选项。
四、地址确认与历史记录
4.1 确认选择
用户在三列滚轮中选好省市区后,点击"确认选择"按钮:
confirmAddress(): void {
let addr = this.getCurrentAddress();
// 去重检查
let exists = false;
for (let i = 0; i < this.history.length; i++) {
if (this.history[i].equals(addr)) {
exists = true;
break;
}
}
if (exists) {
promptAction.showToast({ message: '该地址已保存', duration: 1500 });
return;
}
this.history = this.history.slice().concat(addr);
this.saved = true;
promptAction.showToast({ message: '地址已保存', duration: 1500 });
}
流程分四步:
- 调用
getCurrentAddress()获取当前三省市区名称,组装为SelectedAddress - 遍历
history检查是否重复(equals()比较省市区三个字段) - 如果重复,Toast 提示"该地址已保存",不重复添加
- 如果不重复,通过
slice().concat(addr)创建新数组,更新@State history
4.2 预览区状态切换
预览区根据 saved 状态展示不同内容:
Column() {
Text('当前选择').fontSize(11).fontColor('#BBBBCC').margin({ bottom: 6 })
Text(this.saved ?
this.getCurrentAddress().getFullAddress() : '请选择省/市/区')
.fontSize(20)
.fontColor(this.saved ? '#1a1a2e' : '#BBBBCC')
.fontWeight(this.saved ? FontWeight.Bold : FontWeight.Normal)
if (this.saved) {
Text('已保存到历史记录')
.fontSize(11).fontColor('#52C41A').margin({ top: 6 })
}
}
- 未确认时(
saved = false):显示灰色"请选择省/市/区",字号 20sp,常规字重 - 已确认后(
saved = true):显示深色完整地址(如"广东省 深圳市 南山区"),加粗,下方显示绿色"已保存到历史记录"标签
注意:用户切换任何 TextPicker 时,saved 会被重置为 false,预览区回到提示状态。这确保"已保存"标签只在当前地址确实已保存后才显示。
4.3 历史记录列表
if (this.history.length > 0) {
Column() {
Row() {
Text('历史记录').fontSize(14).fontColor('#1a1a2e').fontWeight(FontWeight.Medium)
Blank()
Text('左滑删除').fontSize(11).fontColor('#CCCCDD')
}
.width('100%').margin({ bottom: 10 })
ForEach(this.history, (addr: SelectedAddress, idx: number) => {
Row() {
Column() {
Text(addr.getFullAddress())
.fontSize(14).fontColor('#1a1a2e').fontWeight(FontWeight.Medium)
Text('已保存')
.fontSize(11).fontColor('#CCCCDD').margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start).layoutWeight(1)
Text('删除')
.fontSize(12).fontColor('#FF4D4F')
.padding({ top: 5, bottom: 5, left: 10, right: 10 })
.borderRadius(12)
.backgroundColor('#FFF1F0')
.onClick(() => { this.deleteHistory(idx); })
}
.width('100%').padding({ top: 12, bottom: 12 })
if (idx < this.history.length - 1) {
Divider().height(1).color('#F8F9FA')
}
}, (addr: SelectedAddress, idx: number) => idx.toString())
}
.width('100%').padding(Spacing.LG)
.backgroundColor('#FFFFFF').borderRadius(BorderRadius.LG)
.margin({ ... })
}
每条历史记录显示完整地址 + "已保存"副标签 + 红色"删除"按钮。记录间有浅灰分割线。当 history.length === 0 时,整个历史记录区域不渲染。
4.4 删除历史
deleteHistory(index: number): void {
let newHistory: SelectedAddress[] = [];
for (let i = 0; i < this.history.length; i++) {
if (i !== index) {
newHistory = newHistory.slice().concat(this.history[i]);
}
}
this.history = newHistory;
}
通过遍历跳过指定索引来创建新数组。头部的历史记录计数(“N 条记录”)自动更新。
五、预览区与滚轮区的视觉设计
5.1 预览卡片
预览区使用白色圆角卡片,内边距 20dp。地址文字使用 20sp 字号——比一般正文大不少,让用户清楚地看到当前选择结果。选择前后颜色和字重的对比强化了"已确认"vs"未确认"的状态差异。
5.2 滚轮区域
三个 TextPicker 并排在一个白色圆角卡片中。每列上方有灰色标签标明"省份"“城市”“区县”。TextPicker 本身的滚动交互由系统处理——用户上下滑动滚轮选择选项,选中项居中高亮显示。
三列并排的设计参考了 iOS 原生地址选择器的经典布局——一眼就能看到省市区三个层级,无需额外的步骤切换。
六、交互流程演示
6.1 默认状态
进入页面,三个 TextPicker 分别显示第一个省份(北京市)、第一个城市、第一个区县。预览区显示"请选择省/市/区"。标题栏显示"0 条记录"。
6.2 选择地址
在省份 TextPicker 上向下滑动,选择"广东省"。城市 TextPicker 自动更新为广东省的城市列表(广州/深圳/东莞),区县 TextPicker 自动更新为第一个城市的区县。
在城市 TextPicker 上滑动,选择"深圳市"。区县 TextPicker 自动更新为深圳市的区县列表(南山/福田/罗湖)。
在区县 TextPicker 上滑动,选择"南山区"。预览区仍然显示"请选择省/市/区"(因为尚未确认)。
6.3 确认保存
点击"确认选择"按钮。Toast 提示"地址已保存"。预览区变为"广东省 深圳市 南山区"(深色加粗),下方显示绿色"已保存到历史记录"。标题栏变为"1 条记录"。历史记录区域出现一条记录。
6.4 再次选择
在省份 TextPicker 切换到"浙江省"。城市和区县自动重置为第一个。选中"杭州市"→"西湖区",再次点击"确认选择"。标题栏变为"2 条记录"。历史记录区域出现两条记录。
如果再次确认"广东省 深圳市 南山区",Toast 提示"该地址已保存",不会重复添加。
6.5 删除记录
点击第一条历史记录右侧的红色"删除"按钮。该记录消失。标题栏更新为"1 条记录"。
七、级联选择器的扩展思路
本文构建的三级地址选择器是一个标准模板,可以轻松扩展到其他级联场景:
商品规格选择:品类 → 品牌 → 型号。例如"手机 → Apple → iPhone 16 Pro"。
组织架构选择:公司 → 部门 → 小组。例如"某某集团 → 技术部 → 前端组"。
课程目录选择:学科 → 章节 → 知识点。例如"数学 → 高等数学 → 微积分"。
只需替换 AddressNode 树的数据内容,其余联动逻辑完全不变。
八、总结
本文通过"三级地址选择器"这个实战案例,全面讲解了 ArkUI TextPicker 滚轮选择器的使用方法。核心知识点包括:
- TextPicker 基础 API:
range数据源 +selected默认索引 +onChange回调(注意参数类型string | string[]) - 树形数据结构:
AddressNode递归定义省份→城市→区县的三级关系 - 级联联动机制:上游索引变化 → 重置下游索引 →
range动态重新计算 - 三列并排布局:
Row+layoutWeight(1)三等分 +TextPicker每列独立 - 索引越界保护:
onChange回调中重置下级索引为 0 - 地址确认与去重:
SelectedAddress.equals()防止重复保存 - 历史记录管理:
slice().concat()不可变添加 + 过滤删除 - TypeScript 类型细节:
onChange回调参数必须兼容string | string[]和number | number[]
滚轮选择器是移动端处理大量选项的最佳方案。它比下拉菜单承载更多选项(滚轮可无限滚动),比单选按钮更节省空间。TextPicker 的级联模式让开发者可以构建多层级的选项关系,让用户在几十上百个选项中快速定位目标——这是地址选择、商品筛选、组织导航等场景中不可或缺的交互模式。
更多推荐




所有评论(0)