引言

几乎每个移动端 App 都有一个"底部面板"——地图 App 点击标记后弹出地点详情卡片,音乐 App 的播放控制条从底部滑出,系统控制中心从屏幕底部上拉展开。这些交互有一个共同模式:一个可以从底部拖拽的、多层级高度切换的面板。

在 HarmonyOS ArkUI 中,Panel 组件专门用于实现这种交互。它支持三种内置模式(Mini / Half / Full)、可拖拽切换、自定义拖拽条、以及高度变化监听。你不需要手动处理手势、计算偏移量、管理动画——Panel 把这些都封装好了。

本文将通过构建一个"智能面板实验室",深入讲解 Panel 组件的核心用法:PanelMode 三态切换、PanelType 类型选择、dragBar 拖拽条定制、onChange / onHeightChange 事件监听,以及自定义面板内容的条件渲染。

读完本文你将能够:

  • 使用 PanelMode.Mini | Half | Full 定义面板的三层高度
  • 使用 PanelType.Foldable | Temporary | Mini 控制面板的展示行为
  • 使用 dragBar 定制拖拽条的显示与样式
  • 使用 onChange 监听面板的模式切换(拖拽或编程式)
  • 使用 onHeightChange 追踪面板高度实时变化
  • 通过自定义布局 + 手势实现无内置 Panel 组件的等效方案(用于理解底层原理)

Panel 组件概述

什么是 Panel

Panel 是 ArkUI 提供的内置容器组件,专门用于构建从屏幕底部弹出的可拖拽面板。它的核心设计是一个三态高度模型:

  • Mini(最小化):面板收缩到最小高度,仅显示一行摘要信息——类似于音乐 App 底部的迷你播放条。
  • Half(半展开):面板展开到半屏高度,显示核心功能和快捷操作——类似于地图 App 的地点预览卡片。
  • Full(全展开):面板展开到接近全屏的高度,显示完整内容和列表——类似于系统设置面板。

用户可以通过在面板上拖拽来切换这三种模式:向上拖拽展开,向下拖拽收缩。Panel 内置了手势识别、惯性动画、边界吸附等交互细节,开发者只需要定义每种模式下的内容。

与 BottomSheet 的区别

如果你熟悉 ArkUI 的 bindPopup 或自定义 BottomSheet,可能会觉得 Panel 与之类似。但 Panel 有本质区别:

特性 Panel BottomSheet / bindPopup
高度层级 三档(Mini / Half / Full) 单档(固定高度或全屏)
拖拽切换 内置手势支持 需自行实现
生命周期 始终存在于组件树 弹出时挂载,关闭时卸载
默认状态 始终可见(可设为 Mini) 默认隐藏,触发时弹出
适用场景 持久性底部面板 临时性弹窗/选择器

Panel 更适合"持久存在"的底部面板——它始终在页面中,只是以不同高度展示。BottomSheet 更适合"临时触发"的弹窗——需要时才出现,关闭后完全消失。

API 废弃说明

在 API 20 中,Panel 组件被标记为 deprecated。替代方案是使用自定义布局 + 手势 + 动画来实现等效的底部面板效果(本质上就是本文 Demo 后半部分采用的自定义方案)。但在 API 24 中,Panel 仍然可用,且对于快速原型和简单场景仍然是最简洁的选择。本文会同时讲解 Panel 组件的用法和自定义方案的底层原理。

核心 API 解析

PanelMode 枚举

enum PanelMode {
  Mini,  // 最小化 — 面板收缩到最小高度,仅显示一行
  Half,  // 半展开 — 面板展开到半屏高度
  Full   // 全展开 — 面板展开到接近全屏高度
}

通过 .mode() 属性设置面板当前模式:

Panel(this.showPanel) {
  // 面板内容
}
.mode(this.currentMode)  // PanelMode.Mini | Half | Full

Panel 在三种模式之间的切换可以通过两种方式触发:

  1. 用户拖拽:在手柄区域向上/向下拖拽,Panel 自动在 Mini → Half → Full 之间切换
  2. 编程式切换:修改 mode 绑定的变量值,Panel 自动动画过渡到新模式

PanelType 枚举

enum PanelType {
  Foldable,  // 可折叠 — 内容始终在面板内,高度变化时内容区域自动调整
  Temporary, // 临时 — 面板覆盖在主内容上方,类似 Modal
  Mini       // 迷你 — 始终处于 Mini 状态,不响应拖拽切换
}

