一、引言:列表布局 —— 鸿蒙应用的数据展示中枢
在鸿蒙应用开发体系中,列表布局是处理结构化数据展示的核心场景。从新闻资讯的信息流、电商平台的商品陈列到任务管理的待办事项,几乎所有中大型应用都依赖高效的列表组件实现数据可视化。鸿蒙提供的 List、ListItem、ListItemGroup 三件套组件,通过标准化的接口设计与分层架构,构建了一套完整的列表解决方案。本文将系统解析这三个组件的核心机制、进阶用法与工程实践,帮助开发者掌握高性能列表开发的鸿蒙范式。
二、核心组件架构与协作机制
2.1 组件层级与职责划分
鸿蒙列表体系采用三层架构设计:
- List:列表容器组件,负责整体布局控制、滚动管理与性能优化
- ListItem:列表项原子单元,承载具体数据展示与交互逻辑
- ListItemGroup:列表分组组件,实现数据逻辑分组与吸顶吸底效果
组件层级关系示意图:
List
├─ ListItemGroup(分组容器)
│ ├─ ListItem(列表项1)
│ ├─ ListItem(列表项2)
├─ ListItem(独立列表项)
2.2 核心技术优势
- 标准化交互模型:内置滑动删除、选中状态、编辑模式等通用交互
- 高性能渲染引擎:支持懒加载、预渲染与虚拟列表优化
- 语义化分组能力:通过 ListItemGroup 实现数据分层与视觉分组
- 多端自适应:自动适配手机、平板、车机等不同设备的屏幕特性
三、List 组件:列表布局的总控制器
3.1 基础接口与布局控制
// xxx.ets
import { ListDataSource } from './ListDataSource';
@Entry
@Component
struct ListLanesExample {
arr: ListDataSource = new ListDataSource([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]);
@State alignListItem: ListItemAlign = ListItemAlign.Start;
build() {
Column() {
List({ space: 20, initialIndex: 0 }) {
LazyForEach(this.arr, (item: string) => {
ListItem() {
Text('' + item)
.width('100%')
.height(100)
.fontSize(16)
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
}
.border({ width: 2, color: Color.Green })
}, (item: string) => item)
}
.height(300)
.width('90%')
.friction(0.6)
.border({ width: 3, color: Color.Red })
.lanes({ minLength: 40, maxLength: 40 })
.alignListItem(this.alignListItem)
.scrollBar(BarState.Off)
Button('点击更改alignListItem:' + this.alignListItem).onClick(() => {
if (this.alignListItem == ListItemAlign.Start) {
this.alignListItem = ListItemAlign.Center;
} else if (this.alignListItem == ListItemAlign.Center) {
this.alignListItem = ListItemAlign.End;
} else {
this.alignListItem = ListItemAlign.Start;
}
})
}.width('100%').height('100%').backgroundColor(0xDCDCDC).padding({ top: 5 })
}
}
3.2 滚动事件与交互控制
// ListDataSource.ets
export class ListDataSource implements IDataSource {
private list: number[] = [];
private listeners: DataChangeListener[] = [];
constructor(list: number[]) {
this.list = list;
}
totalCount(): number {
return this.list.length;
}
getData(index: number): number {
return this.list[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener);
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
this.listeners.splice(pos, 1);
}
}
// 通知控制器数据删除
notifyDataDelete(index: number): void {
this.listeners.forEach(listener => {
listener.onDataDelete(index);
});
}
// 通知控制器添加数据
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => {
listener.onDataAdd(index);
});
}
// 在指定索引位置删除一个元素
public deleteItem(index: number): void {
this.list.splice(index, 1);
this.notifyDataDelete(index);
}
// 在指定索引位置插入一个元素
public insertItem(index: number, data: number): void {
this.list.splice(index, 0, data);
this.notifyDataAdd(index);
}
}
// xxx.ets
import { ListDataSource } from './ListDataSource';
@Entry
@Component
struct ListExample {
private arr: ListDataSource = new ListDataSource([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
build() {
Column() {
List({ space: 20, initialIndex: 0 }) {
LazyForEach(this.arr, (item: number) => {
ListItem() {
Text('' + item)
.width('100%').height(100).fontSize(16)
.textAlign(TextAlign.Center).borderRadius(10).backgroundColor(0xFFFFFF)
}
}, (item: string) => item)
}
.listDirection(Axis.Vertical) // 排列方向
.scrollBar(BarState.Off)
.friction(0.6)
.divider({ strokeWidth: 2, color: 0xFFFFFF, startMargin: 20, endMargin: 20 }) // 每行之间的分界线
.edgeEffect(EdgeEffect.Spring) // 边缘效果设置为Spring
.onScrollIndex((firstIndex: number, lastIndex: number, centerIndex: number) => {
console.info('first' + firstIndex);
console.info('last' + lastIndex);
console.info('center' + centerIndex);
})
.onScrollVisibleContentChange((start: VisibleListContentInfo, end: VisibleListContentInfo) => {
console.info(' start index: ' + start.index +
' start item group area: ' + start.itemGroupArea +
' start index in group: ' + start.itemIndexInGroup);
console.info(' end index: ' + end.index +
' end item group area: ' + end.itemGroupArea +
' end index in group: ' + end.itemIndexInGroup);
})
.onDidScroll((scrollOffset: number, scrollState: ScrollState) => {
console.info(`onScroll scrollState = ScrollState` + scrollState + `, scrollOffset = ` + scrollOffset);
})
.width('90%')
}
.width('100%')
.height('100%')
.backgroundColor(0xDCDCDC)
.padding({ top: 5 })
}
}
3.3 性能优化属性
属性名称 | 类型 | 功能描述 |
---|---|---|
cachedCount | number | 预加载相邻项数量,默认值 5,提升滚动流畅度 |
itemSize | number | 固定列表项高度,避免动态计算布局开销 |
layoutWeight | number | 弹性布局权重,配合 ListItem 使用 |
useVirtualized | boolean | 启用虚拟列表模式,仅渲染可见区域(API 10+) |
四、ListItem 组件:列表项的原子实现单元
4.1 基础结构与样式配置
// xxx.ets
export class ListDataSource implements IDataSource {
private list: number[] = [];
constructor(list: number[]) {
this.list = list;
}
totalCount(): number {
return this.list.length;
}
getData(index: number): number {
return this.list[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
}
unregisterDataChangeListener(listener: DataChangeListener): void {
}
}
@Entry
@Component
struct ListItemExample {
private arr: ListDataSource = new ListDataSource([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
build() {
Column() {
List({ space: 20, initialIndex: 0 }) {
LazyForEach(this.arr, (item: number) => {
ListItem() {
Text('' + item)
.width('100%')
.height(100)
.fontSize(16)
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
}
}, (item: string) => item)
}.width('90%')
.scrollBar(BarState.Off)
}.width('100%').height('100%').backgroundColor(0xDCDCDC).padding({ top: 5 })
}
}
4.2 交互能力实现
ListItem()
.selectable(true) // 可选中状态
.selected($$this.isSelected) // 双向绑定选中状态
.onSelect((selected: boolean) => {
// 选中状态变更回调
console.log(`选中状态: ${selected}`);
})
.swipeAction({ // 滑动操作
end: { // 向右滑动显示
builder: () => Row()
}
})
4.3 卡片样式优化(API 10+)
// xxx.ets
@Entry
@Component
struct ListItemExample3 {
build() {
Column() {
List({ space: '4vp', initialIndex: 0 }) {
ListItemGroup({ style: ListItemGroupStyle.CARD }) {
ForEach([ListItemStyle.CARD, ListItemStyle.CARD, ListItemStyle.NONE], (itemStyle: number, index?: number) => {
ListItem({ style: itemStyle }) {
Text('' + index)
.width('100%')
.textAlign(TextAlign.Center)
}
})
}
ForEach([ListItemStyle.CARD, ListItemStyle.CARD, ListItemStyle.NONE], (itemStyle: number, index?: number) => {
ListItem({ style: itemStyle }) {
Text('' + index)
.width('100%')
.textAlign(TextAlign.Center)
}
})
}
.width('100%')
.multiSelectable(true)
.backgroundColor(0xDCDCDC)
}
.width('100%')
.padding({ top: 5 })
}
}
五、ListItemGroup 组件:列表的逻辑分组器
5.1 分组结构与吸顶效果
// ListDataSource.ets
export class TimeTableDataSource implements IDataSource {
private list: TimeTable[] = [];
private listeners: DataChangeListener[] = [];
constructor(list: TimeTable[]) {
this.list = list;
}
totalCount(): number {
return this.list.length;
}
getData(index: number): TimeTable {
return this.list[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
this.listeners.push(listener);
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
this.listeners.splice(pos, 1);
}
}
// 通知控制器数据变化
notifyDataChange(index: number): void {
this.listeners.forEach(listener => {
listener.onDataChange(index);
});
}
// 修改第一个元素
public change1stItem(temp: TimeTable): void {
this.list[0] = temp;
this.notifyDataChange(0);
}
}
export class ProjectsDataSource implements IDataSource {
private list: string[] = [];
constructor(list: string[]) {
this.list = list;
}
totalCount(): number {
return this.list.length;
}
getData(index: number): string {
return this.list[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
}
unregisterDataChangeListener(listener: DataChangeListener): void {
}
}
export interface TimeTable {
title: string;
projects: string[];
}
// xxx.ets
import { TimeTable, ProjectsDataSource, TimeTableDataSource } from './ListDataSource';
@Entry
@Component
struct ListItemGroupExample {
itemGroupArray: TimeTableDataSource = new TimeTableDataSource([]);
aboutToAppear(): void {
let timeTable: TimeTable[] = [
{
title: '星期一',
projects: ['语文', '数学', '英语']
},
{
title: '星期二',
projects: ['物理', '化学', '生物']
},
{
title: '星期三',
projects: ['历史', '地理', '政治']
},
{
title: '星期四',
projects: ['美术', '音乐', '体育']
}
];
this.itemGroupArray = new TimeTableDataSource(timeTable);
}
@Builder
itemHead(text: string) {
Text(text)
.fontSize(20)
.backgroundColor(0xAABBCC)
.width('100%')
.padding(10)
}
@Builder
itemFoot(num: number) {
Text('共' + num + '节课')
.fontSize(16)
.backgroundColor(0xAABBCC)
.width('100%')
.padding(5)
}
build() {
Column() {
List({ space: 20 }) {
LazyForEach(this.itemGroupArray, (item: TimeTable) => {
ListItemGroup({ header: this.itemHead(item.title), footer: this.itemFoot(item.projects.length) }) {
LazyForEach(new ProjectsDataSource(item.projects), (project: string) => {
ListItem() {
Text(project)
.width('100%')
.height(100)
.fontSize(20)
.textAlign(TextAlign.Center)
.backgroundColor(0xFFFFFF)
}
}, (item: string) => item)
}
.divider({ strokeWidth: 1, color: Color.Blue }) // 每行之间的分界线
})
}
.width('90%')
.sticky(StickyStyle.Header | StickyStyle.Footer)
.scrollBar(BarState.Off)
}.width('100%').height('100%').backgroundColor(0xDCDCDC).padding({ top: 5 })
}
}
5.2 分组布局规则
- 垂直布局:ListItemGroup 高度由内容自动计算,禁止显式设置 height
- 水平布局:宽度由内容自动计算,禁止显式设置 width
- 性能优化:分组内 ListItem 共享滚动上下文,减少内存占用
- 交互限制:分组头部 / 尾部不支持滑动操作,仅内容区支持
六、实战案例:从基础到复杂的列表开发
6.1 新闻资讯垂直列表
@Entry
@Component
struct NewsFeed {
// 使用类替代接口定义数据模型
@State newsData: NewsItem[] = generateNews(20) // 生成模拟数据
private dataSource: NewsDataSource = new NewsDataSource(this.newsData)
build() {
List({ space: 12 }) {
// 使用正确的LazyForEach语法
LazyForEach(this.dataSource, (item: NewsItem) => {
ListItem() {
Column() {
Image(item.image)
.width('100%')
.height(180)
.objectFit(ImageFit.Cover)
.borderRadius(4)
Text(item.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.margin({ top: 8, bottom: 4 })
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(item.summary)
.fontSize(14)
.fontColor('#666666')
.lineHeight(20)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(8)
}
}, (item: NewsItem) => item.id) // 唯一键
}
.width('100%')
.height('100%')
.onReachEnd(() => this.loadMoreNews()) // 滚动到底部加载更多
.cachedCount(8) // 预加载8项
.divider({ strokeWidth: 0.5, color: '#EEEEEE' }) // 添加分割线
}
// 加载更多数据
private loadMoreNews() {
const newItems = generateNews(10)
this.newsData = this.newsData.concat(newItems)
this.dataSource.updateData(this.newsData)
}
}
// 实现LazyForEach所需的数据源
class NewsDataSource implements IDataSource {
private data: NewsItem[] = []
private listeners: DataChangeListener[] = []
constructor(data: NewsItem[]) {
this.data = data
}
// 更新数据源
updateData(newData: NewsItem[]) {
this.data = newData
this.notifyDataReload()
}
// 通知数据变化
private notifyDataReload() {
this.listeners.forEach(listener => listener.onDataReloaded())
}
totalCount(): number {
return this.data.length
}
getData(index: number): NewsItem {
return this.data[index]
}
registerDataChangeListener(listener: DataChangeListener): void {
this.listeners.push(listener)
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const index = this.listeners.indexOf(listener)
if (index !== -1) {
this.listeners.splice(index, 1)
}
}
}
// 新闻数据模型
class NewsItem {
id: string = ''
title: string = ''
summary: string = ''
image: Resource = $r('app.media.default_news') // 默认图片资源
}
// 模拟数据生成函数
function generateNews(count: number): NewsItem[] {
const result: NewsItem[] = []
for (let i = 0; i < count; i++) {
result.push({
id: `news_${Date.now()}_${i}`,
title: `新闻标题 ${i + 1}`,
summary: `这是新闻摘要内容,展示了ArkTS新闻列表的实现方式...`,
image: $r('app.media.news_image') // 实际项目中替换为真实资源
})
}
return result
}
6.2 任务管理分组列表
@Entry
@Component
struct TaskManager {
// 任务分组数据模型
@State tasks: TaskGroup[] = [
{
title: '今日待办',
items: [
{ id: '1', title: '完成工作报告', completed: false },
{ id: '2', title: '准备会议材料', completed: false }
]
},
{
title: '已完成',
items: [
{ id: '3', title: '晨跑锻炼', completed: true },
{ id: '4', title: '回复邮件', completed: true }
]
}
]
// 更新任务状态
private updateTaskStatus(groupIndex: number, itemIndex: number, checked: boolean) {
this.tasks[groupIndex].items[itemIndex].completed = checked
// 创建新数组触发UI更新
this.tasks = [...this.tasks]
}
// 分组头部构建器
@Builder
groupHeaderBuilder(title: string) {
Text(title)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.padding({ top: 20, bottom: 12, left: 16 })
.width('100%')
.backgroundColor('#f5f5f5')
}
build() {
List({ space: 8 }) {
ForEach(this.tasks, (group: TaskGroup, groupIndex: number) => {
ListItemGroup({ header: this.groupHeaderBuilder(group.title) }) {
ForEach(group.items, (task: TaskItem, itemIndex: number) => {
ListItem() {
Row() {
Checkbox()
.onChange((checked: boolean) => {
this.updateTaskStatus(groupIndex, itemIndex, checked)
})
Text(task.title)
.margin({ left: 8 })
.fontSize(16)
}
.padding(16)
.width('100%')
}
.borderRadius(8)
.margin({ bottom: 8 })
.backgroundColor('#FFFFFF')
}, (task: TaskItem) => task.id)
}
}, (group: TaskGroup) => group.title)
}
.width('100%')
.height('100%')
.divider({ strokeWidth: 0 }) // 隐藏分割线
.listDirection(Axis.Vertical)
}
}
// 数据模型定义
class TaskGroup {
title: string = ''
items: TaskItem[] = []
}
class TaskItem {
id: string = ''
title: string = ''
completed: boolean = false
}
七、工程实践最佳指南
7.1 性能优化策略
虚拟列表实现
List() {
// 虚拟列表不需要子组件
}
.width('100%')
.height('100%')
.cachedCount(10) // 预加载项数
.onScrollIndex((start, end) => {
// 滚动事件处理(可选)
console.log(`当前可见项: ${start}-${end}`)
})
长列表优化组合
List()
.cachedCount(10) // 预加载10项
7.2 兼容性处理方案
API 分级适配
#if (API >= 9)
List()
.editMode(true)
.onItemDelete((index) => {
// 新API编辑逻辑
})
#else
List()
.onClick((index) => {
// 旧API模拟编辑逻辑
})
#endif
多端布局适配
List()
.listDirection(
DeviceType.isTablet() ? Axis.Horizontal : Axis.Vertical
)
.then((list) => {
if (DeviceType.isPhone()) {
list.itemSize(80)
} else {
list.itemSize(100)
}
})
7.3 常见问题解决方案
问题场景 | 解决方案 |
---|---|
列表滚动卡顿 | 1. 启用虚拟列表模式 .useVirtualized (true) 2. 设置固定项高度 .itemSize (80) |
分组头部不吸顶 | 确认 .sticky (StickyStyle.Header) 已设置,且 List 为垂直布局 |
滑动删除无响应 | 1. 检查 API 版本是否≥9 2. 确保 actionAreaDistance < ListItem 宽度 |
选中状态不同步 | 使用双向绑定 .selected ($isSelected),避免直接操作状态变量 |
列表项点击穿透 | 在最外层容器添加 .onClick (() => {}) 消耗点击事件 |
八、总结:三层架构构建高效列表体系
鸿蒙 List 组件体系通过 List、ListItem、ListItemGroup 的三层架构,为开发者提供了完整的列表解决方案:
- List 容器:负责整体布局控制、滚动管理与性能优化,是列表的总控制器
- ListItem 单元:承载数据展示与交互逻辑,是列表的原子组件
- ListItemGroup 分组:实现数据逻辑分组与吸顶效果,提升复杂列表的信息层级
在实际开发中,建议遵循以下最佳实践:
- 长列表优先使用 LazyForEach + 虚拟列表模式
- 复杂数据采用 ListItemGroup 进行语义化分组
- 交互操作通过组件内置 API 实现,避免自定义事件系统
- 多端适配结合 DeviceType 与条件编译实现
随着鸿蒙生态向全场景拓展,列表组件将持续进化,未来版本可能加入 AI 驱动的布局优化、3D 滚动效果等创新功能。建议开发者从基础案例入手,结合 DevEco Studio 的实时预览与性能调试工具,逐步掌握列表开发的核心技巧,为用户打造流畅、高效的数据浏览体验