当前位置: 首页 > news >正文

# 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 状态变量

变量类型用途
menusMenuItem[]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 属性详解

参数说明
radius2阴影模糊半径,越大阴影越柔和扩散
color'#20000000'阴影颜色,前两位 20 是透明度(约 12%),后六位 000000 是黑色
offsetX未设置(默认 0)水平偏移量
offsetY1垂直向下偏移 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行为
falsetrue创建 Column 及其子组件,插入到 UI 树
truefalse销毁 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 当前代码亮点

  1. 简洁的数据模型MenuItem接口清晰,子项使用字符串数组而非对象数组,简化数据结构
  2. 不可变更新模式toggleMenu创建新对象新数组,确保 @State 响应
  3. 视觉层级分明:三层背景色递进,层次感强
  4. 箭头方向反馈:▶/▼ 直观指示展开状态

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 对齐子项居中★★★☆☆
展开箭头指示交互反馈★★★☆☆

扩展思路

  1. 嵌套多级菜单:支持三级及以上菜单层级,用于复杂的设置页面
  2. 图标系统:为每个菜单项添加 Material Design 图标(使用 HarmonyOS 的图标资源)
  3. 展开动画:使用animateTotransition属性添加平滑展开/收起动画
  4. 搜索过滤:添加搜索框,实时过滤匹配的菜单项
  5. 状态保持:使用@StoragePropAppStorage在页面切换时保持展开状态
  6. 自定义主题:支持主菜单颜色、字体、圆角等样式自定义

手风琴菜单是移动应用中最经典的导航模式之一。通过这个案例,你可以掌握 List 嵌套渲染、条件展开和交互状态管理的基本模式,这些模式可以复用到任何需要「分类展开」的场景中。

http://www.jsqmd.com/news/1297529/

相关文章:

  • 机器学习入门实战:从环境配置到鸢尾花分类完整流程
  • OAM光束仿真:Matlab实现与湍流影响分析
  • 蚌埠母婴除甲醛公司测甲醛中心怎么选:金耀母婴除甲醛标准、流程、避坑指南 - 信誉隆金银铂奢回收
  • 大白话简单聊一聊:什么是 Graph-Engineering ?
  • 华为HCIP网络工程师认证—VLAN间路由
  • Unity许可证验证失败:Sentinel key not found (h0007)错误全面解析与解决方案
  • 洛阳出发西藏,如何找到靠谱西藏旅行社?15年纯玩地接社避坑攻略| 附:旅行社电话 - 西藏康泰旅行社
  • 大模型应用落地,权限与日志才是你的新护城河?
  • 2026精选:大连别墅地下室防潮的专业除湿与防水系统深度解析 - 优企名品
  • 足迹交易原则:从订单流分析到市场微观结构实战指南
  • 华为OD机试新系统真题【基于上报数据的实时最大值】
  • Windows平台Python 3.10.12源码编译实战:从环境配置到深度定制
  • 如何在3分钟内掌握网页视频下载:Simple Video Download Helper终极指南
  • 廊坊母婴除甲醛公司测甲醛中心怎么选:金耀母婴除甲醛标准、流程、避坑指南 - 信誉隆金银铂奢回收
  • 包头母婴除甲醛公司测甲醛中心怎么选:金耀母婴除甲醛标准、流程、避坑指南 - 信誉隆金银铂奢回收
  • 2026年,这些解码矩阵企业为何声名鹊起?秘密全在这里
  • 前端开发转做渗透测试,利用现有技能挖漏洞真的更简单吗
  • AI量化研究员年薪百万的秘密:掌握这8个PyTorch+TA-Lib深度特征构造模板,效率提升400%
  • 新手做抖店卖什么更容易出单?选品方向与靠谱货源筛选指南 - 抖大侠
  • FNF模组运行指南:环境配置、性能优化与常见问题排查
  • 沈阳母婴除甲醛公司测甲醛中心怎么选:金耀母婴除甲醛标准、流程、避坑指南 - 信誉隆金银铂奢回收
  • PTA编程题解析:L1-019谁先倒的算法实现
  • 为什么让AI同时学多门课,反而会让它“偏科“越来越严重?
  • 绝杀99%格式异常!Flink CDC同步PostgreSQL疑难问题终极解决方案
  • 口语低谷期不敢跟老外实时对话?用异步 Video Interview 降维通关「蒸汽求职分享」
  • 南京大学领衔研发:AI如何真正“看懂“视频里的每一个关键瞬间?
  • 5分钟上手Codex:智能编程助手安装与实战应用指南
  • Grok Build模式:自然语言生成完整应用的AI开发实践
  • 2026年佛山沙子供应商哪家能配送到家? - 品牌排行榜
  • 抖店一件代发新手怎么做?从货源筛选到发货售后完整步骤 - 抖大侠