# 45号应用:菜单演示 — 可展开手风琴菜单的设计与实现
一、应用概述
菜单演示(Accordion Menu Demo)是一个可展开/收起的手风琴菜单应用。四个主菜单(美食、娱乐、运动、学习)各自包含 4 个子项,点击主菜单展开子项列表,再次点击收起。菜单项带有展开指示箭头(▶/▼)和投影阴影,子项以圆角列表形式展示。
手风琴菜单(Accordion)是移动应用中最常用的导航模式之一,广泛用于设置页面、分类浏览、FAQ 等场景。在 HarmonyOS 中,这个应用展示了List 组件的嵌套渲染、条件展开动画和阴影样式的典型用法。
二、技术架构
2.1 应用结构
┌─────────────────────────────────────┐ │ ‹ 返回 📋 菜单演示 │ ├─────────────────────────────────────┤ │ ┌─ 🍕 美食 ▶ ─┐│ ← 主菜单 │ └────────────────────────────────┘│ │ ┌─ 🎮 娱乐 ▼ ─┐│ ← 展开状态 │ │ • 电影 ││ │ │ • 音乐 ││ ← 子菜单 │ │ • 游戏 ││ │ │ • 阅读 ││ │ └────────────────────────────────┘│ │ ┌─ 🏃 运动 ▶ ─┐│ │ ├─ 📚 学习 ▶ ─┤│ ├─────────────────────────────────────┤ │ 点击主菜单展开/收起子项 │ └─────────────────────────────────────┘2.2 数据模型
interface MenuItem { id: number; name: string; // 主菜单名称(含 emoji) sub: string[]; // 子项名称数组 expanded: boolean; // 是否展开 }2.3 状态变量
| 变量 | 类型 | 用途 |
|---|---|---|
menus | MenuItem[] | 4 个菜单项目,驱动列表渲染 |
三、核心代码深度解析
3.1 手风琴展开/收起逻辑
toggleMenu(id: number): void { const newMenus: MenuItem[] = this.menus.concat(); for (let i = 0; i < newMenus.length; i++) { if (newMenus[i].id === id) { newMenus[i] = { id: newMenus[i].id, name: newMenus[i].name, sub: newMenus[i].sub, expanded: !newMenus[i].expanded }; } } this.menus = newMenus; }手风琴 vs 普通展开:
当前实现是独立展开模式——每个菜单项独立切换,互不影响。如果要实现经典手风琴模式(展开一个时自动收起其他),只需稍作修改:
toggleMenuAccordion(id: number): void { this.menus = this.menus.map(menu => ({ ...menu, expanded: menu.id === id ? !menu.expanded : false })); }经典手风琴模式使用map将所有非目标菜单的expanded设为false,实现「最多一个展开」的效果。
3.2 List + ForEach 嵌套渲染
菜单的整体结构是 List 中嵌套多层 Column:
List() { ForEach(this.menus, (menu: MenuItem) => { ListItem() { Column() { // 主菜单行 Row() { Text(menu.name).fontSize(18).fontWeight(FontWeight.Medium).layoutWeight(1) Text(menu.expanded ? '▼' : '▶').fontSize(16).fontColor('#aaa') } .padding(16).backgroundColor('#fff').borderRadius(12) .shadow({ radius: 2, color: '#20000000', offsetY: 1 }) .onClick(() => this.toggleMenu(menu.id)) // 子菜单区域(条件渲染) if (menu.expanded) { Column() { ForEach(menu.sub, (s: string) => { Row() { Text('•').fontSize(16).fontColor('#0077FF').margin({ left: 20, right: 8 }) Text(s).fontSize(15).fontColor('#555') } .padding({ top: 10, bottom: 10, left: 16 }).width('90%') .backgroundColor('#f5f5f5').borderRadius(8).margin({ bottom: 4 }) .alignSelf(ItemAlign.Center) }) } .margin({ top: 4 }).padding(4).backgroundColor('#fafafa').borderRadius(8) } } } }, (menu: MenuItem) => menu.id.toString()) } .layoutWeight(1).width('92%').padding({ top: 8 })嵌套渲染的关键结构:
List └── ForEach (menus) └── ListItem └── Column ├── Row (主菜单: emoji + 名称 + 箭头) └── if (expanded) └── Column └── ForEach (sub) └── Row (子项: • + 名称)为什么用 Column 而非直接展开:
主菜单使用 Row,子菜单使用 Column 嵌套,这是因为:
- Row 让 emoji、名称、箭头在同一行水平排列
- Column 让子项垂直排列在主菜单下方
- Column 将主菜单和子菜单组合成一个逻辑单元,便于 ListItem 管理
3.3 展开指示箭头
Text(menu.expanded ? '▼' : '▶').fontSize(16).fontColor('#aaa')这是最直观的展开指示器设计:
- ▶(右三角):表示「可展开,点击展开」
- ▼(下三角):表示「已展开,点击收起」
用户可以通过箭头的方向直观判断当前菜单的状态,无需阅读文字说明。
3.4 阴影与样式
.shadow({ radius: 2, color: '#20000000', offsetY: 1 })shadow 属性详解:
| 参数 | 值 | 说明 |
|---|---|---|
radius | 2 | 阴影模糊半径,越大阴影越柔和扩散 |
color | '#20000000' | 阴影颜色,前两位 20 是透明度(约 12%),后六位 000000 是黑色 |
offsetX | 未设置(默认 0) | 水平偏移量 |
offsetY | 1 | 垂直向下偏移 1vp,产生微微下沉的效果 |
这种阴影效果模拟了 Material Design 的 elevation(海拔高度)理念——卡片海拔越高,阴影越明显。这里的radius: 2, offsetY: 1营造了轻微的「浮起」效果,让主菜单看起来是可以点击的交互元素。
四、HarmonyOS 特色功能深度解析
4.1 条件渲染与展开动画
if (menu.expanded) { Column() { ForEach(menu.sub, (s: string) => { // 子项内容 }) } .margin({ top: 4 }).padding(4).backgroundColor('#fafafa').borderRadius(8) }ArkUI 的if条件渲染是声明式的——当menu.expanded变化时,框架自动处理子组件的创建和销毁:
expanded | 行为 |
|---|---|
false→true | 创建 Column 及其子组件,插入到 UI 树 |
true→false | 销毁 Column 及其子组件,从 UI 树移除 |
当前局限:这个实现是「显示/隐藏」而非「动画展开/收起」。子菜单是突然出现的,没有过渡动画。
添加展开动画的改进方案:
// 方法一:使用 animation 属性 Column() .height(menu.expanded ? subHeight : 0) .opacity(menu.expanded ? 1 : 0) .animation({ duration: 300, curve: Curve.FastOutSlowIn }) // 方法二:使用 animateTo 显式动画 animateTo({ duration: 300 }, () => { this.menus[idx].expanded = !this.menus[idx].expanded; })4.2 List 组件与滚动
List是 ArkUI 中用于垂直滚动的容器组件:
List() .layoutWeight(1) // 充满父容器剩余空间 .width('92%') .padding({ top: 8 })layoutWeight(1)的作用:
- 让 List 占据 Column 中除导航栏和底部提示文字外的所有垂直空间
- 当菜单展开超过一屏时,List 自动变为可滚动
- 无需手动设置
scrollable属性,List 默认支持滚动
4.3 alignSelf 与子项居中
.alignSelf(ItemAlign.Center)这个属性让子项在交叉轴上居中对齐。在Column容器中,alignSelf覆盖了父容器的alignItems设置。这里子项宽度设为'90%',通过alignSelf(ItemAlign.Center)在父容器中水平居中。
五、UI/UX 设计分析
5.1 视觉层级
第一层:主菜单(白色背景 + 阴影 + 18pt 字体) 第二层:子菜单容器(浅灰背景 #fafafa + 圆角) 第三层:子项(更浅的背景 #f5f5f5 + 8px 圆角)三层背景色递减:
- 主菜单:
#fff(纯白) - 子菜单容器:
#fafafa(最浅灰) - 子项:
#f5f5f5(浅灰)
这种嵌套灰度的视觉语言帮助用户理解层级关系。
5.2 底部提示
Text('点击主菜单展开/收起子项').fontSize(13).fontColor('#aaa').margin({ bottom: 12 })底部的提示文字是 UX 中的引导性文案,帮助用户理解交互方式。对于菜单这种直觉性较强的交互模式,提示文字可以较小(13pt)且颜色较淡(#aaa),不占据视觉焦点但在需要时提供指引。
5.3 emoji 图标系统
四个主菜单均使用 emoji 作为图标:
- 🍕 美食 — 食物 emoji,直觉关联
- 🎮 娱乐 — 游戏手柄,代表娱乐
- 🏃 运动 — 跑步的人,代表运动
- 📚 学习 — 书本,代表学习
Emoji 作为图标的好处是:无需导入图标库、国际化友好、跨平台兼容。HarmonyOS 对 emoji 的渲染支持良好,显示效果一致。
六、最佳实践与优化建议
6.1 当前代码亮点
- 简洁的数据模型:
MenuItem接口清晰,子项使用字符串数组而非对象数组,简化数据结构 - 不可变更新模式:
toggleMenu创建新对象新数组,确保 @State 响应 - 视觉层级分明:三层背景色递进,层次感强
- 箭头方向反馈:▶/▼ 直观指示展开状态
6.2 可优化方向
// 优化建议 1:添加展开/收起动画 // 使用 animateTo 实现平滑展开 toggleMenuWithAnim(id: number): void { animateTo({ duration: 250, curve: Curve.FastOutSlowIn }, () => { const newMenus = this.menus.map(m => m.id === id ? { ...m, expanded: !m.expanded } : m ); this.menus = newMenus; }); } // 优化建议 2:经典手风琴模式(只展开一个) toggleMenuAccordion(id: number): void { this.menus = this.menus.map(menu => ({ ...menu, expanded: menu.id === id ? !menu.expanded : false })); } // 优化建议 3:添加图标/角标 // 子项右侧显示数字角标 Row() { Text(s).fontSize(15).fontColor('#555').layoutWeight(1) Text('3').fontSize(12).fontColor('#fff').padding(4) .backgroundColor('#F44336').borderRadius(10) } // 优化建议 4:支持多级菜单 interface MultiLevelMenu { id: number; name: string; children?: MultiLevelMenu[]; // 递归定义 expanded: boolean; } // 优化建议 5:自定义展开图标 // 使用旋转动画的箭头 Image(this.menu.expanded ? $r('app.media.arrow_down') : $r('app.media.arrow_right')) .rotate({ angle: this.menu.expanded ? 180 : 0 }) .animation({ duration: 200 })6.3 性能优化
- List 虚拟化:当前 4 个菜单项远未达到性能瓶颈。如果扩展到 100+ 菜单,List 的虚拟化特性会自动优化渲染性能
- 避免深层嵌套:目前主菜单→子菜单只有两层,渲染开销极低。如果实现多级菜单(3+ 层),应考虑使用递归组件或树形控件
- 子项复用:同一级别的子项结构相同,HarmonyOS 的组件复用机制会自动优化
七、总结与扩展
菜单演示应用展示了 ArkUI 中实现手风琴菜单的完整模式:
| 知识点 | 应用位置 | 重要程度 |
|---|---|---|
| List + ForEach 列表渲染 | 主菜单列表 | ★★★★★ |
| 条件渲染 if/else | 子菜单展开/收起 | ★★★★★ |
| 不可变对象更新 | toggleMenu | ★★★★☆ |
| shadow 阴影样式 | 主菜单卡片 | ★★★☆☆ |
| alignSelf 对齐 | 子项居中 | ★★★☆☆ |
| 展开箭头指示 | 交互反馈 | ★★★☆☆ |
扩展思路:
- 嵌套多级菜单:支持三级及以上菜单层级,用于复杂的设置页面
- 图标系统:为每个菜单项添加 Material Design 图标(使用 HarmonyOS 的图标资源)
- 展开动画:使用
animateTo或transition属性添加平滑展开/收起动画 - 搜索过滤:添加搜索框,实时过滤匹配的菜单项
- 状态保持:使用
@StorageProp或AppStorage在页面切换时保持展开状态 - 自定义主题:支持主菜单颜色、字体、圆角等样式自定义
手风琴菜单是移动应用中最经典的导航模式之一。通过这个案例,你可以掌握 List 嵌套渲染、条件展开和交互状态管理的基本模式,这些模式可以复用到任何需要「分类展开」的场景中。