HarmonyOS 列表操作实战指南
📚 写给初学者的话:如果你刚开始学习 HarmonyOS 开发,不用担心!这篇文章会用最简单的方式带你掌握列表操作的核心技能。我们会从最基础的概念开始,一步步构建出复杂的列表功能。
📖 开始之前:什么是列表?
🤔 先从生活中的例子说起
在开始学习代码之前,让我们先想想生活中的"列表":
- 📝 购物清单:一行行写着要买的东西
- 📞 手机通讯录:一个个联系人按顺序排列
- 🎵 音乐播放列表:一首首歌曲排成队
在 APP 开发中,列表就是把这些信息在手机屏幕上"一行一行"地展示出来。
📱 列表在 APP 中长什么样?
打开你的手机,你会发现列表无处不在:
- 微信:聊天记录就是一个列表,每条消息占一行
- 淘宝:商品展示就是列表,每个商品占一行或一块区域
- 音乐 APP:歌曲列表、播放列表都是典型的列表
✨ 为什么列表这么重要?
对用户来说:
- 📱 看得清楚:信息整整齐齐,不会眼花缭乱
- 🔍 找得容易:可以快速扫视,找到想要的内容
- ⚡ 用得舒服:符合从上到下的阅读习惯
- 📜 装得下:屏幕放不下的内容可以滑动查看
对开发者来说:
- 🛠️ 开发效率高:HarmonyOS 提供了强大的列表组件
- 🎨 样式灵活:可以做出各种美观的效果
- 🚀 性能优秀:系统自动优化,即使数据很多也很流畅
🔧 HarmonyOS 中的列表技术
🧱 核心组件介绍
想象一下搭积木:我们用几种基本的"积木块"就能搭出各种复杂的列表。
基础积木(组件):
| 组件名称 | 作用 | 生活中的比喻 | 官方文档 |
|---|---|---|---|
| List | 列表容器 | 就像一个大盒子,装下所有的列表内容 | 📚 查看文档 |
| ListItem | 单个列表项 | 盒子里的每一个小格子,放一条信息 | 📚 查看文档 |
| ListItemGroup | 列表分组 | 把相关的小格子归类到一起 | 📚 查看文档 |
📐 组件关系图:
List (大盒子 - 整个列表)
├── ListItem (格子1 - 第一条信息)
├── ListItem (格子2 - 第二条信息)
├── ListItemGroup (分组盒子)
│ ├── ListItem (分组内的格子1)
│ └── ListItem (分组内的格子2)
└── ListItem (格子N - 最后一条信息)🎯 学习路线图
💡 学习建议:建议按照从简单到复杂的顺序学习,每个场景都建议动手实践一遍!
| 难度等级 | 学习内容 | 适合人群 | 预计学习时间 |
|---|---|---|---|
| ⭐ | 多类型列表项 | 刚接触列表开发 | 2-3 小时 |
| ⭐⭐ | Tabs 吸顶效果 | 有基础列表经验 | 1-2 小时 |
| ⭐⭐⭐ | 分组吸顶 | 想做通讯录类应用 | 2-3 小时 |
| ⭐⭐⭐⭐ | 二级联动 | 想做电商分类页面 | 3-4 小时 |
每个场景包含:
- 🎭 场景展示:看看最终要做成什么样子
- 🔍 原理解析:用最简单的话解释技术原理
- 👨💻 代码实战:一步步跟着做,每段代码都有详细注释
- 🎬 效果预览:看看我们的成果
- 💡 扩展思考:举一反三,应用到其他场景
🚀 开始前的准备
环境要求:
- ✅ 已安装 DevEco Studio
- ✅ 已创建 HarmonyOS 项目
- ✅ 对 ArkTS 语法有基本了解
如果你还没准备好,建议先:
- 📖 阅读HarmonyOS 官方入门教程
- 🛠️ 跟着官方 Quick Start搭建开发环境
场景一:多类型列表项 ⭐
🎯 学习目标:掌握在一个列表中展示不同类型内容的技能,做出像电商首页那样的丰富界面
🤔 什么是多类型列表项?
先从熟悉的 APP 说起:
打开你手机上的淘宝或京东,你会发现首页是这样的:
- 🔍 最顶部:搜索框
- 🖼️ 中间:轮播广告图片
- 🎯 下面:功能按钮(天猫超市、聚划算等)
- 📱 再下面:商品推荐列表
神奇的地方在于:这些完全不同的内容(搜索框、图片、按钮、列表)都在同一个可以滚动的页面里!
这就是"多类型列表项"——一个列表容器里装着各种不同类型的内容。
🏆 为什么要学这个?
实际开发中超级常用:
- 🏪 电商 APP:首页几乎都是这种设计
- 📰 新闻 APP:头条 + 导航 + 新闻列表
- 🎵 音乐 APP:推荐歌单 + 排行榜 + 新歌
- 🏠 几乎所有 APP 首页:都需要展示丰富的内容类型
学会了你就能做:
- ✅ 复杂的首页界面
- ✅ 丰富的内容展示
- ✅ 用户体验良好的滚动效果
- ✅ 下拉刷新、上拉加载等高级功能
🎯 本例效果预览
我们将实现一个旅游应用的首页,包含:
- 顶部搜索框:固定在页面顶部
- 轮播图区域:展示热门景点图片
- 功能网格:快速入口(如"门票"、"酒店"等)
- 推荐内容:景点推荐列表
- 交互功能:下拉刷新、上拉加载、标题吸顶
| 页面整体结构图
|
页面效果图
| | | |
|

|

