鸿蒙工具学习二十三:Grid容器二级子项展开收起功能实现
本文详细解析了在HarmonyOS应用开发中,如何利用Grid组件的GridLayoutOptions参数实现二级菜单的展开收起功能。文章首先分析了电商分类、功能菜单等典型应用场景及技术挑战,随后深入讲解GridLayoutOptions的核心参数和原理,包括regularSize、irregularIndexes等关键配置。通过完整的代码实现展示了动态数据管理、布局计算和状态控制的解决方案,并提
在HarmonyOS应用开发中,Grid组件作为强大的网格布局容器,广泛应用于商品展示、菜单导航、图片墙等场景。然而,当需要在网格布局中实现类似"二级菜单"的展开收起功能时,开发者常面临布局动态调整的挑战。本文将深入解析如何利用GridLayoutOptions参数,实现Grid容器中二级子GridItem的智能展开与收起功能。
一、需求场景与核心挑战
1.1 典型应用场景
|
场景类型 |
具体需求 |
交互特点 |
|---|---|---|
|
电商分类 |
商品分类网格,点击主分类展开子分类 |
需要动态插入子项,不破坏原有布局 |
|
功能菜单 |
设置菜单网格,点击主项展开详细选项 |
展开内容需要与主项视觉关联 |
|
内容导航 |
知识库目录网格,展开章节查看子节 |
需要保持网格的整体对齐性 |
1.2 技术挑战分析
-
布局动态性:Grid的布局在初始化时确定,动态添加/移除子项会破坏原有布局结构
-
空间占用:展开的二级内容需要占用额外网格空间,可能影响其他项的布局
-
动画流畅性:展开收起过程需要平滑的过渡动画,避免视觉跳跃
-
状态管理:需要精确管理每个GridItem的展开状态和位置信息
二、核心原理:GridLayoutOptions参数解析
2.1 GridLayoutOptions结构
GridLayoutOptions是控制Grid布局行为的核心配置对象,主要包含以下关键参数:
interface GridLayoutOptions {
// 规则大小:所有GridItem默认占用的行列数
regularSize?: [number, number];
// 不规则索引:指定哪些GridItem采用不规则大小
irregularIndexes?: Array<number>;
// 自定义不规则大小回调(可选)
onGetIrregularSizeByIndex?: (index: number) => [number, number];
}
2.2 参数详细说明
2.2.1 regularSize - 规则大小配置
-
作用:定义大多数GridItem的默认占用空间
-
限制:当前版本只支持
[1, 1],即每个GridItem默认占用1行1列 -
意义:确保网格布局的基础对齐性和一致性
2.2.2 irregularIndexes - 不规则索引
-
作用:指定哪些GridItem需要采用特殊大小
-
默认行为:当不设置
onGetIrregularSizeByIndex时,irregularIndexes中的GridItem默认:-
垂直滚动Grid:占用一整行
-
水平滚动Grid:占用一整列
-
-
应用场景:实现展开效果的关键,让特定GridItem可以跨越多行/列
2.2.3 onGetIrregularSizeByIndex - 自定义大小回调
-
作用:为每个不规则GridItem动态计算占用大小
-
参数:GridItem的索引
-
返回值:
[行数, 列数]数组 -
灵活性:允许根据业务逻辑动态调整每个项的大小
三、实现方案设计
3.1 整体架构思路
实现Grid二级展开收起功能的核心思路是:通过动态管理irregularIndexes和对应的数据源,控制特定GridItem的占用空间。
// 架构示意图
class ExpandableGridManager {
// 数据源:包含主项和可能的子项
private dataSource: GridItemData[] = [];
// 展开状态:记录哪些主项处于展开状态
private expandedStates: Map<number, boolean> = new Map();
// 不规则索引:动态计算哪些项需要特殊大小
private irregularIndexes: number[] = [];
// 布局选项:动态更新的GridLayoutOptions
private layoutOptions: GridLayoutOptions = {
regularSize: [1, 1],
irregularIndexes: this.irregularIndexes
};
}
3.2 关键实现步骤
步骤1:数据结构设计
// Grid项数据类型定义
interface GridItemData {
id: string; // 唯一标识
type: 'primary' | 'secondary'; // 类型:主项或二级子项
parentId?: string; // 二级子项的父项ID
content: any; // 显示内容
isExpanded?: boolean; // 是否展开(仅主项)
}
// 示例数据结构
const sampleData: GridItemData[] = [
{ id: '1', type: 'primary', content: '分类A', isExpanded: false },
{ id: '2', type: 'primary', content: '分类B', isExpanded: false },
{ id: '3', type: 'primary', content: '分类C', isExpanded: false },
// 二级子项(初始不显示)
{ id: '1-1', type: 'secondary', parentId: '1', content: '子项A1' },
{ id: '1-2', type: 'secondary', parentId: '1', content: '子项A2' },
{ id: '2-1', type: 'secondary', parentId: '2', content: '子项B1' }
];
步骤2:展开收起逻辑
// 展开主项
expandPrimaryItem(itemId: string): void {
// 1. 更新主项展开状态
const primaryIndex = this.dataSource.findIndex(item =>
item.id === itemId && item.type === 'primary'
);
if (primaryIndex !== -1) {
this.dataSource[primaryIndex].isExpanded = true;
// 2. 查找并插入二级子项
const secondaryItems = this.getAllSecondaryItems().filter(
item => item.parentId === itemId
);
// 3. 在主项后插入二级子项
const insertIndex = primaryIndex + 1;
this.dataSource.splice(insertIndex, 0, ...secondaryItems);
// 4. 更新irregularIndexes
this.updateIrregularIndexes();
}
}
// 收起主项
collapsePrimaryItem(itemId: string): void {
// 1. 更新主项展开状态
const primaryIndex = this.dataSource.findIndex(item =>
item.id === itemId && item.type === 'primary'
);
if (primaryIndex !== -1) {
this.dataSource[primaryIndex].isExpanded = false;
// 2. 移除二级子项
let itemsToRemove = 0;
for (let i = primaryIndex + 1; i < this.dataSource.length; i++) {
if (this.dataSource[i].type === 'secondary' &&
this.dataSource[i].parentId === itemId) {
itemsToRemove++;
} else {
break;
}
}
this.dataSource.splice(primaryIndex + 1, itemsToRemove);
// 3. 更新irregularIndexes
this.updateIrregularIndexes();
}
}
步骤3:不规则索引计算
// 更新不规则索引
updateIrregularIndexes(): void {
this.irregularIndexes = [];
// 遍历数据源,找出需要特殊处理的项
this.dataSource.forEach((item, index) => {
if (item.type === 'primary' && item.isExpanded) {
// 展开的主项需要占用多行
this.irregularIndexes.push(index);
}
});
// 更新布局选项
this.layoutOptions = {
...this.layoutOptions,
irregularIndexes: this.irregularIndexes
};
}
四、完整代码实现
4.1 CollapseMenu.ets - 可折叠菜单组件
// CollapseMenu.ets
@Component
export struct CollapseMenu {
// 数据源
@State private menuData: GridItemData[] = [];
// 布局选项
@State private layoutOptions: GridLayoutOptions = {
regularSize: [1, 1],
irregularIndexes: []
};
// 列数配置
@Prop columnsTemplate: string = '1fr 1fr 1fr';
// 初始化数据
aboutToAppear(): void {
this.initializeMenuData();
}
build() {
Grid() {
// 动态生成GridItem
ForEach(this.menuData, (item: GridItemData, index: number) => {
GridItem() {
this.buildGridItemContent(item, index);
}
// 应用布局选项
.layoutOptions(this.getLayoutOptionsForIndex(index))
})
}
.columnsTemplate(this.columnsTemplate)
.rowsTemplate('1fr')
.columnsGap(12)
.rowsGap(12)
.layoutOptions(this.layoutOptions)
.onClickIndex((index: number) => {
this.handleItemClick(index);
})
}
// 构建GridItem内容
@Builder
buildGridItemContent(item: GridItemData, index: number) {
Column({ space: 8 }) {
// 主项内容
if (item.type === 'primary') {
this.buildPrimaryItem(item, index);
}
// 二级子项内容
else if (item.type === 'secondary') {
this.buildSecondaryItem(item);
}
}
.width('100%')
.height('100%')
.padding(16)
.backgroundColor(this.getItemBackgroundColor(item.type))
.borderRadius(8)
.shadow({ radius: 4, color: Color.Black, offsetX: 0, offsetY: 2 })
}
// 构建主项
@Builder
buildPrimaryItem(item: GridItemData, index: number) {
Row({ space: 8 }) {
// 图标
Image($r('app.media.category_icon'))
.width(24)
.height(24)
// 标题
Text(item.content)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#1890FF')
.layoutWeight(1)
// 展开/收起指示器
Image($r(item.isExpanded ?
'app.media.arrow_up' : 'app.media.arrow_down'))
.width(16)
.height(16)
}
.width('100%')
}
// 构建二级子项
@Builder
buildSecondaryItem(item: GridItemData) {
Row({ space: 8 }) {
// 缩进标识
Blank()
.width(24)
.height(24)
// 子项内容
Text(item.content)
.fontSize(14)
.fontColor('#666666')
.layoutWeight(1)
}
.width('100%')
}
// 获取项背景色
private getItemBackgroundColor(type: string): ResourceColor {
switch (type) {
case 'primary': return '#FFFFFF';
case 'secondary': return '#F5F5F5';
default: return '#FFFFFF';
}
}
// 获取特定索引的布局选项
private getLayoutOptionsForIndex(index: number): GridLayoutOptions {
const item = this.menuData[index];
if (item.type === 'primary' && item.isExpanded) {
// 展开的主项:根据子项数量计算需要占用的行数
const childCount = this.getChildCount(item.id);
const rowsNeeded = Math.ceil((childCount + 1) / 3); // +1 包含主项自身
return {
regularSize: [1, 1],
irregularIndexes: [index],
onGetIrregularSizeByIndex: (idx: number) => {
return idx === index ? [rowsNeeded, 1] : [1, 1];
}
};
}
// 默认布局
return { regularSize: [1, 1] };
}
// 处理项点击
private handleItemClick(index: number): void {
const item = this.menuData[index];
if (item.type === 'primary') {
if (item.isExpanded) {
this.collapseItem(item.id);
} else {
this.expandItem(item.id);
}
}
}
// 展开项
private expandItem(itemId: string): void {
// 查找主项索引
const primaryIndex = this.menuData.findIndex(
item => item.id === itemId && item.type === 'primary'
);
if (primaryIndex === -1) return;
// 更新展开状态
this.menuData[primaryIndex].isExpanded = true;
// 获取二级子项
const secondaryItems = this.getSecondaryItemsForParent(itemId);
// 插入二级子项
const insertIndex = primaryIndex + 1;
const newItems = [...this.menuData];
newItems.splice(insertIndex, 0, ...secondaryItems);
// 更新数据源
this.menuData = newItems;
// 更新布局选项
this.updateLayoutOptions();
}
// 收起项
private collapseItem(itemId: string): void {
// 查找主项索引
const primaryIndex = this.menuData.findIndex(
item => item.id === itemId && item.type === 'primary'
);
if (primaryIndex === -1) return;
// 更新展开状态
this.menuData[primaryIndex].isExpanded = false;
// 移除二级子项
const newItems = [...this.menuData];
let itemsToRemove = 0;
for (let i = primaryIndex + 1; i < newItems.length; i++) {
if (newItems[i].type === 'secondary' &&
newItems[i].parentId === itemId) {
itemsToRemove++;
} else {
break;
}
}
newItems.splice(primaryIndex + 1, itemsToRemove);
// 更新数据源
this.menuData = newItems;
// 更新布局选项
this.updateLayoutOptions();
}
// 获取父项的子项数量
private getChildCount(parentId: string): number {
return this.menuData.filter(
item => item.type === 'secondary' && item.parentId === parentId
).length;
}
// 获取父项的二级子项
private getSecondaryItemsForParent(parentId: string): GridItemData[] {
// 这里应该从数据源或API获取
// 示例数据
const sampleSecondaryItems: Record<string, GridItemData[]> = {
'1': [
{ id: '1-1', type: 'secondary', parentId: '1', content: '子分类A1' },
{ id: '1-2', type: 'secondary', parentId: '1', content: '子分类A2' },
{ id: '1-3', type: 'secondary', parentId: '1', content: '子分类A3' }
],
'2': [
{ id: '2-1', type: 'secondary', parentId: '2', content: '子分类B1' },
{ id: '2-2', type: 'secondary', parentId: '2', content: '子分类B2' }
]
};
return sampleSecondaryItems[parentId] || [];
}
// 更新布局选项
private updateLayoutOptions(): void {
// 收集所有展开的主项索引
const expandedIndexes: number[] = [];
this.menuData.forEach((item, index) => {
if (item.type === 'primary' && item.isExpanded) {
expandedIndexes.push(index);
}
});
// 更新布局选项
this.layoutOptions = {
regularSize: [1, 1],
irregularIndexes: expandedIndexes,
onGetIrregularSizeByIndex: (index: number) => {
if (expandedIndexes.includes(index)) {
const item = this.menuData[index];
const childCount = this.getChildCount(item.id);
// 计算需要的行数(考虑列数)
const rowsNeeded = Math.ceil((childCount + 1) / 3);
return [rowsNeeded, 1];
}
return [1, 1];
}
};
}
// 初始化菜单数据
private initializeMenuData(): void {
this.menuData = [
{ id: '1', type: 'primary', content: '电子产品', isExpanded: false },
{ id: '2', type: 'primary', content: '家居用品', isExpanded: false },
{ id: '3', type: 'primary', content: '服装服饰', isExpanded: false },
{ id: '4', type: 'primary', content: '食品饮料', isExpanded: false },
{ id: '5', type: 'primary', content: '图书音像', isExpanded: false },
{ id: '6', type: 'primary', content: '运动户外', isExpanded: false }
];
}
}
4.2 Index.ets - 主页面集成
// Index.ets
@Entry
@Component
struct Index {
@State currentTab: string = 'categories';
build() {
Column({ space: 0 }) {
// 顶部导航栏
this.buildTopNavigation()
// 内容区域
Column({ space: 0 }) {
// 标题
Text('商品分类')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ top: 20, bottom: 20 })
.width('100%')
.textAlign(TextAlign.Center)
// 可折叠菜单
CollapseMenu({
columnsTemplate: '1fr 1fr 1fr'
})
.height('80%')
.margin({ left: 16, right: 16 })
}
.width('100%')
.height('100%')
.backgroundColor('#F0F2F5')
}
.width('100%')
.height('100%')
}
@Builder
buildTopNavigation() {
Row({ space: 0 }) {
// 返回按钮
Image($r('app.media.back_icon'))
.width(24)
.height(24)
.margin({ left: 16 })
.onClick(() => {
// 返回逻辑
})
// 标题
Text('网格布局演示')
.fontSize(18)
.fontWeight(FontWeight.Medium)
.layoutWeight(1)
.textAlign(TextAlign.Center)
// 设置按钮
Image($r('app.media.settings_icon'))
.width(24)
.height(24)
.margin({ right: 16 })
.onClick(() => {
// 设置逻辑
})
}
.width('100%')
.height(56)
.backgroundColor('#FFFFFF')
.shadow({ radius: 2, color: Color.Black, offsetX: 0, offsetY: 1 })
}
}
五、高级优化技巧
5.1 动画效果增强
// 添加展开收起动画
private animateExpansion(itemId: string, expand: boolean): void {
// 使用animateTo实现平滑过渡
animateTo({
duration: 300,
curve: expand ? Curve.EaseOut : Curve.EaseIn
}, () => {
if (expand) {
this.expandItem(itemId);
} else {
this.collapseItem(itemId);
}
});
}
5.2 性能优化策略
-
虚拟滚动支持:对于大量数据,实现虚拟滚动避免渲染所有GridItem
-
缓存机制:缓存已加载的二级数据,避免重复请求
-
懒加载:二级内容在需要时再加载,减少初始渲染压力
5.3 响应式布局适配
// 根据屏幕宽度动态调整列数
@Watch('windowSize')
onWindowSizeChange() {
const screenWidth = window.getWindowWidth();
if (screenWidth >= 1200) {
this.columnsTemplate = '1fr 1fr 1fr 1fr';
} else if (screenWidth >= 768) {
this.columnsTemplate = '1fr 1fr 1fr';
} else {
this.columnsTemplate = '1fr 1fr';
}
}
六、常见问题与解决方案
6.1 布局错乱问题
问题现象:展开二级内容后,其他GridItem位置错乱
解决方案:
-
确保
irregularIndexes只包含展开的主项索引 -
在
onGetIrregularSizeByIndex中准确计算所需行数 -
使用
GridLayoutOptions的regularSize保持基础对齐
6.2 动画卡顿问题
问题现象:展开收起过程中动画不流畅
解决方案:
-
减少同时展开的项数量
-
使用
animateTo替代直接状态更新 -
优化二级内容的渲染复杂度
6.3 数据同步问题
问题现象:展开状态与数据源不同步
解决方案:
-
使用单一数据源管理所有状态
-
实现状态回滚机制
-
添加数据验证和错误处理
七、总结与最佳实践
通过本文的深入解析,我们掌握了利用GridLayoutOptions实现Grid容器二级子项展开收起功能的核心技术。关键要点总结如下:
-
理解参数机制:
regularSize定义基础布局,irregularIndexes控制特殊项 -
动态数据管理:通过数据源的动态增删实现展开收起效果
-
精确布局计算:根据子项数量准确计算所需网格空间
-
用户体验优化:添加平滑动画和响应式布局支持
在实际开发中,建议遵循以下最佳实践:
-
渐进式展开:避免同时展开过多项,影响性能
-
状态持久化:保存用户的展开状态,提升体验
-
边界情况处理:考虑空状态、网络错误等场景
-
可访问性支持:确保键盘导航和屏幕阅读器兼容
随着HarmonyOS技术的不断发展,Grid组件的布局能力将持续增强。掌握本文介绍的技术方案,开发者可以灵活应对各种复杂的网格布局需求,打造出既美观又实用的HarmonyOS应用界面。
更多推荐




所有评论(0)