> 技术文档 > HarmonyOS 实战:用 List 与 AlphabetIndexer 打造高效城市选择功能

HarmonyOS 实战:用 List 与 AlphabetIndexer 打造高效城市选择功能


HarmonyOS 实战:用 List 与 AlphabetIndexer 打造高效城市选择功能

在移动应用开发中,城市选择功能是很多 App 的必备模块。想象一下,当用户需要从数百个城市中找到自己所在的城市时,如果没有高效的导航方式,体验会有多糟糕。今天我将带大家实现一个 HarmonyOS 平台上的城市选择器,通过 List 与 AlphabetIndexer 的联动,让用户轻松找到目标城市。

HarmonyOS 实战:用 List 与 AlphabetIndexer 打造高效城市选择功能

功能需求分析

我们需要实现的城市选择功能应该包含这些核心特性:

  • 展示历史选择城市,方便用户快速访问

  • 提供热门城市快捷入口

  • 按字母顺序分组展示所有城市

  • 右侧字母索引条,支持点击快速跳转

  • 列表滚动时自动同步索引位置

这种交互模式在通讯录、词典等应用中也非常常见,掌握了这个技巧可以举一反三。

核心组件介绍

实现这个功能我们主要依赖 HarmonyOS 的两个核心组件:

List 组件:作为容器展示大量数据,支持分组展示和滚动控制,通过 Scroller 可以精确控制滚动位置。

AlphabetIndexer 组件:字母索引条,支持自定义字母数组和选中样式,通过 onSelect 事件可以监听用户选择的索引位置。

这两个组件的联动是实现功能的关键,也是最容易出现问题的地方。

实现步骤详解

1. 数据结构定义

首先我们需要定义城市数据的结构,以及存储各类城市数据:

// 定义城市分组数据结构interface BKCityContent {  initial: string // 字母首字母  cityNameList: string\\[] // 该字母下的城市列表}// 组件内部数据定义@State hotCitys: string\\[] = \\[\'北京\', \'上海\', \'广州\', \'深圳\', \'天津\', ...]@State historyCitys: string\\[] = \\[\'北京\', \'上海\', \'广州\', ...]@State cityContentList: BKCityContent\\[] = \\[  { initial: \'A\', cityNameList: \\[\'阿拉善\', \'鞍山\', \'安庆\', ...] },  { initial: \'B\', cityNameList: \\[\'北京\', \'保定\', \'包头\', ...] },  // 其他字母分组...]

2. 索引数组构建

索引数组需要包含特殊分组(历史、热门)和所有城市首字母,我们在页面加载时动态生成:

@State arr: string\\[] = \\[]scroller: Scroller = new Scroller()aboutToAppear() {  // 先添加特殊分组标识  this.arr.push(\"#\", \"🔥\") // #代表历史,🔥代表热门  // 再添加所有城市首字母  for (let index = 0; index < this.cityContentList.length; index++) {  const element = this.cityContentList\\[index];  this.arr.push(element.initial)  }}

3. 列表 UI 构建

使用 List 组件构建主内容区,包含历史城市、热门城市和按字母分组的城市列表:

List({ scroller: this.scroller }) {  // 历史城市分组  ListItemGroup({ header: this.header(\"历史\") }) {  ListItem() {  Flex({ wrap: FlexWrap.Wrap }) {  ForEach(this.historyCitys, (item: string) => {  Text(item)   .width(\"33.33%\")   .margin({ top: 20, bottom: 20 })   .textAlign(TextAlign.Center)  })  }  }  }  // 热门城市分组  ListItemGroup({ header: this.header(\"热门\") }) {  // 结构类似历史城市...  }  // 字母分组城市  ForEach(this.cityContentList, (item: BKCityContent) => {  ListItemGroup({ header: this.header(item.initial) }) {  ForEach(item.cityNameList, (item2: string) => {  ListItem() {  Text(item2)   .fontSize(20)  }  })  }  })}.width(\"100%\").backgroundColor(Color.White)

4. 索引器 UI 构建

在列表右侧添加 AlphabetIndexer 组件作为索引条:

AlphabetIndexer({ arrayValue: this.arr, selected: this.current })  .itemSize(20)  .font({ size: \"20vp\" })  .selectedFont({ size: \"20vp\" })  .height(\"100%\")  .autoCollapse(false)  .onSelect(index => {  this.current = index  // 点击索引时滚动列表  this.scroller.scrollToIndex(index)  })

解决联动核心问题

很多开发者在实现时会遇到一个问题:索引器与列表位置不匹配。这是因为 List 的分组索引和 AlphabetIndexer 的索引需要正确映射。

原代码中的问题在于滚动回调处理不正确,我们需要修正这个逻辑:

// 正确的列表滚动回调处理.onScrollIndex((start) => {  // 计算当前滚动到的分组对应的索引器位置  if (start === 0) {  this.current = 0; // 历史分组对应索引0  } else if (start === 1) {  this.current = 1; // 热门分组对应索引1  } else {  // 字母分组从索引2开始  this.current = start - 2 + 2;   }})

更好的解决方案是创建一个映射数组,明确记录每个索引器项对应的列表分组索引:

// 定义映射关系数组private listIndexMap: number\\[] = \\[]aboutToAppear() {  // 构建索引映射:索引器索引 -> 列表分组索引  this.listIndexMap = \\[0, 1] // 历史在列表第0组,热门在列表第1组  this.cityContentList.forEach((item, index) => {  this.listIndexMap.push(index + 2) // 字母分组从列表第2组开始  })}// 使用映射数组处理滚动.onScrollIndex((start) => {  const index = this.listIndexMap.indexOf(start)  if (index !== -1) {  this.current = index  }})// 索引器选择时也使用映射数组.onSelect((index: number) => {  this.current = index  const listIndex = this.listIndexMap\\[index]  if (listIndex !== undefined) {  this.scroller.scrollToIndex(listIndex, true)  }})

这种映射方式更灵活,即使后续添加新的分组类型也不容易出错。

完整优化代码

结合以上优化点,我们的完整代码应该是这样的(包含 UI 美化和交互优化):

interface BKCityContent {  initial: string  cityNameList: string\\[]}@Component@Entrystruct CitySelector {  @State isShow: boolean = true  @State selectedCity: string = \"\"  @State currentIndex: number = 0    // 城市数据定义...  hotCitys: string\\[] = \\[\'北京\', \'上海\', \'广州\', ...]  historyCitys: string\\[] = \\[\'北京\', \'上海\', ...]  cityContentList: BKCityContent\\[] = \\[/\\* 城市数据 \\*/]    @State indexArray: string\\[] = \\[]  scroller: Scroller = new Scroller()  private listIndexMap: number\\[] = \\[]  aboutToAppear() {  // 构建索引数组和映射关系  this.indexArray = \\[\"#\", \"🔥\"]  this.listIndexMap = \\[0, 1]     this.cityContentList.forEach((item, index) => {  this.indexArray.push(item.initial)  this.listIndexMap.push(index + 2)  })  }  // 标题构建器  @Builder header(title: string) {  Text(title)  .fontWeight(FontWeight.Bold)  .fontColor(\"#666666\")  .fontSize(16)  .padding({ left: 16, top: 12, bottom: 8 })  }  // 城市网格构建器(用于历史和热门城市)  @Builder cityGrid(cities: string\\[]) {  Flex({ wrap: FlexWrap.Wrap }) {  ForEach(cities, (item: string) => {  Text(item)  .padding(12)  .margin(6)  .backgroundColor(\"#F5F5F5\")  .borderRadius(6)  .onClick(() => {   this.selectedCity = item  })  })  }  .padding(10)  }  build() {  Column() {  // 顶部标题栏  Row() {  Text(this.selectedCity ? \\`已选: \\${this.selectedCity}\\` : \"选择城市\")  .fontSize(18)  .fontWeight(FontWeight.Bold)  }  .padding(16)  .width(\"100%\")     // 主内容区  Stack({ alignContent: Alignment.End }) {  // 城市列表  List({ scroller: this.scroller }) {  // 历史城市分组  ListItemGroup({ header: this.header(\"历史\") }) {   ListItem() { this.cityGrid(this.historyCitys) }  }     // 热门城市分组  ListItemGroup({ header: this.header(\"热门\") }) {   ListItem() { this.cityGrid(this.hotCitys) }  }     // 字母分组城市  ForEach(this.cityContentList, (item: BKCityContent) => {   ListItemGroup({ header: this.header(item.initial) }) {  ForEach(item.cityNameList, (city: string) => {   ListItem() {   Text(city)   .padding(16)   .width(\"100%\")   .onClick(() => { this.selectedCity = city })   }  })   }  })  }  .onScrollIndex((start) => {  const index = this.listIndexMap.indexOf(start)  if (index !== -1) {   this.currentIndex = index  }  })     // 字母索引器  AlphabetIndexer({   arrayValue: this.indexArray,   selected: this.currentIndex   })  .itemSize(24)  .selectedFont({ color: \"#007AFF\" })  .height(\"90%\")  .autoCollapse(false)  .onSelect((index: number) => {  this.currentIndex = index  const listIndex = this.listIndexMap\\[index]  if (listIndex !== undefined) {   this.scroller.scrollToIndex(listIndex, true)  }  })  .padding({ right: 8 })  }  .flexGrow(1)  }  .width(\"100%\")  .height(\"100%\")  .backgroundColor(\"#F9F9F9\")  }}

功能扩展建议

基于这个基础功能,你还可以扩展更多实用特性:

  1. 城市搜索功能:添加搜索框,实时过滤城市列表

  2. 选择动画:为城市选择添加过渡动画,提升体验

  3. 定位功能:调用定位 API,自动推荐当前城市

  4. 数据持久化:保存用户的历史选择,下次打开时恢复

  5. 样式主题:支持深色 / 浅色模式切换

总结

通过本文的讲解,我们学习了如何使用 HarmonyOS 的 List 和 AlphabetIndexer 组件实现高效的城市选择功能。核心要点是理解两个组件的工作原理,建立正确的索引映射关系,实现双向联动。

这种列表加索引的模式在很多场景都能应用,掌握后可以显著提升应用中大数据列表的用户体验。希望本文对你有所帮助,如果你有更好的实现方式,欢迎在评论区交流讨论!