|
🔍 实现原理(简单易懂版)
核心思路:像搭积木一样组装界面
想象你有一个神奇的大盒子(List),可以往里面放各种不同的"积木"(ListItem):
🗃️ List (神奇大盒子)
├── 🧩 ListItem (积木1:搜索框)
├── 🧩 ListItem (积木2:轮播图)
├── 🧩 ListItem (积木3:功能按钮网格)
├── 🧩 ListItem (积木4:推荐标题)
├── 🧩 ListItem (积木5:推荐内容1)
├── 🧩 ListItem (积木6:推荐内容2)
└── 🧩 ...更多积木🛠️ 需要用到的工具(组件):
| 工具名称 | 用途 | 生活中的比喻 | 何时使用 |
|---|---|---|---|
| Refresh | 下拉刷新容器 | 像给手机"拉一下"就能更新内容 | 需要下拉刷新功能时 |
| List | 列表主容器 | 装所有内容的大盒子 | 几乎总是需要 |
| ListItem | 单个列表项 | 盒子里的每个小格子 | 每一块内容都需要 |
| Swiper | 轮播图组件 | 可以左右滑动的相册 | 需要图片轮播时 |
| Grid | 网格布局 | 像九宫格一样排列的格子 | 需要整齐排列按钮时 |
🔄 用户操作流程:
用户下拉页面 🔽
↓
触发刷新事件 🔄
↓
模拟获取新数据 📡
↓
更新界面内容 ✨
用户滚动到底部 📜
↓
触发加载更多 ⬆️
↓
追加新内容 ➕💡 新手提示:不用一次性掌握所有组件,我们会一步步来,每个步骤都有详细说明!
👨💻 动手实践:一步步实现
💡 开始前的提醒:建议在 DevEco Studio 中新建一个项目,跟着教程一步步敲代码,这样学习效果最好!
🔧 步骤一:搭建搜索框(难度:⭐)
🎯 目标: 做一个像淘宝首页那样的搜索框
📝 学习要点:
- 了解 Row 布局(水平排列)
- 学会使用 Image 和 TextInput 组件
- 掌握基本的样式设置
💭 思路: 搜索框 = 搜索图标 + 输入框,用 Row 把它们横向排列
// 🏗️ 搜索框组件 - 新手友好版
@Component
struct SearchHeader {
build() {
Row() { // Row是水平布局,让内容从左到右排列
// 🔍 搜索图标
Image($r('app.media.search_icon'))
.width(20) // 图标宽度20像素
.height(20) // 图标高度20像素
// 📝 搜索输入框
TextInput({ placeholder: '搜索景点、酒店...' })
.layoutWeight(1) // 占据剩余所有空间
.margin({ left: 10 }) // 左边距10像素,和图标保持距离
}
.padding(16) // 整个搜索框内边距16像素
.backgroundColor(Color.White) // 白色背景
}
}📖 代码解释:
Row():让里面的内容水平排列(从左到右)Image():显示搜索图标TextInput():用户可以输入文字的框layoutWeight(1):让输入框占据除图标外的所有空间padding(16):内容和边框之间留 16 像素的空隙
✨ 效果预览:

🎉 恭喜! 你已经学会了基础的组件使用和布局!接下来我们要做更酷的轮播图...
📸 步骤二:制作轮播图(难度:⭐⭐)
🎯 目标: 做一个可以自动播放的图片轮播,就像电商 APP 首页的广告图
📝 学习要点:
- 掌握 Swiper 组件的基本用法
- 学会使用 ForEach 循环渲染数据
- 了解自动播放和指示器设置
💭 思路: 轮播图 = 多张图片 + 自动切换 + 底部小圆点
🔧 实现代码:
// 📸 轮播图组件 - 带详细注释
ListItem() { // 这是List里的一个项目,专门放轮播图
Swiper() { // Swiper让图片可以左右滑动
// 🔄 ForEach循环:把数组里的每张图片都显示出来
ForEach(this.bannerImages, (item: string) => {
Image(item) // 显示图片
.width('100%') // 宽度占满整个容器
.height(200) // 高度固定200像素
.borderRadius(8) // 圆角8像素,看起来更美观
})
}
.autoPlay(true) // ✨ 自动播放(像电视换台一样)
.interval(3000) // ⏰ 每3秒自动切换一次
.indicator(true) // 🔵 显示底部小圆点(告诉用户当前看的是第几张)
.padding(16) // 四周留16像素空隙
}📊 数据准备:
// 📷 轮播图片数组 - 你可以换成自己的图片
@State bannerImages: string[] = [
'https://example.com/banner1.jpg', // 第一张图
'https://example.com/banner2.jpg', // 第二张图
'https://example.com/banner3.jpg' // 第三张图
]📖 代码解释:
Swiper():神奇的轮播容器,让内容可以左右滑动ForEach():循环渲染,有几张图片就显示几张autoPlay(true):开启自动播放模式interval(3000):3000 毫秒 = 3 秒,设置切换间隔indicator(true):显示底部的小圆点导航
✨ 效果预览:

