鸿蒙应用开发实战【18】— Scroll + ForEach 列表滚动实战
📅 2026/7/16 5:20:03
👁️ 阅读次数
📝 编程学习
鸿蒙应用开发实战【18】— Scroll + ForEach 列表滚动实战
前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
列表展示是移动端最核心的 UI 模式之一。号码助手的首页、卡号管理页、状态列表页都大量使用了Scroll + ForEach组合来展示可滚动列表。本篇深入讲解这个组合的使用方式、性能优化策略,以及与List组件的区别。
本篇涵盖:Scroll 组件用法、ForEach 数据渲染、key 函数设计、layoutWeight 弹性高度、底部留空策略、Scroll + Column 的组合、List vs Scroll 的选择、下拉刷新实现。
图1:Scroll + ForEach 布局结构、layoutWeight 弹性高度、key 函数设计、底部留空策略
一、Scroll 组件基础
1.1 Scroll 与 List 的选择
| 特性 | Scroll + Column/Row | List |
|---|---|---|
| 内容类型 | 任意组件(异构内容) | 同类型列表项 |
| 性能 | 全量渲染(适合 <100 项) | 虚拟化(适合 >100 项) |
| 滚动方向 | 横向/纵向 | 纵向/横向(需配置) |
| 编程接口 | 相对简单 | 功能更丰富 |
| 号码助手使用 | ✅ 首页/卡号管理(数量少) | 未使用(数据量小不需要) |
1.2 Scroll 基本用法
Scroll(){Column(){// 列表内容ForEach(this.items,(item:ItemType)=>{ItemComponent({data:item})},(item:ItemType)=>`${item.id}`)// 底部留空(避免内容被 FAB/底栏遮挡)Text('').height(90)}.padding({left:16,right:16})}.layoutWeight(1)// 占用 Column 中的剩余垂直空间二、号码助手中的 Scroll 实现
2.1 首页应用列表
// HomePage.ets — 正常列表模式Scroll(){Column(){// ① ForEach 渲染过滤后的列表ForEach(this.getFilteredRows(),(row:AppRow)=>{AppListItem({appName:row.binding.app_name,iconColor:row.binding.icon_key,category:row.binding.category,cardLabel:row.cardLabel,status:row.binding.status,bindingId:row.binding.id??0})},(row:AppRow)=>`binding_${row.binding.id??0}_${row.binding.status}`)// ↑ key 函数:确保 status 变化时正确重渲染// ② 底部留空:避免最后一项被 FAB 悬浮按钮遮挡Text('').height(90)}.padding({left:16,right:16})}.layoutWeight(1)// ← 关键:弹性占满剩余高度2.2 批量选择模式列表
// 批量选择模式:带复选框的列表Scroll(){Column(){ForEach(this.getFilteredRows(),(row:AppRow)=>{this.SelectableRow(row)// 调用 @Builder 方法},(row:AppRow)=>`sel_${row.binding.id??0}_${this.selectedIds.includes(row.binding.id??0)?'1':'0'}`)// ↑ key 函数:包含选中状态,选中/取消选中时重渲染Text('').height(80)// 批量操作底栏高度留空}.padding({left:16,right:16})}.layoutWeight(1)2.3 卡号管理列表
// CardManagePage.etsScroll(){Column(){if(this.loading){LoadingProgress().width(40).height(40).color(AppColors.PRIMARY)}else{ForEach(this.cards,(card:CardEntity)=>{this.CardRow(card)},(card:CardEntity)=>`card_${card.id??0}`)}Text('').height(90)}.padding({left:16,right:16,top:8})}.layoutWeight(1)三、ForEach 详解
3.1 基本语法
ForEach(arr:Array<T>,// 数据数组itemGenerator:(item:T,idx:number)=>void,// 渲染函数keyGenerator?:(item:T,idx:number)=>string// key 函数(强烈建议))3.2 key 函数的重要性
// key 函数决定 ArkUI 如何识别列表项的"身份"// ❌ 不提供 key:每次数组变化时全量重渲染ForEach(this.rows,(row:AppRow)=>{AppListItem({...})})// ❌ 只用 index 作为 key:删除中间项时后续全部重渲染ForEach(this.rows,(row:AppRow)=>{AppListItem({...})},(row:AppRow,idx:number)=>`${idx}`)// ✅ 用业务 ID + 关键状态:精确最小化重渲染ForEach(this.rows,(row:AppRow)=>{AppListItem({...})},(row:AppRow)=>`binding_${row.binding.id??0}_${row.binding.status}`)3.3 key 函数设计原则
- 唯一性:同一数组内 key 必须唯一
- 稳定性:数据未变化时 key 不变
- 包含影响渲染的状态:如果 status 变化影响 UI,key 中要包含 status
// 示例:不同场景的 key 函数设计// 场景1:只有增删,无状态变化(row:AppRow)=>`${row.binding.id??0}`// 场景2:有状态变化(选中/未选中)(row:AppRow)=>`${row.binding.id??0}_${this.selectedIds.includes(row.binding.id??0)?'sel':''}`// 场景3:有多个维度影响渲染(status + 筛选条件)(row:AppRow)=>`binding_${row.binding.id??0}_${row.binding.status}_${this.filterCatIdx}_${this.filterCardIdx}`四、layoutWeight 高度管理
4.1 为什么 Scroll 需要 layoutWeight
// 首页的布局结构Column(){// 标题栏(固定高度)TitleBar()// height: 44vp// 统计卡(固定高度)StatsRow()// height: ~88vp// 分区标题(固定高度)SectionHeader()// height: 44vp// 内容区(需要占满剩余高度)Scroll(){// ...}.layoutWeight(1)// ← 弹性分配剩余高度// 如果不加 layoutWeight,Scroll 高度为 0,无法滚动!}.height('100%')4.2 layoutWeight 的计算规则
// Column 总高度 = 屏幕高度(.height('100%'))// 减去固定高度子组件后的剩余 = Scroll 的高度// 例:屏幕高度 800vp// 标题栏:44vp// 统计卡:88vp// 分区标题:44vp// 剩余:800 - 44 - 88 - 44 = 624vp → Scroll 的高度五、底部留空策略
5.1 为什么需要底部留空
// 问题:FAB 悬浮按钮(.overlay)遮挡了最后一个列表项// 解决:在列表最后添加一个空白占位Scroll(){Column(){ForEach(this.rows,...)// 底部占位:高度 = FAB 高度 + 底部安全区域Text('').height(90)// FAB 约 48vp + bottom: 24 + 余量 ≈ 90vp}}5.2 不同场景的留空高度
| 场景 | 建议留空高度 |
|---|---|
| 有 FAB 悬浮按钮 | 90vp |
| 有批量操作底栏(~64vp) | 80vp |
| 无底部遮挡 | 20vp(防止内容贴边) |
六、Scroll 的其他配置
6.1 横向滚动
// 横向 Scroll(如卡片横向滑动)Scroll(){Row({space:12}){ForEach(this.cards,(card:CardEntity)=>{CardThumbnail({data:card}).width(160)// 固定每张卡片宽度},(card:CardEntity)=>`${card.id??0}`)}.padding({left:16,right:16})}.scrollable(ScrollDirection.Horizontal)// 横向滚动.scrollBar(BarState.Off)// 隐藏滚动条6.2 ScrollController 编程控制
privatescroller:Scroller=newScroller()Scroll(this.scroller){Column(){...}}// 滚动到顶部Button('回到顶部').onClick(()=>{this.scroller.scrollTo({xOffset:0,yOffset:0})})// 滚动到底部this.scroller.scrollEdge(Edge.Bottom)七、本篇小结
| 知识点 | 核心要点 |
|---|---|
| Scroll + Column | 适合 <100 项的列表,全量渲染 |
| layoutWeight(1) | 让 Scroll 弹性占满剩余高度(必须) |
| ForEach key 函数 | 包含 id + 影响渲染的状态,精确重渲染 |
| 底部留空 | Text(‘’).height(90) 避免 FAB 遮挡最后一项 |
| ScrollController | 编程控制滚动位置 |
参考资料
- 鸿蒙应用开发实战【05】— 首页开发与状态管理
- 鸿蒙应用开发实战【79】— LazyForEach 虚拟化
- Scroll 组件参考
- ForEach 使用指南
- List 组件参考
- LazyForEach 高性能列表
- layoutWeight 属性
- Column 容器
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- 开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
- Scroll 组件文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-scroll
- ForEach 指南:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-rendering-control-foreach
编程学习
技术分享
实战经验