HarmonyOS 6实战:简单列表折叠和展开
HarmonyOS应用开发中实现可折叠列表的两种方案:推荐使用TreeView组件,内置多级展开动画和性能优化,适合层级数据展示;或通过List组件自定义实现,结合@State状态管理、条件渲染和animateTo动画,灵活控制展开效果。关键点包括状态数组管理、条件渲染优化、平滑动画实现及大数据量性能优化(LazyForEach/cachedCount)。扩展场景支持多级嵌套、手风琴效果和状态持久
问题现象
在HarmonyOS应用开发中,开发者经常需要实现可折叠和展开的列表功能,这种交互模式在多种场景中都有广泛应用:
典型应用场景:
-
设置界面中,分类设置项的展开与收起
-
文件管理器中,目录树的展开与收起
-
聊天应用中,消息记录的折叠展示
-
电商应用中,商品分类的层级展示
-
新闻应用中,长评论的展开与收起
交互需求:
-
用户点击"展开"按钮,显示更多内容
-
用户点击"收起"按钮,隐藏详细内容
-
需要平滑的展开/收起动画效果
-
支持多级嵌套的折叠结构
-
保持列表滚动时的性能流畅
背景知识
在深入解决方案之前,我们先了解HarmonyOS中相关的组件特性:
List组件基础特性
List组件是HarmonyOS中用于展示连续、多行同类数据的核心组件:
-
支持垂直滚动,适合长列表展示
-
支持ListItem、ListItemGroup和自定义组件作为子项
-
内置虚拟化技术,优化大数据量性能
-
支持滚动事件监听和定制
TreeView组件特性
TreeView是专门为层级数据设计的组件:
-
专门用于显示嵌套结构数据
-
支持父节点和子节点的展开/折叠
-
适用于效率型应用,如文件管理器、侧边导航栏
-
内置展开/收起动画效果
折叠展开的核心原理
无论使用哪种组件,折叠展开的核心原理都是:
-
状态管理:记录每个列表项的展开状态
-
条件渲染:根据状态决定是否渲染子内容
-
动画过渡:在状态切换时添加平滑动画
-
布局计算:动态计算容器高度变化
解决方案
基于HarmonyOS的特性,我们提供两种实现方案:
方案一:使用TreeView组件(推荐)
TreeView是HarmonyOS官方提供的树形结构组件,专门为层级数据设计,提供了开箱即用的折叠展开功能。
// TreeView实现示例
import { TreeView, TreeNodeData } from '@ohos.arkui.treeview';
@Component
struct TreeViewExample {
// 树形结构数据
private treeData: TreeNodeData[] = [
{
value: '父节点1',
isExpanded: false,
children: [
{ value: '子节点1-1' },
{ value: '子节点1-2' }
]
},
{
value: '父节点2',
isExpanded: true,
children: [
{ value: '子节点2-1' },
{
value: '子节点2-2',
children: [
{ value: '孙节点2-2-1' }
]
}
]
}
];
build() {
Column() {
// 使用TreeView组件
TreeView({
data: this.treeData,
itemTemplate: (item: TreeNodeData) => {
// 自定义节点渲染
return this.renderTreeNode(item);
}
})
.width('100%')
.height('100%')
}
}
// 自定义树节点渲染
@Builder
renderTreeNode(item: TreeNodeData) {
Row() {
// 展开/收起图标
if (item.children && item.children.length > 0) {
Image($r('app.media.ic_arrow'))
.width(20)
.height(20)
.rotate({ angle: item.isExpanded ? 90 : 0 })
.animation({ duration: 200, curve: Curve.Ease })
}
Text(item.value)
.fontSize(16)
.margin({ left: 10 })
}
.width('100%')
.padding(10)
}
}
TreeView方案的优势:
-
✅ 内置展开/收起动画效果
-
✅ 支持多级嵌套结构
-
✅ 性能优化,大数据量表现良好
-
✅ 代码简洁,易于维护
适用场景:
-
文件浏览器
-
组织架构图
-
多级分类菜单
-
复杂的层级数据展示
方案二:使用List组件自定义实现
当需要更灵活的定制或项目已大量使用List组件时,可以使用List结合条件渲染来实现折叠展开功能。
// 完整实现代码
@Entry
@Component
struct ExpandableListExample {
// 列表数据
private dataItems: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
// 记录每个列表项的展开状态
@State isExpanded: boolean[] = new Array(this.dataItems.length).fill(false);
// 获取UI上下文
private uiContext: UIContext = getUIContext();
build() {
Column() {
// 列表标题
Text('可折叠列表示例')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ top: 40, bottom: 20 });
// 主列表容器
List({ space: 10, initialIndex: 0 }) {
ForEach(this.dataItems, (item: number, index: number) => {
ListItem() {
this.renderListItem(item, index)
}
}, (item: number) => item.toString())
}
.width('100%')
.height('100%')
.scrollBar(BarState.Auto)
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor('#F8F9FA')
}
// 构建列表项
@Builder
renderListItem(item: number, index: number) {
Column({ space: 0 }) {
// 顶部控制区域
Row() {
// 项目序号
Text(`项目 ${item}`)
.fontSize(18)
.fontWeight(FontWeight.Medium)
.fontColor('#1A1A1A')
// 右侧占位,用于对齐按钮
Blank()
// 展开/收起按钮
Button(this.isExpanded[index] ? '收起 ▲' : '展开 ▼')
.fontSize(14)
.fontColor('#0066CC')
.backgroundColor('#FFFFFF')
.borderRadius(20)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.border({ width: 1, color: '#0066CC' })
.onClick(() => {
this.toggleExpand(index);
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.justifyContent(FlexAlign.SpaceBetween)
// 分隔线
Divider()
.color('#E0E0E0')
.strokeWidth(1)
.margin({ left: 16, right: 16 })
// 可展开的内容区域
if (this.isExpanded[index]) {
this.renderExpandableContent(item, index)
}
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({ radius: 4, color: '#1A1A1A', offsetX: 0, offsetY: 2 })
}
// 构建可展开的内容
@Builder
renderExpandableContent(item: number, index: number) {
Column({ space: 8 }) {
// 内容标题
Text('详细内容')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.margin({ top: 12, left: 16, right: 16 })
// 内容正文
Text('这是项目' + item + '的详细内容区域,可以放置任何需要展开显示的内容。这里可以包含文本、图片、按钮等任何组件。折叠展开功能让界面更加简洁,同时不丢失信息的完整性。')
.fontSize(14)
.fontColor('#666666')
.lineHeight(20)
.textOverflow({ overflow: TextOverflow.Ellipsis })
.maxLines(3)
.margin({ left: 16, right: 16, bottom: 8 })
// 内容操作按钮区域
Row({ space: 12 }) {
Button('操作1')
.fontSize(12)
.backgroundColor('#F0F7FF')
.fontColor('#0066CC')
.borderRadius(6)
Button('操作2')
.fontSize(12)
.backgroundColor('#F0F7FF')
.fontColor('#0066CC')
.borderRadius(6)
}
.margin({ left: 16, right: 16, bottom: 16 })
.justifyContent(FlexAlign.Start)
}
.width('100%')
}
// 切换展开状态(带动画)
private toggleExpand(index: number): void {
this.uiContext.animateTo({
duration: 300, // 动画持续时间300ms
curve: Curve.EaseInOut, // 缓动曲线
onFinish: () => {
// 动画完成回调
console.info(`第${index}项的${this.isExpanded[index] ? '收起' : '展开'}动画完成`);
}
}, () => {
// 在动画上下文中更新状态
this.isExpanded[index] = !this.isExpanded[index];
});
}
}
关键点分析
1. 状态管理的实现
折叠展开功能的核心是状态管理。我们使用@State装饰器来管理每个列表项的展开状态:
// 创建与数据项相同长度的布尔数组
@State isExpanded: boolean[] = new Array(dataItems.length).fill(false);
// 初始化所有项为收起状态
// true表示展开,false表示收起
状态管理的最佳实践:
-
使用数组存储每个列表项的状态,便于通过索引访问
-
初始状态设为
false(默认收起) -
状态变化触发UI自动刷新
2. 条件渲染的实现
HarmonyOS使用条件渲染语句来控制组件的显示与隐藏:
// 条件渲染语法
if (this.isExpanded[index]) {
// 当条件为true时渲染的内容
this.renderExpandableContent(item, index)
}
条件渲染的特点:
-
基于布尔值的动态渲染
-
支持复杂的条件表达式
-
可以与动画效果结合使用
-
性能优化:不渲染隐藏的组件
3. 动画效果的实现
平滑的动画效果能显著提升用户体验。我们使用animateTo方法实现:
// 创建动画上下文
this.uiContext.animateTo({
duration: 300, // 动画持续时间
curve: Curve.EaseInOut, // 缓动曲线
onFinish: () => {
// 动画完成后的回调
}
}, () => {
// 在动画上下文中更新状态
this.isExpanded[index] = !this.isExpanded[index];
});
动画参数详解:
-
duration: 动画持续时间,单位毫秒 -
curve: 动画曲线,控制动画速度变化-
Curve.Linear: 线性 -
Curve.Ease: 缓入缓出 -
Curve.EaseIn: 缓入 -
Curve.EaseOut: 缓出 -
Curve.EaseInOut: 缓入缓出
-
-
onFinish: 动画完成回调函数
4. 性能优化考虑
在处理大量列表项时,性能优化至关重要:
// 使用LazyForEach优化大数据量
LazyForEach(this.dataSource, (item: DataItem) => {
ListItem() {
this.renderListItem(item)
}
}, (item: DataItem) => item.id.toString())
// 虚拟化技术
List({ scroller: this.listScroller })
.cachedCount(5) // 预加载数量
.edgeEffect(EdgeEffect.None) // 禁用边缘效果提升性能
扩展应用
场景一:多级嵌套折叠
在实际应用中,我们经常需要多级嵌套的折叠结构:
@Component
struct MultiLevelExpandableList {
@State expandedLevel1: boolean[] = [false, false, false];
@State expandedLevel2: boolean[][];
aboutToAppear() {
// 初始化二级展开状态
this.expandedLevel2 = [
[false, false],
[false, false, false],
[false]
];
}
build() {
List() {
ForEach(this.expandedLevel1, (_, level1Index) => {
ListItem() {
Column() {
// 一级项目
this.renderLevel1Item(level1Index)
// 二级项目(条件渲染)
if (this.expandedLevel1[level1Index]) {
ForEach(this.expandedLevel2[level1Index], (_, level2Index) => {
this.renderLevel2Item(level1Index, level2Index)
})
}
}
}
})
}
}
}
场景二:手风琴效果(同时只展开一项)
某些场景需要手风琴效果,即同时只能展开一个项目:
@Component
struct AccordionList {
@State expandedIndex: number = -1; // -1表示没有展开项
toggleExpand(index: number): void {
this.uiContext.animateTo({
duration: 300,
curve: Curve.EaseInOut
}, () => {
if (this.expandedIndex === index) {
// 点击已展开的项,收起
this.expandedIndex = -1;
} else {
// 点击其他项,展开新项
this.expandedIndex = index;
}
});
}
build() {
List() {
ForEach(this.dataItems, (item, index) => {
ListItem() {
Column() {
// 列表项标题
this.renderItemHeader(item, index)
// 内容区域
if (index === this.expandedIndex) {
this.renderItemContent(item)
}
}
}
})
}
}
}
场景三:记住展开状态
在应用重启后保持用户的展开状态:
import { PersistentStorage } from '@ohos.data.preferences';
@Component
struct RememberExpandedState {
@State isExpanded: boolean[] = [];
private preferences: PersistentStorage.Preferences | null = null;
async aboutToAppear() {
// 初始化Preferences
this.preferences = await PersistentStorage.getPreferences(this.context, 'expandState');
// 从持久化存储加载状态
const savedState = await this.preferences.get('expandedStates', '[]');
this.isExpanded = JSON.parse(savedState);
}
async toggleExpand(index: number) {
this.uiContext.animateTo({
duration: 300
}, () => {
this.isExpanded[index] = !this.isExpanded[index];
// 保存状态到持久化存储
this.saveExpandedState();
});
}
async saveExpandedState() {
if (this.preferences) {
await this.preferences.put('expandedStates', JSON.stringify(this.isExpanded));
await this.preferences.flush();
}
}
}
常见问题与解决方案
Q1: 展开内容时列表跳动或闪烁?
问题原因:
-
高度计算不准确
-
动画与布局更新不同步
-
条件渲染触发时机问题
解决方案:
// 1. 使用固定高度或最大高度
if (this.isExpanded[index]) {
Column() {
// 展开内容
}
.height(200) // 固定高度
.clip(false) // 允许内容溢出
}
// 2. 使用布局过渡动画
.animation({
duration: 300,
curve: Curve.EaseInOut,
delay: 0,
iterations: 1,
playMode: PlayMode.Normal
})
// 3. 确保状态更新在动画上下文中
this.uiContext.animateTo({
duration: 300
}, () => {
this.isExpanded[index] = !this.isExpanded[index];
});
Q2: 大数据量时性能下降?
优化策略:
// 1. 使用LazyForEach懒加载
LazyForEach(this.dataSource, (item) => {
ListItem() {
// 列表项内容
}
})
// 2. 设置合理的缓存数量
List({ initialIndex: 0 })
.cachedCount(10) // 预渲染前后10个项目
// 3. 避免复杂计算在渲染过程中
aboutToAppear() {
// 在组件出现前预处理数据
this.preprocessData();
}
// 4. 使用shouldComponentUpdate优化
shouldComponentUpdate(params: Record<string, unknown>): boolean {
// 只在自己相关的状态变化时更新
return params.index === this.index;
}
Q3: 嵌套滚动冲突?
问题场景:
展开内容内部有可滚动区域,与外部List滚动冲突。
解决方案:
// 内部滚动区域禁用滚动,由外部List统一控制
Scroll() {
// 内部内容
}
.scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Off)
.onScrollEdge((side: ScrollEdge) => {
// 滚动到边缘时传递给外部
if (side === ScrollEdge.Top || side === ScrollEdge.Bottom) {
return; // 允许外部List继续滚动
}
})
Q4: 展开动画不流畅?
优化方法:
// 1. 使用硬件加速
.transform({
translate: { x: 0, y: 0, z: 0 }
})
.backdropBlur(0) // 启用硬件加速
// 2. 优化动画曲线
animateTo({
duration: 300,
curve: Curve.Friction // 物理曲线,更自然
})
// 3. 减少重绘区域
.clip(true) // 裁剪超出部分
.opacity(1) // 避免透明度变化
总结与最佳实践
方案选择建议
|
场景特点 |
推荐方案 |
理由 |
|---|---|---|
|
简单的展开收起 |
List + 条件渲染 |
灵活控制,代码简单 |
|
多级嵌套结构 |
TreeView |
内置支持,开发效率高 |
|
大数据量 |
List + LazyForEach |
性能优化,内存占用低 |
|
需要复杂动画 |
List + animateTo |
动画控制更精细 |
|
需要记住状态 |
任意方案 + PersistentStorage |
状态持久化 |
性能优化清单
-
数据层面
-
使用懒加载(LazyForEach)
-
分页加载大数据集
-
避免在渲染过程中进行复杂计算
-
-
渲染层面
-
设置合理的cachedCount
-
使用shouldComponentUpdate避免不必要渲染
-
简化组件结构,减少嵌套
-
-
动画层面
-
使用硬件加速
-
优化动画曲线
-
避免同时执行多个复杂动画
-
-
内存层面
-
及时清理不需要的状态
-
使用轻量级数据格式
-
监控内存使用情况
-
代码规范建议
// ✅ 推荐写法
@Component
struct ExpandableList {
// 明确的命名
@State private expandedStates: boolean[] = [];
// 单一职责的方法
private toggleItem(index: number): void {
this.uiContext.animateTo({
duration: 300,
curve: Curve.EaseInOut
}, () => {
this.expandedStates[index] = !this.expandedStates[index];
});
}
// 清晰的组件结构
@Builder
renderListItem(item: DataItem, index: number) {
Column() {
this.renderHeader(item, index)
this.renderContent(item, index)
}
}
}
// ❌ 不推荐写法
@Component
struct BadExample {
@State flag: boolean[] = []; // 命名不清晰
// 方法职责不单一
// 组件结构混乱
}
测试要点
在实现折叠展开功能后,需要进行全面测试:
功能测试:
-
点击展开/收起按钮功能正常
-
动画流畅,无卡顿
-
多级嵌套展开正确
-
手风琴模式工作正常
性能测试:
-
100+项目流畅滚动
-
展开收起响应迅速
-
内存占用合理
-
动画帧率稳定
兼容性测试:
-
不同设备尺寸适配
-
横竖屏切换正常
-
深色/浅色主题适配
-
不同HarmonyOS版本兼容
用户体验测试:
-
动画自然流畅
-
交互反馈及时
-
状态记忆正确
-
无障碍功能支持
通过本文的详细介绍,相信你已经掌握了在HarmonyOS 6中实现列表折叠展开功能的多种方法。无论选择TreeView还是自定义List方案,关键是根据实际业务需求选择最合适的方案,并结合性能优化和用户体验考虑,打造出既美观又实用的列表交互体验。
在实际开发中,建议先使用TreeView快速实现基础功能,如果需要更复杂的定制再考虑使用List自定义实现。同时,不要忘记性能优化和用户体验的细节,这些都是打造高质量应用的关键。
更多推荐

所有评论(0)