💡 小贴士:如果图片加载不出来,检查一下网络地址是否正确,或者先用本地图片测试
🎯 步骤三:制作功能网格(难度:⭐⭐)
🎯 目标: 做一个像手机桌面那样的图标网格,放各种功能入口
📝 学习要点:
- 掌握 Grid 网格布局
- 理解行列模板的设置
- 学会处理点击事件
💭 思路: 网格 = 多个按钮整齐排列,就像九宫格游戏
🔧 实现代码:
// 🎯 功能网格组件 - 新手详解版
ListItem() { // 又是List里的一个项目
Grid() { // Grid是网格布局,让内容像表格一样排列
// 🔄 循环显示每个功能按钮
ForEach(this.functionList, (item: FunctionItem) => {
GridItem() { // 网格中的一个格子
Column() { // Column是垂直布局,图标在上,文字在下
// 🎨 功能图标
Image(item.icon)
.width(40) // 图标宽40像素
.height(40) // 图标高40像素
// 📝 功能名称
Text(item.title)
.fontSize(12) // 文字大小12像素
.margin({ top: 8 }) // 和图标保持8像素距离
}
.justifyContent(FlexAlign.Center) // 内容居中显示
}
.onClick(() => {
// 🖱️ 点击事件:用户点击按钮时会执行这里的代码
console.log(`用户点击了${item.title}`)
// 这里可以添加跳转页面、弹窗等功能
})
})
}
.columnsTemplate('1fr 1fr 1fr 1fr') // 📐 4列,每列宽度相等
.rowsTemplate('1fr 1fr') // 📐 2行,每行高度相等
.height(160) // 整个网格高度160像素
.padding(16) // 四周留16像素空隙
}📊 数据准备:
// 🛠️ 功能列表数据
interface FunctionItem {
icon: string // 图标地址
title: string // 功能名称
}
@State functionList: FunctionItem[] = [
{ icon: 'app.media.hotel_icon', title: '酒店' },
{ icon: 'app.media.ticket_icon', title: '门票' },
{ icon: 'app.media.food_icon', title: '美食' },
{ icon: 'app.media.car_icon', title: '租车' },
{ icon: 'app.media.guide_icon', title: '导游' },
{ icon: 'app.media.more_icon', title: '更多' }
]📖 代码解释:
Grid():网格布局容器,像 Excel 表格一样GridItem():网格中的每一个格子Column():垂直布局,让图标和文字上下排列columnsTemplate('1fr 1fr 1fr 1fr'):分成 4 列,每列宽度相等rowsTemplate('1fr 1fr'):分成 2 行,每行高度相等onClick():点击事件处理函数
✨ 效果预览:

🎮 互动体验:运行代码后试着点击不同的按钮,看看控制台的输出信息
步骤 4:创建推荐内容列表
目标: 添加可滚动的推荐内容列表
关键点:
- 先添加一个标题 ListItem(设置 sticky 属性)
- 然后添加多个内容 ListItem
- 每个内容项包含图片、标题、描述等
// 推荐标题(吸顶效果)
ListItem() {
Text('推荐景点')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.padding(16)
.backgroundColor(Color.White)
}
.sticky(Sticky.Normal) // 关键:设置吸顶效果
// 推荐内容项
ForEach(this.recommendList, (item: RecommendItem) => {
ListItem() {
Row() {
Image(item.image)
.width(80)
.height(80)
.borderRadius(8)
Column() {
Text(item.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
Text(item.description)
.fontSize(14)
.fontColor(Color.Gray)
.margin({ top: 4 })
Text(`¥${item.price}`)
.fontSize(16)
.fontColor(Color.Red)
.margin({ top: 8 })
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 12 })
.layoutWeight(1)
}
.padding(16)
.alignItems(VerticalAlign.Top)
}
})实现效果:

步骤 5:添加刷新和加载功能
目标: 实现下拉刷新和上拉加载更多功能
关键点:
- 用 Refresh 组件包裹整个 List
- 添加 onRefreshing 回调处理下拉刷新
- 添加 onReachEnd 回调处理上拉加载
// 完整页面结构
@Component
struct HomePage {
@State isRefreshing: boolean = false
@State recommendList: RecommendItem[] = []
build() {
Column() {
// 顶部搜索框(固定)
SearchHeader()
// 可刷新的内容区域
Refresh({ refreshing: this.isRefreshing }) {
List() {
// 轮播图
ListItem() { /* Swiper内容 */ }
// 功能网格
ListItem() { /* Grid内容 */ }
// 推荐标题(吸顶)
ListItem() { /* 标题内容 */ }
.sticky(Sticky.Normal)
// 推荐列表
ForEach(this.recommendList, (item) => {
ListItem() { /* 推荐项内容 */ }
})
}
.onReachEnd(() => {
// 上拉加载更多
this.loadMoreData()
})
}
.onRefreshing(() => {
// 下拉刷新
this.refreshData()
})
}
}
// 刷新数据
refreshData() {
this.isRefreshing = true
// 模拟网络请求
setTimeout(() => {
this.recommendList = this.getNewData()
this.isRefreshing = false
}, 2000)
}
// 加载更多数据
loadMoreData() {
// 模拟加载更多
setTimeout(() => {
this.recommendList.push(...this.getMoreData())
}, 1000)
}
}🎬 最终实现效果
通过以上步骤,我们成功实现了一个功能完整的多类型列表页面:
主要功能展示:
| 下拉刷新 + 标题吸顶效果 | 上拉加载更多效果 |
|---|---|
![]() | ![]() |
| 用户下拉时显示刷新动画,"推荐景点"标题在滚动时自动吸顶 | 滚动到底部时自动加载更多内容,提升用户体验 |
核心特性总结:
- ✅ 多类型内容:轮播图、网格、列表完美融合
- ✅ 下拉刷新:模拟真实的数据更新场景
- ✅ 上拉加载:无限滚动,数据分页加载
- ✅ 标题吸顶:重要信息始终可见
- ✅ 流畅交互:符合用户操作习惯
适用场景回顾: 这种实现方式特别适合需要展示丰富内容的首页场景,如电商 APP 首页、新闻资讯首页、旅游 APP 首页等。
Tabs 吸顶场景
📖 场景说明
什么是 Tabs 吸顶效果?
想象一下你在使用今日头条、网易新闻等新闻 APP 时的体验:
- 页面顶部有搜索框和标题
- 中间有分类标签(如"推荐"、"热点"、"科技"、"娱乐"等)
- 下面是对应分类的新闻列表
- 关键点:当你向上滑动时,顶部内容会消失,但分类标签会"粘"在屏幕顶部不动
这就是"Tabs 吸顶"效果!它让用户在浏览长列表时,依然能够方便地切换不同分类。
为什么需要这个效果?
- 🎯 提升用户体验:无论滚动到哪里,都能快速切换分类
- 📱 节省屏幕空间:隐藏不重要的顶部内容,留更多空间给内容
- 🔄 符合使用习惯:用户已经习惯了这种交互方式
常见应用场景:
- 📰 新闻资讯 APP:分类新闻浏览
- 🛒 电商 APP:商品分类浏览
- 🎵 音乐 APP:歌单分类
- 📺 视频 APP:内容分类
🎯 本例效果预览
我们将实现一个新闻资讯 APP 的首页,包含:
- 顶部区域:搜索框、用户头像等(滚动时会隐藏)
- 图片横幅:热门新闻展示(滚动时会隐藏)
- Tabs 标签:新闻分类导航(滚动时吸顶显示)
- 内容列表:对应分类的新闻列表(可正常滚动)
| 页面整体结构图
|
页面效果图
| | | |
|

