本篇将围绕一个完整的 HarmonyOS 通讯录应用,从 UI 结构设计、核心功能逻辑、性能优化策略 到 模块化架构设计 进行全方位详细剖析,适合作为经典鸿蒙开发案例参考。
一、项目结构概览
1. 文件组织结构
路径 | 功能描述 |
---|---|
pages/Index.ets | 主页面入口,包含状态管理与事件绑定 |
components/index.ets | UI 组件库,拆分为多个可复用组件 |
constant/index.ets | 静态数据源(联系人列表) |
constant/typs.ets | 数据类型定义 |
utils/index.ets | 工具函数集合 |
二、核心功能详解
通讯录核心列表完整代码
import { getRandomHexColor } from '../utils'
import { IContactContent } from '../constant/typs'
@Component
struct ContactsList {
@State backIconY: number = 100
@State backIconX: number = 100
@Watch('onLisChange')
@Prop contactsData: IContactContent[]
@State betIndexLis: string[] = this.contactsData.map(item => item.initial)
@State selected: number = 0
scroller: Scroller = new Scroller()
onLisChange() {
this.betIndexLis = this.contactsData.map(item => item.initial)
}
build() {
Column() {
Stack({ alignContent: Alignment.BottomEnd }) {
// 联系人列表
Stack({ alignContent: Alignment.End }) {
List({ scroller: this.scroller }) {
// 分组的联系人信息
ForEach(this.contactsData, (item: IContactContent) => {
ListItemGroup({ header: this.itemHead(item.initial), space: 10 }) {
ForEach(item.nameList, (name: string) => {
// 循环渲染分组A的ListItem
ListItem() {
Contacts({ name })
}
})
}
})
}
.sticky(StickyStyle.Header)
.onWillScroll(() => {
const scrollOffset = this.scroller.currentOffset()
const yOffset = scrollOffset.yOffset
this.backIconY = yOffset > 300 ? 0 : 100
this.backIconX = yOffset > 300 ? -10 : 100
})
.onScrollIndex((start: number, end: number, center: number) => {
this.selected = start
})
.height('100%')
}
.height('100%')
// 字母索引
AlphabetIndexer({ arrayValue: this.betIndexLis, selected: $$this.selected })
.position({ top: '20%', right: 0 })
.usingPopup(true)
.onSelect((index: number) => {
this.scroller.scrollToIndex(index)
})
// 返回按钮
Image($r('app.media.toTop'))
.width(35)
.offset({ x: this.backIconX, y: this.backIconY })
.animation({})
.onClick(() => {
this.scroller.scrollTo({ xOffset: 0, yOffset: 0, animation: true })
})
}
.height('100%')
if (this.contactsData.length === 0) {
Text('暂无联系人')
.fontColor(Color.Gray)
.position({ top: '50%', left: '40%' })
}
}
.layoutWeight(1)
}
@Builder
itemHead(text: string) {
// 列表分组的头部组件,对应联系人分组A、B等位置的组件
Text(text)
.fontSize(20)
.backgroundColor('#fff1f3f5')
.width('100%')
.padding(10)
.fontWeight(600)
}
}
1. 联系人展示与分组显示
实现目标
- 按照首字母进行分类。
- 每个分组下展示对应的联系人列表。
- 分组头部固定显示(sticky header)。
技术实现
使用 List
+ ListItemGroup
构建分组列表,并通过itemHead
构造器自定义头部:
ForEach(this.contactsData, (item: IContactContent) => {
ListItemGroup({ header: this.itemHead(item.initial), space: 10 }) {
ForEach(item.nameList, (name: string) => {
ListItem() {
Contacts({ name })
}
})
})
})
自定义头部组件:
@Builder itemHead(text: string) {
Text(text)
.fontSize(20)
.backgroundColor('#fff1f3f5')
.width('100%')
.padding(10)
.fontWeight(600)
}
2. 联系人动态查找(模糊搜索)
实现目标
输入关键字后,仅显示包含该关键字的联系人,同时保留原有首字母分组结构。
核心逻辑分析
在 Index.ets
中定义 filterTargetContactsData(value: string)
方法:
filterTargetContactsData = (value: string): IContactContent[] => {
// 初始化一个新的联系人数据数组,用于存储过滤后的联系人信息
let contactsData_new: IContactContent[] = []
contacts.forEach((item: IContactContent, index: number) => {
item.nameList.forEach((t: string) => {
// 如果名称中包含输入值,则进行后续处理
if (t.includes(value)) {
// 创建当前联系人的深拷贝,用于修改而不影响原始数据
let item_new: IContactContent = JSON.parse(JSON.stringify(item)) as IContactContent
// 设置联系人的排序索引,以便于后续排序
item_new.sort_i = index
// 检查新联系人数据中是否已有相同首字母的联系人
const isHaved = contactsData_new.findIndex(c => c.initial === item.initial)
if (isHaved !== -1) {
// 如果已存在相同首字母的联系人,则将当前名称添加到该联系人的名称列表中
contactsData_new[isHaved].nameList = [...contactsData_new[isHaved].nameList, t]
} else {
// 如果不存在相同首字母的联系人,则将当前名称设置为新联系人的名称列表,并添加到新联系人数据数组中
item_new.nameList = [t]
contactsData_new = [...contactsData_new, item_new]
}
}
})
})
// 返回过滤和重新组织后的联系人数据数组
return contactsData_new
}
关键处理步骤:
- 深拷贝原始数据:避免直接修改原数据。
- 遍历匹配项:对每个名字进行关键字判断。
- 聚合相同分组:确保同一首字母只出现一次。
- 排序更新索引:按原始顺序重新排序,保持 AlphabetIndexer 正确联动。
3. 字母索引联动(AlphabetIndexer)
实现目标
右侧字母导航栏点击时,自动定位到对应分组位置。
技术实现
在ContactsList
中使用 AlphabetIndexer
控件并绑定当前所有首字母:
@State betIndexLis: string[] = this.contactsData.map(item => item.initial)
AlphabetIndexer({
arrayValue: this.betIndexLis,
selected: $$this.selected
})
.position({ top: '20%', right: 0 })
.usingPopup(true)
.onSelect((index: number) => {
this.scroller.scrollToIndex(index)
})
滚动监听同步更新选中项:
.onScrollIndex((start: number, end: number, center: number) => {
this.selected = start
})
4. 输入防抖优化(Debounce)
实现目标
防止频繁触发搜索请求,提升性能体验。
技术实现
父主页面中通过封装好的debounce
防抖函数执行子组件传递的关键字搜索逻辑:
import { debounce } from '../utils'
@Entry
@Component
struct Index {
@State contactsData: IContactContent[] = contacts
// 查找检索目标联系人
filterTargetContactsData = (value: string): IContactContent[] => {...}
// 在组件内提前定义防抖函数
debouncedSearch = debounce((value?: string) => {
if (value) {
// 查找检索关键字联系人
const contactsData_new = this.filterTargetContactsData(value)
// 将分组联系人进行排序 更新排序索引 AlphabetIndexer联动更新UI
contactsData_new.sort((a: IContactContent, b: IContactContent) => a.sort_i! - b.sort_i!)
// 排序后赋值更新UI
this.contactsData = contactsData_new
} else {
// 当检索关键字清空后 列表赋值全部数据
this.contactsData = contacts
}
}, 500)
build() {
Column() {
// 顶部导航栏
TopTitle()
// 搜索栏
SearchBar({
searchInput: (value?: string) => {
this.debouncedSearch(value)
}
})
// 联系人列表
ContactsList({ contactsData: this.contactsData })
}
}
}
子组件绑定至TextInput
检索框的 onChange
事件,将用户输入的关键字通过自定义时间传递给父主页面:
@Component
struct SearchBar {
searchInput = (value?: string) => {}
build() {
// 顶部区域
Stack({ alignContent: Alignment.Start }) {
Image($r('app.media.ic_public_search'))
.width(20)
.fillColor(Color.Gray)
.offset({ x: 10 })
TextInput({ placeholder: '搜索' })
.borderRadius(0)
.backgroundColor(Color.Transparent)
.placeholderFont({ size: 18 })
.padding({ left: 35 })
.border({ width: { bottom: 1 }, color: "#eee" })
.onChange((value: string) => {
this.searchInput(value) // 使用预先定义好的防抖函数
})
}
}
}
关键处理步骤:
- 遵循数据单向流原则:尽量避免在子组件直接对关键数据进行修改,提高可维护性。
- 避免频繁进行数据筛查: 对于频繁操作数据,耗费性能逻辑产生无用结果逻辑,增加函数防抖。
- 子父组件数据传递:子组件通过自定义事件向父组件传递查询关键字,父组件通过防抖对原始数据进行筛查,将筛查最终结果通过自定义参数传递给子组件,并引起子组件UI界面的更新。
5. 返回顶部按钮动画控制
实现目标
当用户滚动一定距离后显示返回顶部按钮,并带有动画效果。
技术实现
使用 onWillScroll
监听滚动偏移量,动态设置按钮位置:
.onWillScroll(() => {
const scrollOffset = this.scroller.currentOffset()
const yOffset = scrollOffset.yOffset
this.backIconY = yOffset > 300 ? 0 : 100
this.backIconX = yOffset > 300 ? -10 : 100
})
按钮绑定动画和点击事件:
Image($r('app.media.toTop'))
.width(35)
.offset({ x: this.backIconX, y: this.backIconY })
.animation({})
.onClick(() => {
this.scroller.scrollTo({ xOffset: 0, yOffset: 0, animation: true })
})
6. 空状态提示
实现目标
当没有联系人或搜索无结果时,显示“暂无联系人”提示。
技术实现
在 build()
中添加条件渲染:
if (this.contactsData.length === 0) {
Text('暂无联系人')
.fontColor(Color.Gray)
.position({ top: '50%', left: '40%' })
}
三、模块化架构设计
1. 组件化设计
组件名 | 功能说明 |
---|---|
TopTitle | 顶部标题栏 |
SearchBar | 搜索输入框 |
ContactsList | 联系人主列表容器 |
Contacts | 单个联系人卡片组件 |
优势:
- 易维护、易扩展
- 可复用性强
- 清晰的父子组件关系
2. 数据模型抽象
定义统一接口 IContactContent
:
interface IContactContent {
initial: string
sort_i?: number
nameList: string[]
}
保证联系人数据结构清晰,便于后续扩展字段。
3. 工具函数封装
封装通用工具函数,提高代码复用性:
// 十六进制颜色生成器
const getRandomHexColor = (): string => {
const randomColor = Math.floor(Math.random() * 0xffffff).toString(16);
return `#${randomColor.padStart(6, '0')}`;
}
// 防抖函数
const debounce = (func: (v?: string) => void, delay: number) => {
let timer: number;
return (v?: string) => {
clearTimeout(timer);
timer = setTimeout(() => {
func(v)
},delay)
}
}
4. 常量集中管理
联系人数据统一存放于constant/index.ets
方便替换和测试。
四、性能优化建议(进阶)
虽然当前已具备良好的基础性能,但仍可进一步优化:
优化点 | 描述 |
---|---|
虚拟滚动 | 对超长联系人列表使用虚拟滚动技术减少 DOM 渲染数量 |
缓存机制 | 对搜索结果缓存,避免重复计算 |
异步加载 | 图片资源使用懒加载策略 |
首屏优化 | 对非首屏组件使用异步加载(如路由懒加载) |
五、总结:
特性 | 描述 |
---|---|
完整功能覆盖 | 包括搜索、分组、索引联动、返回顶部等主流功能 |
高性能交互 | 使用防抖、动画、滚动监听提升用户体验 |
高可维护性架构 | 组件化+模块化设计,便于团队协作与后期扩展 |
良好编码规范 | 接口定义清晰、逻辑分离、命名语义明确 |
适合教学与实战 | 是初学者掌握 ArkTS 和 HarmonyOS 开发的理想入门案例 |
六、附录:部分关键代码引用
filterTargetContactsData
联系人筛选逻辑debounce
:防抖函数封装AlphabetIndexer
:索引联动实现onScrollIndex
:滚动监听同步索引
结语:本案例不仅展示了 HarmonyOS 应用开发的基本流程,还涵盖了现代前端开发中的诸多高级技巧。对于希望深入理解 ArkTS、HarmonyOS 开发框架及模块化设计理念的开发者来说,是一个极具学习价值的经典案例。