在这里插入图片描述

引言

在移动应用的导航体系中,抽屉式菜单(Drawer Navigation)是最经典的导航模式之一。从最早的 Android 原生 Navigation Drawer,到 Material Design 中规范的 Navigation Rail,再到各平台对侧滑手势的原生支持,抽屉导航以其「隐藏式面板 + 内容覆盖」的独特交互,成为了承载多级菜单、用户中心、功能入口等场景的不二之选。HarmonyOS NEXT 提供了 SideBarContainer 组件,以声明式的方式实现了抽屉导航,支持 Overlay(覆盖式)和 Inline(内联式)两种展示模式。

示例 91 以「我的菜单」为主题,实现了一个左侧抽屉导航页面。用户点击「打开菜单」按钮,左侧菜单栏会从左侧滑出,覆盖在主内容区之上;菜单项以列表形式排列,支持选中高亮;点击菜单项后,菜单自动收起,主内容区显示选中状态,并通过 promptAction.showToast 弹出操作反馈。整个流程涉及 SideBarContainer 的状态绑定、菜单项的 ForEach 渲染、选中态的视觉反馈、以及 onChange 事件的双向同步,几乎涵盖了抽屉导航实现的所有核心要点。

这篇文章会严格按源码顺序,先介绍应用的整体功能与布局结构,再拆解 SideBarContainer 的核心属性与事件,接着逐段解读 .ets 源码中的菜单渲染、选中逻辑、交互反馈,然后分析 Overlay 模式与 Inline 模式的区别、showSideBar 绑定机制、菜单 UI 的样式设计思路,最后给出运行操作指南、可扩展方向与常见问题调试技巧。读完后,你不仅能看懂这一个侧滑菜单页面,还能举一反三,把它应用到用户中心、功能导航、分类浏览等任何需要抽屉导航的场景。

1. 应用概述与功能

「侧滑菜单」是一个面向导航场景的工具型页面,交互路径清晰:点击打开菜单 → 查看菜单项 → 点击选中 → 自动收起

