鸿蒙新特性:Panel 可滑动面板实战 — 构建多层级底部抽屉
引言
几乎每个移动端 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 在三种模式之间的切换可以通过两种方式触发:
- 用户拖拽:在手柄区域向上/向下拖拽,Panel 自动在 Mini → Half → Full 之间切换
- 编程式切换:修改
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; // 实时追踪面板高度变化
})
onHeightChange 与 onChange 的区别在于触发时机: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 个交互点
- 模式切换按钮:三个按钮(Mini/Half/Full)编程式切换面板高度,面板以 300ms EaseOut 动画过渡到新高度。
- 拖拽切换模式:在自定义拖拽条上向上拖拽展开面板(Mini→Half→Full),向下拖拽收缩面板(Full→Half→Mini)。使用 PanGesture 实现。
- 显示/隐藏切换:Toggle 开关控制面板的完全显示或隐藏。隐藏后面板消失,主内容占据全屏。
- 自定义拖拽条切换: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:
- 自定义拖拽条样式:原生 Panel 的拖拽条只有"显示/隐藏"两种状态,自定义方案可以绘制任意样式的拖拽手柄(如用品牌色、加宽、加图标)。
- 非标准高度:原生 Panel 的三档高度由系统计算,自定义方案可以定义任意高度值(如 100vp、350vp、600vp)。
- 面板外内容联动:面板高度变化时,主内容需要做复杂的联动动画(如缩放、模糊、视差效果),自定义方案可以直接在
onChange中控制任意@State变量。 - 非底部弹出方向:原生 Panel 固定从底部弹出,自定义方案可以改为从顶部、左侧、右侧弹出。
总结
本文通过构建一个"智能面板实验室",深入讲解了 HarmonyOS ArkUI 中 Panel 可滑动面板组件的核心用法:
- PanelMode 三态模型:Mini(最小化摘要)/ Half(半展开核心功能)/ Full(全展开完整内容),用户通过拖拽在三态之间切换。
- PanelType 类型:Foldable(内容自适应)、Temporary(覆盖悬浮)、Mini(仅最小化)。
- dragBar 拖拽条:控制系统拖拽条的显示/隐藏,custom 方案则可以完全自由绘制。
- onChange / onHeightChange:模式切换完成回调 vs 高度实时变化回调,不同场景使用不同粒度的监听。
- 自定义等效方案:Stack + Alignment.Bottom + PanGesture + animation + height 动态绑定,实现与原生 Panel 等价的行为但具有更高的定制自由度。
Panel 组件体现了 ArkUI"声明式 UI + 内置交互"的设计哲学——它将一个常见但实现复杂的交互模式(可拖拽多层级底部面板)封装为简洁的声明式 API。对于标准场景,原生 Panel 非常高效;对于需要深度定制的场景,理解 Panel 的底层原理后可以用自定义方案实现等效行为。
更多推荐



所有评论(0)