PanelType.Foldable(默认)是最常用的模式——主内容区域会随面板高度变化而自动调整(如列表重新排版),面板和内容是"折叠"关系。PanelType.Temporary 则让面板悬浮覆盖在主内容上方,主内容不受影响。

dragBar 属性

Panel(this.showPanel) {
  // ...
}
.dragBar(true)   // 显示系统默认拖拽条(顶部居中的小横条)
.dragBar(false)  // 隐藏拖拽条

默认拖拽条是一根 32vp 宽的灰色横条,位于面板顶部居中。如果你需要完全自定义拖拽条样式,可以将 dragBar 设为 false,然后在面板内容顶部自行绘制拖拽条。

onChange 回调

.onChange((width: number, height: number, mode: PanelMode) => {
  console.log(`面板模式切换为: ${mode}, 高度: ${height}`);
})

onChange 在面板完成模式切换后触发,三个参数分别是面板的宽度、高度和当前模式。在回调中更新 @State 变量(如当前模式标签),可实现状态同步。

onHeightChange 回调

.onHeightChange((height: number) => {
  this.panelHeight = height;  // 实时追踪面板高度变化
})

onHeightChangeonChange 的区别在于触发时机:onChange 在模式切换完成后触发一次,onHeightChange 在拖拽过程中持续触发(每一帧),提供实时的高度值。如果你需要做跟随面板高度的动画效果(如主内容区域的缩放或位移),使用 onHeightChange

Demo 设计:智能面板实验室

本文 Demo 实现了一个完整的可滑动面板,包含以下功能:

页面结构

Stack(根容器,Alignment.Bottom)
├── Column(主内容层)
│   ├── Header(深色标题栏:"智能面板实验室" + Panel API 标签)
│   └── Scroll
│       └── Column
│           ├── 面板控制区(Mini/Half/Full 三个按钮 + 显示/隐藏 Toggle + 自定义拖拽条 Toggle)
│           ├── 实时状态区(当前模式 / 面板可见 / 面板高度 三个标签)
│           ├── PanelMode 三模式说明
│           └── API 参考区
└── Column(自定义面板层,从底部弹出)
    ├── 自定义拖拽条(36×4vp 灰色横条 + PanGesture 拖拽手势)
    ├── [Mini 内容] 一行摘要文字
    ├── [Half 内容] 2×2 快捷操作网格
    └── [Full 内容] 全部设置列表

在这里插入图片描述
在这里插入图片描述

为什么不使用原生 Panel 组件

虽然本文的主题是 Panel 组件,但 Demo 采用了一个自定义面板方案(Stack + Column + PanGesture + animation)——原因是在 API 20+ 的原生 Panel 已标记为 deprecated,而自定义方案是官方推荐的替代方式。

更重要的是:使用自定义方案能让你深入理解 Panel 的底层原理——手势识别、高度计算、动画过渡、条件渲染。理解这些原理后,你不仅能用 Panel,还能在 Panel 不支持的场景中自如地构建等效方案。

4 个交互点

  1. 模式切换按钮:三个按钮(Mini/Half/Full)编程式切换面板高度,面板以 300ms EaseOut 动画过渡到新高度。
  2. 拖拽切换模式:在自定义拖拽条上向上拖拽展开面板(Mini→Half→Full),向下拖拽收缩面板(Full→Half→Mini)。使用 PanGesture 实现。
  3. 显示/隐藏切换:Toggle 开关控制面板的完全显示或隐藏。隐藏后面板消失,主内容占据全屏。
  4. 自定义拖拽条切换:Toggle 开关控制是否显示自定义拖拽条。关闭后面板顶部不再有拖拽手柄,仅能通过按钮切换模式。

核心实现

面板高度动画

自定义面板的核心是通过 .height().animation() 实现高度变化动画:

Column() {
  // 面板内容...
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 16, topRight: 16 })
.shadow({ radius: 12, color: '#00000015', offsetY: -2 })
.height(
  this.currentMode === PanelMode.Mini ? 60 :
  this.currentMode === PanelMode.Half ? 240 : 420
)
.animation({ duration: 300, curve: Curve.EaseOut })

三种模式对应三个固定高度值。当 currentMode 变化时,height() 的值变化触发 animation() 定义的过渡动画。Curve.EaseOut 让面板展开时有"减速停止"的自然感觉。

拖拽手势实现

自定义拖拽条使用 PanGesture 手势识别用户的垂直拖拽:

Row() {
  Row()
    .width(36)
    .height(4)
    .borderRadius(2)
    .backgroundColor('#CCCCDD')
}
.width('100%')
.height(20)
.justifyContent(FlexAlign.Center)
.gesture(
  PanGesture({ direction: PanDirection.Vertical })
    .onActionUpdate((event: GestureEvent) => {
      if (event.offsetY < -50) {
        // 向上拖拽超过 50vp → 展开
        if (this.currentMode === PanelMode.Mini) {
          this.handleModeChange(PanelMode.Half);
        } else if (this.currentMode === PanelMode.Half) {
          this.handleModeChange(PanelMode.Full);
        }
      } else if (event.offsetY > 50) {
        // 向下拖拽超过 50vp → 收缩
        if (this.currentMode === PanelMode.Full) {
          this.handleModeChange(PanelMode.Half);
        } else if (this.currentMode === PanelMode.Half) {
          this.handleModeChange(PanelMode.Mini);
        }
      }
    })
)

关键设计:

  • PanDirection.Vertical 限制手势只检测垂直方向的拖拽,避免与水平滚动冲突。
  • 阈值 50vp:不是拖拽一像素就切换(那会导致过于敏感),而是需要拖拽超过 50vp 才触发模式切换。这个阈值过滤了用户的误触和微调操作。
  • 阶梯式切换:Mini 只能到 Half,Half 可以到 Mini 或 Full,Full 只能到 Half——不会从 Mini 直接跳到 Full。

条件内容渲染

不同模式下显示不同内容,通过 if 条件实现:

// Mini 模式 — 一行摘要
if (this.currentMode === PanelMode.Mini) {
  Row() {
    Text('面板最小化')
      .fontSize(13)
      .fontColor('#1a1a2e')
      .fontWeight(FontWeight.Medium)
    Blank()
    Text('上滑展开')
      .fontSize(11)
      .fontColor('#888899')
  }
  .width('100%')
  .padding({ left: 16, right: 16, top: 8, bottom: 12 })
}

// Half 模式 — 2×2 快捷操作网格
if (this.currentMode === PanelMode.Half) {
  Column() {
    Text('快捷操作')
      .fontSize(14)
      .fontColor('#1a1a2e')
      .fontWeight(FontWeight.Medium)
      .margin({ bottom: 10 })

    Grid() {
      ForEach(this.quickActions, (item: PanelDemoItem) => {
        GridItem() { /* ... */ }
      })
    }
    .columnsTemplate('1fr 1fr')
    .columnsGap(8)
    .rowsGap(8)
  }
}

// Full 模式 — 完整设置列表
if (this.currentMode === PanelMode.Full) {
  Column() {
    Text('全部设置')
      .fontSize(15)
      .fontColor('#1a1a2e')
      .fontWeight(FontWeight.Medium)
      .margin({ bottom: 10 })

    ForEach(this.allSettings, (item: PanelDemoItem) => {
      Row() { /* 设置项 */ }
    })
  }
}

每种模式下的内容是独立的条件分支。当面板高度变化时,内容随 currentMode 切换而改变,创建出"同一面板、不同层级"的体验。

实时高度追踪

使用 onAreaChange 回调追踪面板的实际渲染高度:

.onAreaChange((oldValue: Area, newValue: Area) => {
  this.panelHeight = newValue.height as number;
})

这个高度值实时显示在状态区中,让用户直观看到面板的当前高度。在真实的 Panel 组件中,onHeightChange 回调提供相同的功能。

Stack + Alignment.Bottom 布局

自定义面板通过 Stack 容器定位到底部:

Stack({ alignContent: Alignment.Bottom }) {
  // 主内容层 — 占据全屏
  Column() {
    // Header + Scroll...
  }
  .width('100%')
  .height('100%')

  // 面板层 — 从底部弹出,覆盖在主内容上方
  if (this.panelVisible) {
    Column() {
      // 拖拽条 + 面板内容...
    }
    .width('100%')
    .backgroundColor('#FFFFFF')
    .borderRadius({ topLeft: 16, topRight: 16 })
    .shadow({ radius: 12, color: '#00000015', offsetY: -2 })
    .height(/* 根据模式动态计算 */)
    .animation({ duration: 300, curve: Curve.EaseOut })
  }
}

Alignment.Bottom 将面板固定在 Stack 底部。当面板高度变化时,面板从底部向上生长或向下收缩——这与原生 Panel 的行为一致。