|

|
🔧 实现原理
核心思路:嵌套滚动 + 高度计算
实现 Tabs 吸顶的关键在于理解"嵌套滚动"的概念:
页面结构:
┌─────────────────────┐
│ 顶部搜索区域 │ ← 滚动时会隐藏
├─────────────────────┤
│ 图片横幅区域 │ ← 滚动时会隐藏
├─────────────────────┤
│ Tabs标签区域 │ ← 滚动时吸顶(关键!)
├─────────────────────┤
│ │
│ 内容列表区域 │ ← 可以正常滚动
│ (新闻列表) │
│ │
└─────────────────────┘技术实现要点:
整体布局使用 Stack:
- 底层:完整的页面内容(包括顶部区域)
- 顶层:固定的 Tabs 标签(初始时透明或隐藏)
关键属性 - nestedScroll:
typescriptList().nestedScroll({ scrollForward: NestedScrollMode.PARENT_FIRST, // 向上滚动时优先滚动父容器 scrollBackward: NestedScrollMode.SELF_FIRST, // 向下滚动时优先滚动自己 });高度计算:
typescript// 计算Tabs应该显示的位置 .height(`calc(100% - ${this.topHeight}px)`)
工作流程:
用户向上滚动 → 顶部内容逐渐隐藏 → 当滚动到Tabs位置时 → Tabs固定在顶部 → 继续滚动内容列表关键组件说明:
- Tabs:标签页容器,提供分类切换功能
- Stack:堆叠布局,实现层级效果
- List + nestedScroll:嵌套滚动的核心
- Scroll:滚动容器,承载顶部内容
👨💻 开发步骤
步骤 1:创建自定义 Tabs 标签栏
目标: 创建一个美观的分类标签栏,支持点击切换
关键点:
- 使用自定义 tabBar 而不是默认样式
- 支持滑动切换和点击切换
- 标签样式要清晰易识别
@Component
struct CustomTabBar {
@State currentIndex: number = 0
private tabTitles: string[] = ['推荐', '热点', '科技', '娱乐', '体育']
build() {
Row() {
ForEach(this.tabTitles, (title: string, index: number) => {
Column() {
Text(title)
.fontSize(16)
.fontColor(this.currentIndex === index ? Color.Red : Color.Black)
.fontWeight(this.currentIndex === index ? FontWeight.Bold : FontWeight.Normal)
// 底部指示线
Divider()
.width(20)
.height(2)
.color(Color.Red)
.opacity(this.currentIndex === index ? 1 : 0)
.margin({ top: 4 })
}
.layoutWeight(1)
.padding({ vertical: 12 })
.onClick(() => {
this.currentIndex = index
// 切换到对应标签页
})
})
}
.width('100%')
.backgroundColor(Color.White)
}
}实现效果:

步骤 2:创建顶部搜索区域
目标: 实现顶部搜索框和用户信息区域(滚动时会隐藏)
关键点:
- 包含搜索框、用户头像、消息图标等
- 这部分内容在滚动时会被隐藏
- 布局要美观且功能完整
@Component
struct TopSearchArea {
build() {
Row() {
// 搜索框
Row() {
Image($r('app.media.search_icon'))
.width(16)
.height(16)
Text('搜索新闻、资讯...')
.fontSize(14)
.fontColor(Color.Gray)
.margin({ left: 8 })
}
.layoutWeight(1)
.padding({ horizontal: 12, vertical: 8 })
.backgroundColor('#F5F5F5')
.borderRadius(20)
// 右侧图标
Row() {
Image($r('app.media.message_icon'))
.width(24)
.height(24)
.margin({ right: 12 })
Image($r('app.media.avatar'))
.width(32)
.height(32)
.borderRadius(16)
}
.margin({ left: 16 })
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
}
}实现效果:

步骤 3:创建图片横幅和内容列表
目标: 添加热门新闻横幅和新闻列表内容
关键点:
- 横幅图片要吸引眼球
- 新闻列表要支持滚动
- 内容要丰富且布局合理
@Component
struct NewsContent {
@State newsList: NewsItem[] = []
build() {
Column() {
// 热门新闻横幅
Image($r('app.media.hot_news_banner'))
.width('100%')
.height(200)
.objectFit(ImageFit.Cover)
.borderRadius(8)
.margin(16)
// 新闻列表
List() {
ForEach(this.newsList, (news: NewsItem) => {
ListItem() {
Row() {
Column() {
Text(news.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(news.summary)
.fontSize(14)
.fontColor(Color.Gray)
.maxLines(1)
.margin({ top: 4 })
Row() {
Text(news.source)
.fontSize(12)
.fontColor(Color.Gray)
Text(news.time)
.fontSize(12)
.fontColor(Color.Gray)
.margin({ left: 16 })
}
.margin({ top: 8 })
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
Image(news.image)
.width(80)
.height(60)
.borderRadius(4)
.margin({ left: 12 })
}
.padding(16)
.alignItems(VerticalAlign.Top)
}
})
}
.layoutWeight(1)
}
}
}实现效果:

步骤 4:实现 Tabs 吸顶效果
目标: 使用 nestedScroll 属性实现 Tabs 标签的吸顶效果
关键点:
- 使用 Stack 布局实现层级效果
- 配置 nestedScroll 属性
- 计算正确的高度值
@Component
struct TabsStickyPage {
@State topHeight: number = 200 // 顶部区域高度
build() {
Stack({ alignContent: Alignment.Top }) {
// 底层:完整页面内容
Column() {
TopSearchArea() // 顶部搜索区域
Tabs() {
TabContent() {
NewsContent() // 推荐新闻内容
}.tabBar('推荐')
TabContent() {
NewsContent() // 热点新闻内容
}.tabBar('热点')
// 更多标签页...
}
.barPosition(BarPosition.Start)
.layoutWeight(1)
}
// 顶层:固定的Tabs标签(吸顶时显示)
Column() {
CustomTabBar()
}
.width('100%')
.position({ x: 0, y: this.topHeight })
.zIndex(1)
// 内容区域List(关键:配置nestedScroll)
List() {
// 内容项...
}
.nestedScroll({
scrollForward: NestedScrollMode.PARENT_FIRST,
scrollBackward: NestedScrollMode.SELF_FIRST
})
.height(`calc(100% - ${this.topHeight}px)`)
.margin({ top: this.topHeight })
}
.width('100%')
.height('100%')
}
}核心配置说明:
NestedScrollMode.PARENT_FIRST:向上滚动时优先滚动父容器(隐藏顶部内容)NestedScrollMode.SELF_FIRST:向下滚动时优先滚动自己(显示顶部内容)calc(100% - ${this.topHeight}px):动态计算内容区域高度
🎬 最终实现效果
通过以上步骤,我们成功实现了 Tabs 吸顶的完整效果:
核心功能展示:

效果说明:
- 初始状态:顶部搜索区域、图片横幅、Tabs 标签都正常显示
- 向上滚动:顶部内容逐渐隐藏,Tabs 标签开始上移
- 吸顶状态:Tabs 标签固定在屏幕顶部,内容列表继续滚动
- 向下滚动:Tabs 标签回到原位,顶部内容重新显示
技术特点总结:
- ✅ 流畅的滚动体验:使用 nestedScroll 实现自然的滚动效果
- ✅ 精确的吸顶定位:通过高度计算确保 Tabs 准确吸顶
- ✅ 完整的交互功能:支持标签切换和内容滚动
- ✅ 良好的用户体验:符合主流 APP 的交互习惯
适用场景回顾: 这种 Tabs 吸顶效果特别适合内容分类较多的应用,如新闻资讯、电商分类、视频分类等场景,能够显著提升用户的浏览效率。
分组吸顶场景
📖 场景说明
什么是分组吸顶效果?
这是一个非常实用的功能,最典型的例子就是手机通讯录:
- 联系人按照姓名首字母分组(A、B、C、D...)
- 右侧有一个字母索引条(A-Z)
- 滚动时,当前分组的字母标题会"吸"在顶部
- 点击右侧字母,左侧列表会快速跳转到对应分组
- 滚动左侧列表时,右侧对应字母会高亮显示
为什么需要这个效果?
- 🔍 快速定位:在大量数据中快速找到目标内容
- 📍 位置感知:用户始终知道当前浏览到哪个分组
- ⚡ 高效导航:一键跳转到任意分组
- 👀 视觉清晰:分组标题吸顶,分类一目了然
常见应用场景:
- 📞 通讯录:按姓名首字母分组的联系人列表
- 🏙️ 城市选择:按首字母分组的城市列表
- 🏢 公司选择:按首字母分组的公司列表
- 📚 词典应用:按首字母分组的词汇列表
- 🎵 音乐列表:按歌手首字母分组的歌曲
🎯 本例效果预览
我们将实现一个城市选择页面,包含:
左侧城市列表:
- 当前城市(定位获取)
- 热门城市(常用城市)
- 按字母分组的所有城市(A-Z)
- 分组标题支持吸顶效果
右侧字母索引:
- A-Z 字母快速导航
- 当前分组字母高亮显示
- 点击字母快速跳转
双向联动效果:
- 滚动左侧列表 → 右侧字母高亮变化
- 点击右侧字母 → 左侧列表跳转到对应分组
| 页面整体结构图
|
页面效果图
| | | |
|

|

|
🔧 实现原理详解
核心思想:分组 + 吸顶 + 双向联动
想象一下,我们要实现的效果就像是一个"智能书签系统":
- 📚 书本:整个城市列表
- 🏷️ 书签:每个字母分组的标题
- 📍 智能吸顶:当前章节的书签始终显示在顶部
- 🔗 快速索引:右侧字母导航就像目录,点击直接跳转
技术架构图:
Stack (堆叠布局)
├── 左侧城市列表 (List)
│ ├── ListItemGroup (当前城市)
│ │ ├── 分组标题 (sticky吸顶)
│ │ └── 城市项目 (ListItem)
│ ├── ListItemGroup (热门城市)
│ │ ├── 分组标题 (sticky吸顶)
│ │ └── 城市项目 (ListItem)
│ └── ListItemGroup (A-Z分组)
│ ├── 分组标题 (sticky吸顶)
│ └── 城市项目 (ListItem)
└── 右侧字母索引 (List)
├── 字母A (ListItem)
├── 字母B (ListItem)
└── ...关键技术组件:
| 组件 | 作用 | 关键属性/方法 | 说明 |
|---|---|---|---|
| ListItemGroup | 分组容器 | sticky: StickyStyle.Header | 实现分组标题吸顶效果 |
| Stack | 布局容器 | 分层显示 | 左侧列表 + 右侧索引的叠加布局 |
| List | 列表容器 | onScrollIndex、scrollToIndex | 监听滚动、控制跳转 |
| ListItem | 列表项 | 数据展示 | 城市信息的具体展示 |
核心技术点详解:
🎯 分组吸顶机制:
typescriptListItemGroup({ sticky: StickyStyle.Header }) { // 分组标题 - 会自动吸顶 // 分组内容 - 正常滚动 }📡 滚动监听机制:
typescriptList() { // 监听滚动位置变化 .onScrollIndex((start, end) => { // 根据当前显示的项目更新右侧字母高亮 }) }⚡ 快速跳转机制:
typescript// 点击右侧字母时 this.listController.scrollToIndex(targetIndex);
工作流程:
- 初始化:构建分组数据,计算每个分组的起始索引
- 滚动监听:实时监听列表滚动位置
- 状态同步:根据滚动位置更新右侧字母高亮
- 快速跳转:点击字母时跳转到对应分组
- 吸顶显示:当前分组标题自动吸顶
🛠️ 开发步骤详解
步骤 1:构建数据结构 📊
目标:创建完整的城市数据结构,支持分组和索引
关键实现点:
- 定义城市数据模型
- 构建当前城市、热门城市、字母分组数据
- 计算每个分组的起始索引位置
核心代码示例:
// 城市数据模型
interface CityInfo {
name: string;
code?: string;
}
// 分组数据结构
interface CityGroup {
title: string;
cities: CityInfo[];
startIndex: number; // 在整个列表中的起始位置
}
// 构建完整数据
const cityData = {
current: { title: "当前城市", cities: [currentCity] },
hot: { title: "热门城市", cities: hotCities },
groups: alphabetGroups, // A-Z分组
};视觉效果:数据结构清晰,为后续功能奠定基础
步骤 2:创建 Stack 分层布局 🏗️
目标:使用 Stack 组件实现左右分层显示
关键实现点:
- Stack 容器包含左侧城市列表和右侧字母索引
- 右侧字母索引悬浮在左侧列表之上
- 合理设置对齐方式和层级关系
核心代码示例:
Stack({ alignContent: Alignment.End }) {
// 左侧城市列表(底层)
List() {
// 城市列表内容
}
.width('100%')
.height('100%')
// 右侧字母索引(顶层)
List() {
// 字母导航内容
}
.width('30vp')
.height('100%')
}视觉效果:左右布局清晰,字母索引悬浮显示
步骤 3:实现左侧分组列表 📋
目标:使用 ListItemGroup 创建支持吸顶的分组列表
关键实现点:
- 使用 ListItemGroup 包装每个分组
- 设置 sticky 属性实现标题吸顶
- 渲染当前城市、热门城市、字母分组
核心代码示例:
List() {
// 当前城市分组
ListItemGroup({
header: this.groupHeader('当前城市'),
sticky: StickyStyle.Header
}) {
ForEach(currentCities, (city) => {
ListItem() {
this.cityItem(city)
}
})
}
// 热门城市分组
ListItemGroup({
header: this.groupHeader('热门城市'),
sticky: StickyStyle.Header
}) {
// 热门城市内容
}
// A-Z字母分组
ForEach(alphabetGroups, (group) => {
ListItemGroup({
header: this.groupHeader(group.title),
sticky: StickyStyle.Header
}) {
// 分组城市内容
}
})
}视觉效果:分组清晰,标题支持吸顶显示
步骤 4:创建右侧字母索引 🔤
目标:实现可点击的字母导航,支持快速跳转
关键实现点:
- 渲染 A-Z 字母列表
- 监听字母点击事件
- 实现高亮状态切换
- 调用 scrollToIndex 跳转到对应分组
核心代码示例:
List() {
ForEach(alphabetList, (letter, index) => {
ListItem() {
Text(letter)
.fontSize(12)
.fontColor(this.currentLetter === letter ? '#007DFF' : '#666')
.fontWeight(this.currentLetter === letter ? 600 : 400)
}
.onClick(() => {
// 跳转到对应分组
const targetIndex = this.getGroupStartIndex(letter);
this.listController.scrollToIndex(targetIndex);
this.currentLetter = letter;
})
})
}视觉效果:字母导航清晰,点击有反馈,支持快速跳转
步骤 5:实现双向联动效果 🔄
目标:滚动左侧列表时,右侧字母自动高亮;点击右侧字母时,左侧列表跳转
关键实现点:
- 监听 onScrollIndex 事件
- 根据当前显示区域计算对应字母
- 更新右侧字母高亮状态
- 实现平滑的联动效果
核心代码示例:
List() {
// 城市列表内容
}
.onScrollIndex((start, end) => {
// 根据当前显示的索引范围,计算对应的字母分组
const currentGroup = this.getCurrentGroupByIndex(start);
if (currentGroup && this.currentLetter !== currentGroup.title) {
this.currentLetter = currentGroup.title;
}
})视觉效果:滚动流畅,字母高亮准确,用户体验良好
相关文档链接:
🎬 最终实现效果
通过以上步骤,我们成功实现了分组吸顶的完整功能:
核心功能展示:

效果说明:
- 分组显示:城市按照"当前城市"、"热门城市"、"A-Z 字母"清晰分组
- 吸顶效果:滚动时,当前分组标题自动吸附在顶部
- 字母索引:右侧 A-Z 字母导航,当前分组字母高亮显示
- 快速跳转:点击右侧字母,左侧列表立即跳转到对应分组
- 双向联动:滚动左侧列表,右侧字母高亮状态实时更新
技术特点总结:
- ✅ 智能分组:支持多种分组类型(当前、热门、字母)
- ✅ 精准吸顶:ListItemGroup 的 sticky 属性确保标题准确吸顶
- ✅ 快速导航:字母索引提供一键跳转功能
- ✅ 实时联动:滚动和点击事件完美同步
- ✅ 用户友好:符合用户对通讯录等应用的使用习惯
适用场景回顾: 这种分组吸顶效果广泛应用于需要大量数据分类展示的场景,如通讯录、城市选择、公司列表等,能够显著提升用户查找效率和使用体验。
性能优势:
- 🚀 高效渲染:List 组件的虚拟滚动确保大数据量下的流畅性能
- 💾 内存优化:只渲染可见区域的列表项,节省内存占用
- ⚡ 快速响应:原生组件确保交互响应速度
二级联动场景
📖 场景说明
什么是二级联动效果?
这是一个经典的"主从"关系界面,最常见的例子就是电商 APP 的商品分类页面:
- 左侧一级分类:显示主要分类(如"服装"、"数码"、"家居"等)
- 右侧二级分类:显示选中一级分类下的子分类
- 联动关系:点击左侧分类 → 右侧内容立即更新
- 反向联动:滚动右侧内容 → 左侧对应分类高亮
- 吸顶效果:右侧列表的分组标题支持吸顶显示
为什么需要这个效果?
- 🎯 层级清晰:将复杂的分类体系分解为两个层级,降低认知负担
- 🔄 高效导航:左侧提供快速分类切换,右侧展示详细内容
- 👀 状态同步:用户始终知道当前浏览的是哪个分类
- 📱 空间利用:充分利用屏幕空间,展示更多信息
常见应用场景:
- 🛒 电商分类:商品的一级分类和二级分类选择
- 🎨 编辑工具:风格分类和具体风格选择
- 📚 内容分类:文章分类和子分类浏览
- 🏢 组织架构:部门和子部门的层级展示
- 🎵 音乐分类:音乐类型和具体歌单分类
- 🍔 餐饮菜单:菜品分类和具体菜品展示
🎯 本例效果预览
我们将实现一个商品分类选择页面,包含:
左侧一级分类导航:
- 垂直排列的主要分类列表
- 当前选中分类高亮显示
- 点击切换分类功能
右侧二级内容区域:
- 显示选中分类的详细子分类
- 支持分组标题吸顶效果
- 丰富的内容展示(图片+文字)
双向联动机制:
- 主动联动:点击左侧分类 → 右侧内容切换
- 被动联动:滚动右侧内容 → 左侧分类高亮更新
- 状态保持:切换分类时保持滚动位置
🏗️ 页面结构设计
整体布局架构:
| 页面整体结构图 | 页面效果图 |
|---|---|
![]() | ![]() |
| 左右分栏布局:左侧导航 + 右侧内容 | 实际效果:清晰的分类层级和丰富的内容展示 |
🔧 实现原理详解
核心思想:双 List 联动 + 状态同步
想象一下,这就像是一个"智能目录系统":
- 📖 左侧目录:显示章节标题,点击可快速跳转
- 📄 右侧内容:显示具体内容,滚动时目录自动跟随
- 🔗 智能联动:目录和内容始终保持同步
技术架构:
Row (水平布局)
├── 左侧导航区域 (List)
│ ├── 分类1 (ListItem) - 可点击
│ ├── 分类2 (ListItem) - 可点击
│ └── 分类N (ListItem) - 可点击
└── 右侧内容区域 (List)
├── ListItemGroup (分类1内容)
│ ├── 分组标题 (sticky吸顶)
│ └── 子分类项目 (ListItem)
├── ListItemGroup (分类2内容)
└── ListItemGroup (分类N内容)关键技术点:
🎯 双向监听机制:
- 左侧 List 监听点击事件 → 控制右侧 List 跳转
- 右侧 List 监听滚动事件 → 更新左侧 List 高亮
📡 状态同步算法:
typescript// 右侧滚动时,计算当前显示的分类 .onScrollIndex((start, end) => { const currentCategory = this.getCategoryByIndex(start); this.updateLeftHighlight(currentCategory); })⚡ 精准跳转控制:
typescript// 左侧点击时,跳转到对应分类 .onClick(() => { const targetIndex = this.getCategoryStartIndex(category); this.rightListController.scrollToIndex(targetIndex); })
相关文档链接:
🛠️ 开发步骤详解
步骤 1:构建数据结构 📊
目标:创建支持二级联动的分类数据结构
关键实现点:
- 定义一级分类和二级分类的数据模型
- 建立分类之间的映射关系
- 计算每个分类在右侧列表中的起始位置
核心代码示例:
// 分类数据模型
interface Category {
id: string;
name: string;
icon?: string;
subCategories: SubCategory[];
startIndex: number; // 在右侧列表中的起始位置
}
interface SubCategory {
id: string;
name: string;
image?: string;
description?: string;
}
// 构建完整的分类数据
const categoryData: Category[] = [
{
id: '1',
name: '服装鞋帽',
subCategories: [...],
startIndex: 0
},
// 更多分类...
];步骤 2:创建左右分栏布局 🏗️
目标:使用 Row 组件实现左右分栏显示
关键实现点:
- Row 容器包含左侧导航和右侧内容
- 合理分配左右区域的宽度比例
- 设置合适的分割线或背景色区分
核心代码示例:
Row() {
// 左侧分类导航
List() {
ForEach(categories, (category) => {
ListItem() {
this.categoryNavItem(category)
}
.onClick(() => this.onCategoryClick(category))
})
}
.width('25%')
.backgroundColor('#F5F5F5')
// 右侧内容区域
List() {
ForEach(categories, (category) => {
ListItemGroup({
header: this.categoryHeader(category.name),
sticky: StickyStyle.Header
}) {
ForEach(category.subCategories, (subCategory) => {
ListItem() {
this.subCategoryItem(subCategory)
}
})
}
})
}
.width('75%')
.onScrollIndex((start, end) => this.onRightScroll(start, end))
}步骤 3:实现联动控制逻辑 🔄
目标:实现左右列表的双向联动效果
关键实现点:
- 左侧点击事件处理
- 右侧滚动事件监听
- 状态同步和高亮更新
- 防抖处理避免频繁更新
核心代码示例:
// 左侧分类点击处理
onCategoryClick(category: Category) {
this.currentCategoryId = category.id;
this.rightListController.scrollToIndex(category.startIndex);
}
// 右侧列表滚动处理
onRightScroll(start: number, end: number) {
const currentCategory = this.getCategoryByIndex(start);
if (currentCategory && this.currentCategoryId !== currentCategory.id) {
this.currentCategoryId = currentCategory.id;
}
}
// 根据索引获取对应分类
getCategoryByIndex(index: number): Category | null {
for (const category of this.categories) {
const endIndex = category.startIndex + category.subCategories.length;
if (index >= category.startIndex && index < endIndex) {
return category;
}
}
return null;
}🎬 最终实现效果
通过以上步骤,我们成功实现了二级联动的完整功能:
核心功能展示:

效果说明:
- 分类导航:左侧显示主要分类,当前选中分类高亮显示
- 内容展示:右侧显示选中分类的详细子分类内容
- 主动联动:点击左侧分类,右侧内容立即切换到对应分类
- 被动联动:滚动右侧内容,左侧分类高亮状态自动更新
- 吸顶效果:右侧分组标题支持吸顶显示,提升浏览体验
技术特点总结:
- ✅ 响应迅速:点击和滚动事件响应及时,用户体验流畅
- ✅ 状态同步:左右两侧状态始终保持一致
- ✅ 视觉清晰:分类层级清晰,内容组织有序
- ✅ 交互友好:符合用户对分类选择界面的使用习惯
- ✅ 性能优化:使用 List 组件的虚拟滚动,支持大量数据展示
适用场景总结: 这种二级联动效果特别适合需要展示层级分类数据的场景,如电商分类、内容分类、组织架构等,能够帮助用户快速定位和浏览目标内容,显著提升应用的易用性和用户满意度。
🎓 学习总结与进阶指南
📚 知识点回顾
通过本教程,你已经掌握了:
| 技能等级 | 掌握内容 | 实用性 |
|---|---|---|
| 基础级 | List、ListItem、Image、Text 等基础组件 | ⭐⭐⭐⭐⭐ |
| 进阶级 | Swiper 轮播、Grid 网格、下拉刷新 | ⭐⭐⭐⭐ |
| 高级级 | sticky 吸顶、nestedScroll 嵌套滚动、双向联动 | ⭐⭐⭐ |
🚀 实战建议
立即可以做的项目:
- 📱 仿淘宝首页:应用多类型列表项技能
- 📰 新闻 APP:使用 Tabs 吸顶功能
- 📞 通讯录应用:练习分组吸顶效果
- 🛒 购物分类页:实现二级联动
进阶学习方向:
- 🎨 动画效果:为列表添加炫酷的过渡动画
- 🔄 状态管理:学习复杂数据的状态管理
- 🌐 网络请求:结合真实 API 接口获取数据
- 📱 响应式设计:适配不同尺寸的设备
💡 开发小贴士
性能优化:
- ✅ 列表数据较多时,List 组件会自动进行虚拟滚动优化
- ✅ 图片建议使用适当尺寸,避免内存浪费
- ✅ 合理使用@State 状态管理,避免不必要的重新渲染
调试技巧:
- 🐛 使用
console.log()输出调试信息 - 🐛 利用 DevEco Studio 的预览功能实时查看效果
- 🐛 善用官方文档和示例代码
代码规范:
- 📝 组件名称使用大驼峰命名(如 SearchHeader)
- 📝 添加有意义的注释,方便后续维护
- 📝 保持代码结构清晰,一个组件完成一个功能
📖 相关资源
官方文档:
学习社区:
🎯 下一步学习建议
- 巩固基础:多敲代码,熟练掌握基础组件
- 实战练习:选择一个感兴趣的 APP 界面进行仿制
- 深入学习:探索更多高级组件和 API
- 参与社区:加入开发者群体,交流学习心得
🌟 记住:编程是一个不断实践和提升的过程,每写一行代码都是在进步!加油!
如果这篇文章对你有帮助,欢迎点赞、收藏和分享!有问题欢迎在评论区讨论~



