鸿蒙应用开发UI基础第二十九节:列表布局核心 —— List 组件全能力详解
本文介绍了鸿蒙应用开发中List组件的核心使用方法,包括List、ListItem、ListItemGroup三大组件的层级关系和布局概念。重点讲解了主轴与交叉轴布局原理,以及常用属性如divider、scrollBar、sticky等的配置方法。通过工程目录展示和基础示例代码,演示了如何构建静态列表,包括垂直/水平布局、单列/多列切换等功能。文章还介绍了Scroller控制器的使用场景,为后续实
【学习目标】
- 掌握 List、ListItem、ListItemGroup 三大核心组件的组合使用;
- 理解 List 的主轴与交叉轴布局概念,掌握基础布局、样式配置全能力;
- 熟练运用 List 核心属性(sticky、divider、scrollBar、edgeEffect)实现业务效果;
- 掌握 Scroller 滚动控制器的使用,实现返回顶部、指定位置跳转等交互;
- 通过 List+Scroller+AlphabetIndexer 组件完成模仿微信联系人。
一、本节工程目录结构
ListDemo/
├── entry/
│ └── src/main/
│ ├── ets/
│ │ ├── pages/
│ │ │ ├── Index.ets # 首页:功能导航入口
│ │ │ ├── BasicListDemo.ets # 基础静态列表示例
│ │ │ └── WeChatContactsDemo.ets # 仿微信通讯录
│ │ ├── model/
│ │ │ └── ContactModel.ets # 数据模型
│ │ ├── utils/
│ │ │ └── JsonUtil.ets # JSON工具类
│ └── resources/
│ └── rawfile/
│ └── contacts.json # 联系人数据
└── build-profile.json5
二、List 核心认知
2.1 什么是 List?
List 是鸿蒙应用中用于构建可滚动列表的核心容器组件,专门用于呈现同类数据集合,当列表内容超出屏幕大小时,会自动提供滚动能力。它是移动端开发中最高频的组件之一,系统设置页、通讯录、商品列表、消息列表等场景均基于 List 实现。
2.2 核心组成三要素
List 的使用有严格的父子组件约束,必须由以下三层结构构成,否则会触发编译警告或渲染异常:
- List:根容器,负责列表的整体布局规则、滚动控制、全局样式配置,是整个列表的载体。
- ListItemGroup(可选):分组容器,专门用于列表的分组展示、粘性吸顶效果,其子组件只能是 ListItem。
- ListItem:列表项容器,列表的每一行/每一项内容,必须包裹在 ListItem 内,且它必须是 List 或 ListItemGroup 的直接子元素。ListItem 内部只能包含一个根组件,可通过 Row/Column 等容器嵌套多个子组件。
强制规则:List 的子组件必须是 ListItem 或 ListItemGroup;ListItem 和 ListItemGroup 必须配合 List 使用,不可单独在其他容器中使用。
2.3 核心布局概念:主轴与交叉轴
列表的布局方向、滚动能力,都围绕主轴和交叉轴展开,这是理解 List 布局的核心:
- 主轴:列表项的排列方向,也是列表的滚动方向。
- 默认值:
Axis.Vertical垂直方向,主轴为垂直轴,列表上下滚动; - 可配置:
Axis.Horizontal水平方向,主轴为水平轴,列表左右滚动。
- 默认值:
- 交叉轴:与主轴相互垂直的方向。
- 垂直列表的交叉轴是水平方向,控制列表项的水平对齐、列数;
- 水平列表的交叉轴是垂直方向,控制列表项的垂直对齐、行数。
三、核心组件常用接口
3.1 List 根容器组件
构造函数
| 参数名 | 说明 |
|---|---|
| space | 列表项之间沿主轴方向的间距 |
| scroller | 滚动控制器,用于手动控制列表滚动位置 |
| initialIndex | 列表首次渲染时,默认显示的项索引,默认 0 |
常用布局属性
| 属性 | 说明 |
|---|---|
| listDirection(value: Axis) | 设置列表主轴方向(滚动方向) • Vertical:垂直列表 • Horizontal:水平列表 |
| lanes(value) | 设置交叉轴排列的列表项数量 • 数字:固定列数 • 对象:自适应列数 |
| alignListItem(value) | 设置列表项在交叉轴方向的对齐方式 |
常用样式属性
| 属性 | 说明 |
|---|---|
| divider(options) | 设置列表项之间的分隔线 • strokeWidth:粗细(必填) • startMargin:左侧间距 • endMargin:右侧间距 • color:颜色 |
| scrollBar(value) | 滚动条显示策略 • Auto:触摸显示 • Off:始终隐藏 • On:始终显示 |
| sticky(value) | 粘性标题(配合 ListItemGroup) • Header:头部吸顶 • Footer:底部吸底 |
| edgeEffect(value) | 边缘回弹效果 • Spring:弹性回弹 • None:无效果 |
| cachedCount(value) | 懒加载缓存数量,配合 LazyForEach 使用 |
核心事件回调
| 事件 | 说明 |
|---|---|
| onReachEnd() | 列表滚动到底部时触发,用于上拉加载更多 |
| onReachStart() | 列表滚动到顶部时触发 |
| onScrollIndex((first, last) => {}) | 可视区域索引变化时触发 |
| onScroll((x, y) => {}) | 滚动过程中持续触发 |
| onScrollStop() | 滚动停止时触发 |
3.2 ListItem 列表项组件
列表的最小单元,必须作为 List 或 ListItemGroup 的直接子组件。
构造函数
| 参数 | 说明 |
|---|---|
| style | 列表项样式 • Normal:普通样式 • CARD:卡片样式(自带圆角、内边距、阴影) |
常用属性
| 属性 | 说明 |
|---|---|
| swipeAction(options) | 配置侧滑菜单(start 右滑 / end 左滑) |
| selected(value) | 设置列表项是否选中 |
| draggable(value) | 是否启用拖拽,用于拖拽排序 |
3.3 ListItemGroup 分组容器组件
用于实现列表的分组展示,配合 sticky 属性实现粘性吸顶标题,是联系人列表、设置页的核心组件。
构造函数
| 参数 | 说明 |
|---|---|
| header | 分组头部组件(@Builder 函数) |
| footer | 分组尾部组件(@Builder 函数) |
| style | 分组样式 • None:默认 • CARD:卡片分组 |
3.4 Scroller 滚动控制器
用于手动控制列表的滚动行为,比如返回顶部、滚动到指定位置、滚动到列表边缘等。
核心方法
| 方法 | 说明 |
|---|---|
| scrollToIndex(index, smooth, align) | 滚动到指定索引项,smooth 开启平滑滚动 |
| scrollEdge(edge, smooth) | 滚动到边缘:Top / Bottom / Start / End |
| scrollTo(x, y, animation) | 滚动到指定偏移量位置 |
四、基础实战:从零构建静态列表
4.1 最简静态列表
// pages/BasicListDemo.ets
@Entry
@Component
struct BasicListDemo {
@State lanes:number = 1;
@State axis:Axis = Axis.Vertical;
build() {
// List 根容器
List({ space: 1 }) {
// 每一行内容必须用 ListItem 包裹
ListItem() {
Text('单列布局')
.fontSize(18)
.width('100%')
.padding(16)
.onClick(()=>{
this.lanes = 1
})
}
ListItem() {
Text('双列布局')
.fontSize(18)
.width('100%')
.padding(16)
.onClick(()=>{
this.lanes = 2
})
}
ListItem() {
Text('垂直排列')
.fontSize(18)
.width('100%')
.padding(16)
.onClick(()=>{
this.axis = Axis.Vertical;
})
}
ListItem() {
Text('横向排列')
.fontSize(18)
.width('100%')
.padding(16)
.onClick(()=>{
this.axis = Axis.Horizontal;
})
}
}
.backgroundColor('#F5F5F5')
.listDirection(this.axis) // 默认垂直布局Axis.Vertical,设置Horizontal水平布局
.scrollBar(BarState.Off) // 隐藏滚动条
.divider({ // 核心:分隔线配置
strokeWidth: 0.5, // 分隔线粗细
startMargin: 56, // 距离左侧56vp
endMargin: 16, // 距离右侧16vp
color: '#E5E5E5' // 分隔线颜色
})
.lanes(this.lanes) // 核心:设置交叉轴列数为2,实现两列网格 通常用于动态适配不同的屏幕宽度
.width('100%')
.height('100%')
}
}
这是最直观的示例,没有使用循环遍历组件。List作为根容器,每一行列表元素由ListItem包裹Text组件,给List设置不同的链式属性添加不同的样式。
五、仿微信联系人页面
我们使用ForEach+List复刻一个微信通讯录:
- 实现分组每一组标题是大写首字母;
- 实现侧边字母索引,选中字母快速滚动到对应分组;
- 侧滑拉出备注菜单。
- 点击或按压有反馈效果
5.1 准备数据
我们需要准备一份本地数据contacts.json:
[
{
"initial": "A",
"list": [
{ "name": "艾小米", "avatar": "https://picsum.photos/id/1/200/200" },
{ "name": "安小北", "avatar": "https://picsum.photos/id/2/200/200" }
]
},
// 总共52条数据 每个字母对应两条联系人信息,完整数据内容通过代码仓库下载。或自行生成属性字段不变就可以。
]
5.2 配置:module.json5申请联网权限
{
"module": {
"requestPermissions": [
{ // 图片通过网络加载,需要开通连网权限
"name": "ohos.permission.INTERNET"
}
]
}
}
5.3 JSON数据读取
JSON数据读取(零基础鸿蒙应用开发第三十二节:JSON核心基础与文件的读写)已学习过,完整代码如下:
// main/ets/utils/JsonUtil.ets
import { BusinessError } from '@kit.BasicServicesKit';
import { util } from '@kit.ArkTS';
import { Context } from '@kit.AbilityKit';
import fs from '@ohos.file.fs';
export class JsonUtil {
/**
* 读取 rawfile 目录下的 JSON 文件并返回字符串内容
* @param context 应用/组件上下文
* @param fileName JSON 文件名(支持子目录如 "user/user_info.json")
* @returns JSON 字符串
*/
static async readRawFileJson(context: Context, fileName: string): Promise<string> {
try {
const buffer = await context.resourceManager.getRawFileContent(fileName);
const uint8Array = new Uint8Array(buffer);
const decoder = new util.TextDecoder();
return decoder.decodeToString(uint8Array);
} catch (err) {
const error = err as BusinessError;
const msg = `读取JSON文件 ${fileName} 失败:${error.code} - ${error.message}`;
console.error(msg);
throw new Error(msg);
}
}
/**
* 写入JSON数据到应用沙箱的cache目录(可读写)
* @param context 应用上下文
* @param fileName 文件名(如 "user_config.json")
* @param data 要序列化的对象
*/
static async writeSandboxJson(context: Context, fileName: string, data: object): Promise<void> {
let file: fs.File | undefined = undefined;
try {
const cacheDir = context.cacheDir;
const filePath = `${cacheDir}/${fileName}`;
const jsonStr = JSON.stringify(data, null, 2);
// 打开/创建文件
file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
// 写入数据
fs.writeSync(file.fd, jsonStr);
console.log(`JSON数据已写入沙箱:${filePath}`);
} catch (err) {
const error = err as BusinessError;
const msg = `写入沙箱JSON文件 ${fileName} 失败:${error.code} - ${error.message}`;
console.error(msg);
throw new Error(msg);
} finally {
// 无论是否异常,都要关闭文件句柄,避免泄漏
if (file) {
fs.closeSync(file);
}
}
}
/**
* 从应用沙箱的cache目录读取JSON文件
* @param context 应用上下文
* @param fileName 文件名
* @returns JSON 字符串
*/
static async readSandboxJson(context: Context, fileName: string): Promise<string> {
let file: fs.File | undefined = undefined;
try {
const cacheDir = context.cacheDir;
const filePath = `${cacheDir}/${fileName}`;
// 1. 检查文件是否存在
fs.accessSync(filePath);
// 2. 打开文件(只读模式)
file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
// 3. 获取文件大小
const fileStat = fs.statSync(filePath);
// 4. 创建缓冲区读取数据
const buffer = new ArrayBuffer(fileStat.size);
const readBytes = fs.readSync(file.fd, buffer, { length: fileStat.size });
// 5. 校验读取完整性
if (readBytes !== fileStat.size) {
throw new Error(`文件读取不完整:预期${fileStat.size}字节,实际${readBytes}字节`);
}
// 6. 解码为字符串
const decoder = new util.TextDecoder();
return decoder.decodeToString(new Uint8Array(buffer));
} catch (err) {
const error = err as BusinessError;
const msg = `读取沙箱JSON文件 ${fileName} 失败:${error?.code} - ${error?.message || err}`;
console.error(msg);
throw new Error(msg);
} finally {
if (file) {
fs.closeSync(file);
}
}
}
/**
* 类型安全的JSON反序列化
* @param jsonStr JSON字符串
* @returns 指定类型的对象
*/
static parseJson<T>(jsonStr: string): T {
try {
return JSON.parse(jsonStr) as T;
} catch (err) {
const msg = `JSON反序列化失败:${(err as Error).message}`;
console.error(msg);
throw new Error(msg);
}
}
}
5.4 联系人数据模型
/**
* 单个联系人模型
* 对应 json 里的 list 项
*/
export interface ContactItem {
name: string;
avatar: string;
}
/**
* 联系人分组模型
* 对应 json 里的每一组数据
*/
export interface ContactGroup {
initial: string;
list: ContactItem[];
}
5.4 微信联系人页面
import { ContactGroup, ContactItem } from '../model/ContactModel';
import { JsonUtil } from '../utils/JsonUtil';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct WeChatContactsPage {
private scroller: Scroller = new Scroller();
private context: Context | undefined = this.getUIContext().getHostContext();
@State contactGroups: ContactGroup[] = [];
@State letterList: string[] = [];
async aboutToAppear() {
try {
if (this.context) {
// 读取联系人信息
const jsonStr = await JsonUtil.readRawFileJson(this.context, 'contacts.json');
// 反序列化数据模型
const contactGroups = JsonUtil.parseJson<ContactGroup[]>(jsonStr);
this.contactGroups = contactGroups;
// 生成索引需要的字母数组
this.letterList = this.contactGroups.map(g => g.initial);
}
} catch (err) {
console.error("加载失败", JSON.stringify(err as BusinessError));
}
}
// 分组标题
@Builder
groupHeaderBuilder(title: string) {
Text(title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.width('100%')
.padding({ left: 16, top: 8, bottom: 8 })
.backgroundColor('#F7F7F7')
}
// 联系人条目
@Builder
contactItemViewBuilder(item: ContactItem, index: number, count: number) {
Column() {
Row() {
Image(item.avatar)
.width(44)
.height(44)
.borderRadius(6)
.margin({ right: 15 });
Text(item.name)
.fontSize(17)
.fontColor('#000')
}
.width('100%')
.padding({ left: 15, right: 15, top: 12, bottom: 12 })
Divider()
.strokeWidth(0.5)
.color('#E5E5E5')
.padding({ left: 74, right: 0 })
.visibility(index + 1 === count ? Visibility.Hidden : Visibility.Visible)
}
}
// 侧滑菜单
@Builder
SwipeMenu() {
Row() {
Text("备注")
.layoutWeight(1)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.height("100%")
.backgroundColor("#007AFF")
Text("删除")
.layoutWeight(1)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.height("100%")
.backgroundColor("#FF3B30")
}.width(150)
}
build() {
RelativeContainer() {
// 1. 顶部固定标题
Text("微信通讯录")
.fontSize(18)
.fontWeight(FontWeight.Medium)
.textAlign(TextAlign.Center)
.padding(12)
.backgroundColor('#FFF')
.id('constant_title')
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top },
left: { anchor: '__container__', align: HorizontalAlign.Start },
right: { anchor: '__container__', align: HorizontalAlign.End }
});
// 2. 联系人列表
List({ scroller: this.scroller }) {
ForEach(this.contactGroups, (group: ContactGroup) => {
ListItemGroup({
header: this.groupHeaderBuilder(group.initial)
}) {
ForEach(group.list, (item: ContactItem, row: number) => {
ListItem({ style: ListItemStyle.NONE }) { // 默认样式可以不写
this.contactItemViewBuilder(item, row, group.list.length)
}
.stateStyles({
normal: {
.backgroundColor('#fff') // 正常白色
},
clicked: {
.backgroundColor('#F2F3F5') // 点击后的颜色
}
})
.onClick(() => {
console.log(`点击了${item.name}`)
})
.swipeAction({ end: this.SwipeMenu() })
});
};
});
}
.id('contactList')
.sticky(StickyStyle.Header) // 吸顶
.scrollBar(BarState.Off) // 关闭滚动条
.alignRules({
top: { anchor: 'constant_title', align: VerticalAlign.Bottom },
left: { anchor: '__container__', align: HorizontalAlign.Start },
bottom: { anchor: '__container__', align: VerticalAlign.Bottom },
right: { anchor: '__container__', align: HorizontalAlign.End }
});
// 3. 右侧字母索引(垂直居中,贴右边)
AlphabetIndexer({
arrayValue: this.letterList,
selected: 0 // 默认选中第1个
})
.id('alphabetIndex')
.color(Color.Black) // 未选中的文本颜色
.selectedColor(Color.White) // 设置选中项文本颜色
.selectedBackgroundColor(Color.Green) // 设置选中项背景色
.onSelect((index: number) => {
this.scroller.scrollToIndex(index, true);
})
.margin({ right: 5 })
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
right: { anchor: '__container__', align: HorizontalAlign.End }
});
}
.width('100%')
.height('100%')
}
}
运行效果
我们通过ForEach + List + AlphabetIndexer模仿了微信联系人的简单页面,熟悉了List的能力与样式设定。但当前页面存在诸多性能问题。(关于ForEach渲染细节等问题我们后边章节讲清楚)
六、内容总结
- 核心结构约束:List 组件必须配合 ListItem 使用,分组场景使用 ListItemGroup,三者构成列表的核心架构,必须遵守父子组件强制约束。
- 布局核心能力:通过
listDirection控制主轴滚动方向(垂直/水平),通过lanes控制交叉轴列数,实现单列、多列、水平列表等多种布局。 - 样式配置能力:通过
space设置列表项间距,divider配置分隔线(分组时分割线以组为单位需自定义分割线),scrollBar控制滚动条,edgeEffect配置回弹效果。 - 进阶核心能力:
- 分组列表:通过 ListItemGroup 实现功能分组,配合
.sticky(StickyStyle.Header)实现分组标题吸顶效果; - 滚动控制:通过 Scroller 控制器实现返回顶部、滚动到底部、指定索引跳转等交互;
- 事件监听:通过
onScrollIndex、onReachEnd等回调,实现列表滚动状态的监听与业务联动; - 侧边字母索引:通过
AlphabetIndexer组件及属性配置实现侧边栏字母索引。
- 分组列表:通过 ListItemGroup 实现功能分组,配合
七、代码仓库
- 工程名称:ListDemo
- 仓库地址:https://gitee.com/HarmonyOS-UI-Basics/harmony-os-ui-basics.git
八、下节预告
下一节我们深入研究ForEach的渲染过程、提升渲染性能、掌握ForEach的适用场景。
更多推荐



所有评论(0)