底部面板的关键视觉元素:

  • borderRadius({ topLeft: 16, topRight: 16 }) — 顶部圆角,让面板看起来像一张从底部抽出的卡片
  • shadow — 轻微阴影,在面板和主内容之间建立视觉层次
  • backgroundColor('#FFFFFF') — 白色背景,与主内容的灰色背景形成对比

原生 Panel 的使用方式

虽然 Demo 使用自定义方案,但原生 Panel 的用法也非常简洁。以下是等效的原生 Panel 实现:

build() {
  Column() {
    // 主内容
    Scroll() { /* ... */ }
    .layoutWeight(1)
  }
  .width('100%')
  .height('100%')
}

// 原生 Panel — 自动定位到底部,内置拖拽手势
Panel(this.panelVisible) {
  Column() {
    if (this.currentMode === PanelMode.Mini) { /* Mini 内容 */ }
    if (this.currentMode === PanelMode.Half) { /* Half 内容 */ }
    if (this.currentMode === PanelMode.Full) { /* Full 内容 */ }
  }
}
.mode(this.currentMode)
.type(PanelType.Foldable)
.dragBar(true)
.onChange((width: number, height: number, mode: PanelMode) => {
  this.currentMode = mode;
})

相比自定义方案,原生 Panel 省略了:Stack 布局、手势处理、高度动画设置。如果你不需要深度定制面板外观(如自定义圆角、阴影、拖拽条样式),原生 Panel 是更高效的选择。

实际应用场景

音乐播放器

最经典的应用场景。Mini 模式显示当前歌曲名和播放/暂停按钮,Half 模式增加进度条和收藏按钮,Full 模式显示完整歌词和播放列表。

地图地点详情

点击地图上的标记后,Panel 以 Half 模式显示地点名称、评分和简要信息。上滑到 Full 显示完整评论、照片和营业时间。下滑到 Mini 仅显示地点名称,让用户重新关注地图。

快捷控制面板

类似 iOS 控制中心或 Android 快速设置。Mini 模式显示一行快捷开关(WiFi、蓝牙、手电筒),Half 模式增加亮度和音量滑块,Full 模式展示所有可用的控制项。

表单/评论区

Half 模式显示少量核心输入项(如评论输入框),Full 模式展开后显示完整表单(包括附件上传、格式工具栏等)。

自定义方案的适用场景

当你需要以下效果时,自定义方案优于原生 Panel:

  1. 自定义拖拽条样式:原生 Panel 的拖拽条只有"显示/隐藏"两种状态,自定义方案可以绘制任意样式的拖拽手柄(如用品牌色、加宽、加图标)。
  2. 非标准高度:原生 Panel 的三档高度由系统计算,自定义方案可以定义任意高度值(如 100vp、350vp、600vp)。
  3. 面板外内容联动:面板高度变化时,主内容需要做复杂的联动动画(如缩放、模糊、视差效果),自定义方案可以直接在 onChange 中控制任意 @State 变量。
  4. 非底部弹出方向:原生 Panel 固定从底部弹出,自定义方案可以改为从顶部、左侧、右侧弹出。

总结

本文通过构建一个"智能面板实验室",深入讲解了 HarmonyOS ArkUI 中 Panel 可滑动面板组件的核心用法:

  1. PanelMode 三态模型:Mini(最小化摘要)/ Half(半展开核心功能)/ Full(全展开完整内容),用户通过拖拽在三态之间切换。
  2. PanelType 类型:Foldable(内容自适应)、Temporary(覆盖悬浮)、Mini(仅最小化)。
  3. dragBar 拖拽条:控制系统拖拽条的显示/隐藏,custom 方案则可以完全自由绘制。
  4. onChange / onHeightChange:模式切换完成回调 vs 高度实时变化回调,不同场景使用不同粒度的监听。
  5. 自定义等效方案:Stack + Alignment.Bottom + PanGesture + animation + height 动态绑定,实现与原生 Panel 等价的行为但具有更高的定制自由度。

Panel 组件体现了 ArkUI"声明式 UI + 内置交互"的设计哲学——它将一个常见但实现复杂的交互模式(可拖拽多层级底部面板)封装为简洁的声明式 API。对于标准场景,原生 Panel 非常高效;对于需要深度定制的场景,理解 Panel 的底层原理后可以用自定义方案实现等效行为。


Logo

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

更多推荐