页面自上而下分为三块区域:顶部是返回栏,左侧「返回」按钮调用 router.back() 返回上一页,中间是标题「侧滑菜单」;中部是 SideBarContainer 容器,左侧菜单栏占 40% 宽度,深色背景(#1f2733),右侧主内容区占满剩余空间,浅灰背景(#f2f3f5);底部功能说明卡片列出三条使用提示。

1.1 核心功能清单

  • 抽屉式菜单展示:使用 SideBarContainer 组件实现左侧抽屉菜单,支持 Overlay 覆盖模式。
  • 菜单项高亮选中:点击菜单项时,选中项文字变蓝色(#1a6cff)、加粗、带半透明蓝色背景,未选中项为灰色。
  • 自动收起:点击菜单项后,侧边栏自动收起,给用户完整的主内容区操作空间。
  • 双向状态同步:通过 showSideBar 属性和 onChange 回调实现菜单展开状态的双向绑定。
  • 操作反馈:点击菜单项后通过 promptAction.showToast 弹出提示,如「点击了首页」。
  • 功能说明卡片:主内容区底部展示白色圆角卡片,列出三条使用提示引导用户操作。

1.2 技术要点一览

整个示例用到的关键技术对「抽屉导航」类页面很有代表性:SideBarContainer 组件的 Overlay 模式、showSideBar 状态绑定与 onChange 事件回调、菜单项列表的 ForEach 渲染与选中态判断、Text 组件的动态样式绑定(颜色、字重、背景色)、以及 Divider 分隔线的使用。把这些要点串起来,就构成了一条完整的「状态驱动 → UI 渲染 → 交互反馈 → 状态更新」的交互闭环。

2. 核心知识点

在逐段读代码之前,先把侧滑菜单页面承载的 ArkTS 核心知识讲清楚。

2.1 SideBarContainer 组件与 Overlay 模式

SideBarContainer 是 ArkUI 提供的抽屉容器组件,用于实现「侧边栏 + 主内容区」的布局结构。它支持两种展示模式:

  • SideBarContainerType.Overlay:覆盖模式,侧边栏滑出时覆盖在主内容区之上,主内容区不移动。这是最常见的抽屉效果,适合移动端。
  • SideBarContainerType.Inline:内联模式,侧边栏展开时主内容区被挤压,侧边栏与主内容区并排显示,适合平板或横屏场景。

示例使用的是 Overlay 模式:

SideBarContainer(SideBarContainerType.Overlay) {
  // 左侧菜单栏(第一个子组件)
  Column() { ... }
  // 右侧主内容区(第二个子组件)
  Column() { ... }
}

SideBarContainer 接受一个类型参数和两个子组件:第一个子组件是侧边栏内容,第二个子组件是主内容区。组件内部会自动处理滑动手势和动画过渡。

2.2 showSideBar 状态绑定与 onChange 回调

SideBarContainer 的展开/收起通过 showSideBar 属性控制,而 onChange 回调则在侧边栏状态变化时触发:

SideBarContainer(SideBarContainerType.Overlay) {
  // 子组件...
}
.showSideBar(this.show)
.onChange((v: boolean) => {
  this.show = v;
})

这里涉及一个双向绑定的模式:

  • 正向控制:当 this.showtrue 时,侧边栏展开;为 false 时收起。
  • 反向同步:当用户通过手势滑动或点击外部区域使侧边栏收起时,onChange 回调被触发,参数 v 会更新 this.show 的值。

这个双向绑定确保了无论通过哪种方式改变侧边栏状态,this.show 都能保持同步。如果只使用 showSideBar 而不处理 onChange,那么用户手动收起菜单后,this.show 仍然是 true,下次点击按钮时就会出现状态不一致的问题。

2.3 菜单项的动态样式绑定

菜单项的选中态通过动态样式绑定实现:

Text(item)
  .fontColor(this.selIdx === idx ? '#1a6cff' : '#cccccc')
  .fontWeight(this.selIdx === idx ? FontWeight.Bold : FontWeight.Normal)
  .backgroundColor(this.selIdx === idx ? 'rgba(26,108,255,0.18)' : 'rgba(0,0,0,0)')

这是 ArkUI 中条件样式绑定的标准写法:通过三元运算符根据 this.selIdx 与当前 idx 的比较结果,动态决定 fontColorfontWeightbackgroundColor 三个属性的值。选中项使用主题蓝色 #1a6cff、加粗字重、半透明蓝色背景,未选中项使用浅灰色 #cccccc、正常字重、透明背景。这种方式比为选中项单独创建一个组件更简洁,性能也更好。

2.4 ForEach 渲染菜单列表

菜单项通过 ForEach 组件从数组渲染:

ForEach(this.menus, (item: string, idx: number) => {
  Text(item)
    .width('86%')
    .height(52)
    // ... 其他属性
    .onClick(() => {
      this.chooseMenu(idx);
    })
}, (item: string, idx: number) => idx.toString())

ForEach 的三个参数分别是:

  1. 数据源this.menus 数组,包含四个菜单项名称。
  2. 渲染函数:接收每个元素和索引,返回对应的 UI 组件。这里返回的是一个带完整样式和点击事件的 Text 组件。
  3. 键值生成器:为每个元素生成唯一的 key,用于 ArkUI 的 diff 算法。这里使用 idx.toString() 作为 key,确保每个菜单项有稳定的标识。

2.5 promptAction 轻提示

promptAction 是 ArkUI 提供的轻量级提示 API,来自 @kit.ArkUI

import { promptAction } from '@kit.ArkUI';

promptAction.showToast({ message: '点击了' + this.menus[idx] });

showToast 会在屏幕底部弹出一个短消息,持续约 2 秒后自动消失。它适用于不需要用户交互的简单提示,比自定义的 AlertDialog 更轻量,也不需要额外的状态管理。在侧滑菜单这种交互场景中,promptAction 是最合适的反馈方式。

3. 源码逐段解析

现在开始按源码顺序逐段解读 index91.ets,从导入声明到 build 方法,完整展示侧滑菜单的实现细节。

3.1 导入声明与组件声明

import { router } from '@kit.ArkUI';
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct Index91 {

源码开头导入了两个 ArkUI 模块:router 用于页面导航(调用 router.back() 返回上一页),promptAction 用于轻提示反馈。@Entry 装饰器标记该组件为页面入口,@Component 装饰器标记该结构体为可复用的 UI 组件。

3.2 状态变量与菜单数据

@State show: boolean = false;
@State selIdx: number = 0;
private menus: string[] = ['首页', '消息', '设置', '关于'];

组件声明了两个 @State 状态变量和一个私有数组:

  • show:控制侧边栏的展开/收起,初始为 false(收起状态)。当用户点击「打开菜单」按钮或菜单项时,这个值会被修改。
  • selIdx:记录当前选中的菜单项索引,初始为 0(选中第一项「首页」)。这个值驱动菜单项的高亮样式和主内容区的选中状态文本。
  • menus:菜单项名称数组,包含四个导航入口。使用 private 修饰,因为它不需要从外部访问。

3.3 chooseMenu 选中处理方法

private chooseMenu(idx: number): void {
  this.selIdx = idx;
  this.show = false;
  promptAction.showToast({ message: '点击了' + this.menus[idx] });
}

chooseMenu 是菜单项点击的处理函数,接收一个参数 idx(选中的菜单项索引),执行三个操作:

  1. 更新 selIdx 为点击的索引,触发菜单项的高亮样式重新渲染。
  2. 设置 showfalse,自动收起侧边栏。这是侧滑菜单的标准交互——选中后自动关闭。
  3. 通过 promptAction.showToast 弹出提示,告知用户点击了哪个菜单项。

这三个操作的顺序很重要:先更新选中态,再收起菜单,最后弹出提示。如果先收起菜单再更新选中态,在菜单收起的动画过程中用户可能看到短暂的旧选中状态。

3.4 build 方法整体结构

build 方法构建了整个页面的 UI 结构,最外层是一个 Column,包含顶部返回栏和 SideBarContainer 两部分:

build() {
  Column() {
    // 顶部返回栏
    Row() { ... }
    // 侧滑菜单容器
    SideBarContainer(SideBarContainerType.Overlay) { ... }
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#f2f3f5')
}

最外层 Column 设置为全屏宽高(100%),背景色为浅灰色 #f2f3f5,给整个页面一个统一的底色。

3.5 顶部返回栏

Row() {
  Button('返回')
    .backgroundColor('#1a6cff')
    .fontColor(Color.White)
    .onClick(() => {
      router.back();
    })
  Text('侧滑菜单')
    .fontSize(18)
    .fontWeight(FontWeight.Bold)
  Blank()
}
.width('100%')
.padding({ left: 12, right: 12, top: 10, bottom: 10 })

顶部返回栏是一个 Row,宽度占满整屏,带内边距。从左到右依次是:蓝色「返回」按钮(点击调用 router.back())、居中的标题文字「侧滑菜单」、右侧的 Blank()(弹性空白,保证标题居中)。

Blank() 是 ArkUI 中一个特殊的弹性空白组件,它会占据 Row 中所有剩余空间。由于 Blank() 放在标题右侧,它会把标题推到中间位置,实现标题居中的效果。

3.6 SideBarContainer 左侧菜单栏

SideBarContainer 是整个页面的核心,它的第一个子组件是左侧菜单栏:

SideBarContainer(SideBarContainerType.Overlay) {
  // 左侧菜单栏
  Column() {
    Text('我的菜单')
      .fontSize(20)
      .fontColor(Color.White)
      .fontWeight(FontWeight.Bold)
      .margin({ top: 24, bottom: 20 })
    Divider()
      .color('#3a4657')
      .strokeWidth(1)
      .margin({ bottom: 10 })

    ForEach(this.menus, (item: string, idx: number) => {
      Text(item)
        .width('86%')
        .height(52)
        .fontSize(16)
        .fontColor(this.selIdx === idx ? '#1a6cff' : '#cccccc')
        .fontWeight(this.selIdx === idx ? FontWeight.Bold : FontWeight.Normal)
        .borderRadius(8)
        .textAlign(TextAlign.Center)
        .backgroundColor(this.selIdx === idx ? 'rgba(26,108,255,0.18)' : 'rgba(0,0,0,0)')
        .margin({ top: 6 })
        .onClick(() => {
          this.chooseMenu(idx);
        })
    }, (item: string, idx: number) => idx.toString())
  }
  .width('40%')
  .height('100%')
  .backgroundColor('#1f2733')
  .padding({ left: 12, right: 12, top: 8 })

左侧菜单栏的结构分为三部分:

  1. 标题区:白色大字「我的菜单」,上下带外边距,作为菜单的视觉起点。
  2. 分隔线Divider 组件在标题和菜单项之间画一条细线,颜色为深灰色 #3a4657,与深色背景形成层次。
  3. 菜单项列表:通过 ForEach 渲染四个 Text 菜单项,每项宽度 86%、高度 52vp、圆角 8、居中对齐。选中态用主题蓝 #1a6cff 文字 + 半透明蓝背景,未选中态用浅灰 #cccccc 文字 + 透明背景。

整个菜单栏宽度为屏幕的 40%,高度 100%,深色背景 #1f2733,带内边距。

3.7 SideBarContainer 右侧主内容区

SideBarContainer 的第二个子组件是右侧主内容区:

// 右侧主内容区
Column() {
  Text('主内容')
    .fontSize(24)
    .fontWeight(FontWeight.Bold)
    .fontColor('#333333')
    .margin({ top: 80 })
  Text('当前选中:' + this.menus[this.selIdx])
    .fontSize(14)
    .fontColor('#888888')
    .margin({ top: 12 })
  Text('点击下方按钮打开侧滑菜单')
    .fontSize(13)
    .fontColor('#aaaaaa')
    .margin({ top: 8 })
  Button('打开菜单')
    .width(180)
    .height(46)
    .backgroundColor('#1a6cff')
    .fontColor(Color.White)
    .margin({ top: 40 })
    .onClick(() => {
      this.show = true;
    })

  Column() {
    Text('功能说明')
      .fontSize(16)
      .fontWeight(FontWeight.Bold)
      .fontColor('#333333')
    Divider()
      .color('#eeeeee')
      .margin({ top: 10, bottom: 10 })
    Text('1. 点击"打开菜单"展开左侧菜单')
      .fontSize(13)
      .fontColor('#666666')
      .margin({ bottom: 6 })
    Text('2. 点击菜单项可选中并自动收起')
      .fontSize(13)
      .fontColor('#666666')
      .margin({ bottom: 6 })
    Text('3. 点击空白区域或返回箭头也可收起')
      .fontSize(13)
      .fontColor('#666666')
  }
  .width('86%')
  .padding(16)
  .backgroundColor('#ffffff')
  .borderRadius(12)
  .margin({ top: 40 })
}
.width('100%')
.height('100%')
.backgroundColor('#f2f3f5')

主内容区从上到下依次包含:

  1. 标题区:大号「主内容」标题 + 动态选中状态文本 + 操作提示文本,垂直排列。
  2. 打开菜单按钮:蓝色主题按钮,宽度 180vp、高度 46vp,点击后设置 this.show = true 展开侧边栏。
  3. 功能说明卡片:白色圆角卡片(圆角 12),标题「功能说明」下方带浅色分隔线,列出三条使用提示,每行间距 6vp。

3.8 SideBarContainer 属性绑定

最后是 SideBarContainer 的属性绑定部分:

.width('100%')
.layoutWeight(1)
.showSideBar(this.show)
.onChange((v: boolean) => {
  this.show = v;
})

SideBarContainer 设置为全宽(100%)、layoutWeight(1) 占满剩余空间。showSideBar(this.show) 绑定状态变量控制展开/收起,onChange 回调在侧边栏状态变化时同步更新 this.show

layoutWeight(1) 在这里非常关键——它确保 SideBarContainer 占据 Column 中除顶部返回栏之外的所有剩余空间,实现自适应布局。如果不设置 layoutWeightSideBarContainer 的高度将由内容决定,可能无法填满屏幕。

4. 交互流程详解

4.1 打开菜单流程

用户点击「打开菜单」按钮,触发以下流程:

  1. 按钮的 onClick 回调执行 this.show = true
  2. @State show 的变化触发 ArkUI 的响应式更新机制。
  3. SideBarContainer 检测到 showSideBar 属性变为 true,播放滑入动画,展示左侧菜单栏。
  4. 菜单栏从左侧滑出,覆盖在主内容区之上。

整个过程由 ArkUI 框架自动处理动画和手势,开发者只需关注状态的变化。

4.2 选中菜单项流程

用户点击菜单项,触发以下流程:

  1. 菜单项的 onClick 回调执行 this.chooseMenu(idx)
  2. chooseMenu 方法更新 this.selIdx 为点击的索引。
  3. chooseMenu 方法设置 this.show = false,触发侧边栏收起动画。
  4. chooseMenu 方法调用 promptAction.showToast 弹出提示。
  5. @State selIdx 的变化触发菜单项的样式重新渲染——选中项变蓝色加粗。
  6. 主内容区的「当前选中」文本同步更新为新选中的菜单项名称。
  7. 侧边栏收起完成后,onChange 回调被触发,this.show 被设为 false(实际上已经是 false,这一步确保状态一致)。

4.3 手势收起流程

用户通过手势或点击菜单外部区域收起侧边栏时:

  1. ArkUI 框架检测到收起手势,开始收起动画。
  2. 动画完成后,onChange 回调被触发,参数 vfalse
  3. this.show 被更新为 false,与实际状态保持一致。

如果没有 onChange 回调,this.show 仍然是 true,下次点击「打开菜单」按钮时,由于 showSideBar 已经是 true,不会触发新的展开动画,导致按钮失效。

5. UI 样式设计思路

5.1 深色侧边栏 + 浅色内容区

示例采用了经典的「深色导航 + 浅色内容」配色方案:

  • 侧边栏使用深色背景 #1f2733,白色文字,营造出专业的导航氛围。
  • 主内容区使用浅灰色背景 #f2f3f5,黑色文字,内容区域清晰可读。
  • 选中态使用主题蓝 #1a6cff,在深色和浅色背景上都有良好的视觉效果。

这种配色方案的优点是导航区和内容区层次分明,用户可以快速区分两种功能区域。

5.2 选中态视觉反馈

选中态通过三重视觉效果区分:

  • 文字颜色:从浅灰色 #cccccc 变为主题蓝 #1a6cff
  • 字重:从 FontWeight.Normal 变为 FontWeight.Bold
  • 背景色:从透明变为半透明蓝色 rgba(26,108,255,0.18)

三重效果叠加,确保选中项在任何背景下都有清晰的视觉反馈。

5.3 卡片式布局

主内容区的功能说明卡片采用卡片式设计:

  • 白色背景 #ffffff,与页面浅灰色背景形成对比。
  • 圆角 12vp,视觉柔和。
  • 宽度 86%,左右留足边距。
  • 内边距 16vp,内容不拥挤。

卡片式布局在移动端应用中非常常见,它通过边框、圆角和阴影(此示例未添加阴影)将相关信息聚合在一起,提升了页面的层次感。

6. 运行与测试

6.1 运行步骤

  1. 使用 DevEco Studio 打开项目。
  2. 运行项目到模拟器或真机。
  3. 在首页找到「侧滑菜单」示例入口,点击进入。
  4. 点击「打开菜单」按钮,观察侧边栏滑出效果。
  5. 点击任意菜单项,观察选中高亮、自动收起、Toast 提示效果。

6.2 测试场景

测试场景 预期结果
点击「打开菜单」按钮 侧边栏从左侧滑出
点击菜单项「首页」 菜单项高亮、侧边栏收起、Toast 显示「点击了首页」
点击菜单项「设置」 菜单项高亮变为「设置」、主内容区文本更新
点击菜单外部区域 侧边栏收起,this.show 更新为 false
连续快速点击菜单项 每次都正确切换选中态和收起菜单

7. 可扩展方向

7.1 Inline 模式适配

SideBarContainerType.Overlay 改为 SideBarContainerType.Inline,即可实现内联模式,侧边栏与主内容区并排显示。适合平板或横屏场景,可以通过屏幕宽度动态切换模式。

7.2 多级菜单

当前示例只有一级菜单,可以扩展为多级菜单结构。点击一级菜单项展开二级子菜单,使用 ForEach 嵌套渲染,配合动画效果实现平滑的折叠展开。

7.3 菜单图标

为每个菜单项添加图标,使用 Row 布局 + Image/Text 组件组合。图标可以使用系统内置图标或自定义资源,提升菜单的视觉辨识度。

7.4 路由导航

将菜单项与路由绑定,点击菜单项不仅更新选中态,还跳转到对应的页面。可以使用 router.pushUrlrouter.replaceUrl 实现页面导航,构建完整的应用导航体系。

7.5 持久化选中状态

使用 Preferences 存储用户最后选中的菜单项,下次进入页面时自动高亮上次选中的项,提供个性化的用户体验。

8. 常见问题与调试

8.1 侧边栏无法通过按钮打开

问题:点击「打开菜单」按钮后,侧边栏没有展开。

排查

  • 检查 show 状态是否正确更新——在 onClick 回调中添加 console.log(this.show) 确认。
  • 检查 showSideBar(this.show) 属性绑定是否正确。
  • 确认没有在其他地方重置 this.show 的值。

8.2 手势收起后按钮失效

问题:手动滑动收起侧边栏后,再次点击「打开菜单」按钮无效。

排查

  • 确认 onChange 回调是否正确设置。没有回调时,this.show 在手势收起后仍为 true,导致按钮无法触发新的展开。
  • onChange 回调中添加日志,确认回调是否被触发。

8.3 菜单项样式不更新

问题:点击菜单项后,选中项的高亮样式没有更新。

排查

  • 确认 selIdx 是否正确更新——在 chooseMenu 中添加日志。
  • 检查 ForEach 的键值生成器是否稳定。如果 key 不稳定,ArkUI 可能无法正确 diff 和重新渲染。
  • 确认条件表达式 this.selIdx === idx 是否正确——注意类型比较,selIdxnumberidx 也是 number,类型一致。

8.4 布局错乱

问题SideBarContainer 没有正确占满屏幕剩余空间。

排查

  • 确认 SideBarContainer 设置了 .layoutWeight(1),这是自适应布局的关键。
  • 检查父容器 Column 的高度是否为 100%
  • 确认顶部返回栏的高度是固定的,不会影响剩余空间的计算。

9. 技术总结

示例 91 的侧滑菜单虽然代码量不大,但涉及了 SideBarContainer 的核心用法、状态绑定与双向同步、动态样式绑定、列表渲染等多个 ArkUI 关键知识点。通过这个示例,我们可以总结出抽屉导航的实现范式:

  1. 状态驱动:用一个 @State boolean 变量控制侧边栏的展开/收起。
  2. 双向同步:同时使用 showSideBar 属性和 onChange 回调,确保状态与 UI 始终一致。
  3. 动态样式:用三元表达式根据选中索引动态计算组件样式,避免创建额外组件。
  4. 即时反馈:用 promptAction.showToast 提供轻量级的操作反馈。

掌握了这些范式后,就可以轻松地将其扩展到多级菜单、路由导航、用户中心等更复杂的场景中。侧滑导航作为移动端应用的基础导航模式,值得每个鸿蒙开发者深入学习和实践。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