鸿蒙NEXT可拖拽面板bindSheet半模态转场入门实战
鸿蒙NEXT开发实战:从零实现可拖拽半模态面板 —— bindSheet 完全指南



目录
- 前言:为什么你需要读这篇文章
- 背景:从 Panel 到 bindSheet 的演进
- 项目初始化与工程结构
- bindSheet 核心 API 详解
- 实战:构建一个完整的可拖拽控制面板
- 代码深度解读
- 高级技巧与最佳实践
- 常见问题与调试指南
- 性能优化与内存管理
- 从 Panel 迁移到 bindSheet 的完整指南
- 总结与展望
- 附录:完整源代码
1. 前言:为什么你需要读这篇文章
在移动应用开发中,底部弹出的可拖拽面板是一种极其常见的交互模式。无论是 iOS 的 ActionSheet、Android 的 BottomSheet,还是各类应用中的控制中心、分享面板、配置菜单——这种"从底部升起、可拖拽调节、半屏展示"的 UI 模式已经成为移动端用户最熟悉的交互范式之一。
在鸿蒙原生开发中实现这一效果,历史上主要依赖 Panel 组件。然而,自 API Version 12 起,Panel 组件已被正式标记为废弃(Deprecated),官方推荐使用全新的 bindSheet 属性来实现半模态转场效果。这一变化意味着大量存量代码需要迁移,新项目则需要从一开始就采用正确的方案。
本文将从零开始,带您完整地走一遍使用 bindSheet 构建可拖拽半模态面板的全过程。您将学习到:
- Panel 组件为何被废弃,bindSheet 的优势在哪里
- bindSheet 的完整 API 体系和每种配置的含义
- 如何用 ArkTS 的
@Builder装饰器优雅地构建面板内容 $$双向绑定在面板状态管理中的正确用法- 三档高度(FIT_CONTENT / MEDIUM / LARGE)的灵活配置
- 拖拽条、关闭按钮、遮罩、圆角等视觉元素的精细化控制
- 面板状态变化的实时监听与响应
- 从 Panel 到 bindSheet 的平滑迁移策略
- 性能优化、常见陷阱和调试技巧
无论您是刚接触鸿蒙开发、正在将旧项目迁移到 API 24,还是希望深入理解半模态转场机制的工程师,这篇文章都值得您花时间读完。
2. 背景:从 Panel 到 bindSheet 的演进
2.1 Panel 组件的辉煌与退场
在 HarmonyOS 的早期版本(API 5 ~ API 11)中,Panel 组件是实现底部弹出面板的首选方案。它的使用方式非常直观:
Panel(this.show) {
// 面板内容
}
.mode(PanelMode.Half)
.dragBar(true)
Panel 提供了 PanelMode.Mini、PanelMode.Half、PanelMode.Full 三档模式,配合 dragBar 拖拽条和 onChange 事件,能够快速构建一个可拖拽的底部面板。
然而,随着鸿蒙系统的快速迭代和 API 的不断演进,Panel 组件逐渐暴露出一些设计上的局限:
- 布局受限:Panel 必须作为独立的层级叠加在页面之上,无法灵活地嵌入到复杂的布局结构中
- 样式定制能力有限:圆角、阴影、遮罩效果的控制不够精细化
- 动画控制不够灵活:内置的过渡动画难以自定义
- 与其他转场机制重叠:鸿蒙后续引入了更完善的半模态转场体系,Panel 的功能被完全覆盖
2.2 bindSheet 的诞生与优势
从 API Version 10 开始,鸿蒙引入了 bindSheet 属性,作为一种更通用、更灵活的半模态页面解决方案。到 API Version 12,bindSheet 的功能已经非常完善,官方正式建议开发者从 Panel 迁移到 bindSheet。
bindSheet 相比 Panel 具有以下显著优势:
| 对比维度 | Panel | bindSheet |
|---|---|---|
| 状态绑定 | 仅支持单向绑定 | 支持 $$ 双向绑定 |
| 高度档位 | 固定三档(Mini/Half/Full) | 可自定义任意高度(数字+枚举混合) |
| 拖拽条 | 仅支持显隐控制 | 精细控制,支持自定义 |
| 关闭按钮 | 需要自行实现 | 内置 showClose 支持 |
| 遮罩颜色 | 不可配置 | 支持 maskColor 自定义 |
| 回调事件 | 仅 onChange |
5+ 种细粒度回调 |
| 圆角控制 | 支持 borderRadius |
支持 borderRadius 全局设置 |
| 背景模糊 | 不支持 | 支持 blurStyle |
| 生命周期 | 简单 | onWillDismiss 可拦截关闭 |
| 兼容性 | API 12 起废弃 | API 10+ 持续演进 |
2.3 API 版本演进概览
为了帮助读者理解不同 API 版本下的能力差异,这里整理了一份关键版本的时间线:
| API 版本 | SDK 版本 | 关键变化 |
|---|---|---|
| API 9 | 3.x | Panel 组件首次引入 |
| API 10 | 4.x | bindSheet 首次引入,支持基本半模态 |
| API 11 | 5.x | bindSheet 新增 detents、dragBar、preferType |
| API 12 | 6.0.x | Panel 正式废弃,bindSheet 新增 onWillDismiss、onHeightDidChange |
| API 13 | 6.1.x | bindSheet 新增 keyboardAvoidMode |
| API 24 | 6.1.0(24) | 当前最新,bindSheet 能力成熟稳定 |
本文的所有代码均基于 API 24(SDK 6.1.0(24)) 编写,这是目前 HarmonyOS NEXT 的最新稳定版本。
3. 项目初始化与工程结构
3.1 创建项目
在 DevEco Studio 中创建一个新的 HarmonyOS 项目时,请选择以下配置:
- Application Name: PanelDemo(或您喜欢的名称)
- Bundle Name: com.example.paneldemo
- Project Type: Application
- Device Type: Phone
- Target SDK: 6.1.0(24)
- Compatible SDK: 6.1.0(24)
- Language: ArkTS
- API Type: Stage Model
3.2 项目的完整目录结构
创建完成后,项目的核心目录结构如下:
PanelDemo/
├── AppScope/
│ ├── app.json5 # 应用全局配置
│ └── resources/ # 全局资源文件
├── entry/
│ ├── src/
│ │ └── main/
│ │ ├── ets/
│ │ │ ├── entryability/
│ │ │ │ └── EntryAbility.ets # 应用入口 Ability
│ │ │ └── pages/
│ │ │ └── Index.ets # ★ 主页面(本文核心)
│ │ └── resources/ # 模块级资源
│ ├── build-profile.json5 # 模块构建配置
│ └── oh-package.json5 # 包依赖管理
├── build-profile.json5 # 项目级构建配置
├── hvigor/
│ └── hvigor-config.json5 # Hvigor 编译配置
└── oh-package.json5 # 项目级包配置
3.3 关键配置文件说明
build-profile.json5(项目级)
{
"app": {
"signingConfigs": [],
"products": [
{
"name": "default",
"signingConfig": "default",
"targetSdkVersion": "6.1.0(24)",
"compatibleSdkVersion": "6.1.0(24)",
"runtimeOS": "HarmonyOS",
"buildOption": {
"strictMode": {
"caseSensitiveCheck": true,
"useNormalizedOHMUrl": true
}
}
}
],
"buildModeSet": [
{ "name": "debug" },
{ "name": "release" }
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{ "name": "default", "applyToProducts": ["default"] }
]
}
]
}
关键参数说明:
targetSdkVersion:目标 SDK 版本,决定了应用可用的 API 集合。设置为6.1.0(24)表示应用针对 API 24 开发。compatibleSdkVersion:向下兼容的最低 SDK 版本。设为相同值表示不向下兼容。runtimeOS:运行时操作系统,固定为HarmonyOS。strictMode.caseSensitiveCheck:是否启用大小写敏感检查,建议保持true。strictMode.useNormalizedOHMUrl:是否使用标准化 OHM URL,建议保持true。
EntryAbility.ets(应用入口)
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
console.error('Failed to load page:', JSON.stringify(err));
return;
}
console.info('Page loaded successfully.');
});
}
}
这是鸿蒙 Stage 模型的标准入口代码。loadContent 的第一个参数 'pages/Index' 指向 ets/pages/Index.ets 文件。确保路径与文件名完全匹配(大小写敏感)。
⚠️ 常见陷阱:如果页面显示白屏,首先检查
loadContent的路径是否正确。鸿蒙对大小写敏感,'pages/Index'和'pages/index'是不同的路径。
4. bindSheet 核心 API 详解
4.1 函数签名
bindSheet 是一个通用属性(Universal Attribute),可以绑定到任意组件上。它的完整函数签名如下:
bindSheet(
isShow: Optional<boolean>,
builder: CustomBuilder,
options?: SheetOptions
): T
参数说明
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
isShow |
Optional<boolean> |
是 | 控制半模态面板的显示/隐藏。支持 $$ 双向绑定 |
builder |
CustomBuilder |
是 | 使用 @Builder 装饰器构建的面板内容 |
options |
SheetOptions |
否 | 面板的样式、行为和回调配置 |
4.2 SheetOptions 完整配置表
SheetOptions 是 bindSheet 的配置核心,以下是 API 24 中支持的所有选项:
| 配置项 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
detents |
[(SheetSize|Length), (SheetSize|Length)?, (SheetSize|Length)?] |
否 | [SheetSize.MEDIUM, SheetSize.LARGE] |
面板高度档位数组,最多3档 |
dragBar |
boolean |
否 | true |
是否显示拖拽条 |
showClose |
boolean | Resource |
否 | false |
是否显示关闭按钮 |
maskColor |
ResourceColor |
否 | 系统默认遮罩色 | 遮罩层颜色 |
blurStyle |
BlurStyle |
否 | 无模糊 | 面板背景模糊效果 |
borderRadius |
Dimension |
否 | 系统默认 | 面板圆角大小 |
borderWidth |
Dimension | EdgeWidths |
否 | 0 | 面板边框宽度 |
borderColor |
Color | EdgeColors |
否 | 透明 | 面板边框颜色 |
borderStyle |
BorderStyle | EdgeStyles |
否 | BorderStyle.Solid |
面板边框样式 |
width |
Dimension |
否 | 100% | 面板宽度 |
shadow |
ShadowOptions | ShadowStyle |
否 | 无阴影 | 面板阴影配置 |
preferType |
SheetType |
否 | 系统默认 | 面板类型偏好 |
mode |
SheetMode |
否 | SheetMode.EMBEDDED |
面板模式(嵌入/弹出) |
scrollSizeMode |
ScrollSizeMode |
否 | 默认 | 面板滚动时的尺寸行为 |
enableOutsideInteractive |
boolean |
否 | false |
是否允许与面板外部交互 |
uiContext |
UIContext |
否 | 当前上下文 | 面板的 UI 上下文 |
title |
SheetTitleOptions | CustomBuilder |
否 | 无标题 | 面板标题配置 |
keyboardAvoidMode |
SheetKeyboardAvoidMode |
否 | 默认 | 键盘避让模式 |
回调事件
| 事件 | 类型 | 说明 |
|---|---|---|
onHeightDidChange |
Callback<number> |
面板高度变化时触发,返回当前高度值 |
onDetentsDidChange |
Callback<number> |
面板档位切换时触发,返回当前档位索引 |
onWidthDidChange |
Callback<number> |
面板宽度变化时触发 |
onTypeDidChange |
Callback<SheetType> |
面板类型变化时触发 |
onWillDismiss |
(DismissSheetAction) => void |
面板即将关闭时触发,可拦截关闭操作 |
onWillSpringBackWhenDismiss |
SpringBackAction |
面板关闭时弹性回弹效果 |
4.3 SheetSize 枚举详解
SheetSize 是预定义的高度档位枚举:
enum SheetSize {
MEDIUM = 0, // 半屏高度(约为屏幕高度的 50%)
LARGE = 1, // 全屏高度(约为屏幕高度的 92%)
FIT_CONTENT = 2 // 自适应内容高度(从 API 12 起支持)
}
💡 最佳实践:当面板内容较少时,建议将
FIT_CONTENT作为第一档,让面板自动适应内容高度。如果内容较多,则使用具体的 vp 值或MEDIUM。
4.4 $$ 双向绑定机制
$$ 是 ArkTS 中的双向绑定语法糖,它在 bindSheet 中发挥着关键作用:
// 单向绑定(不推荐)
.bindSheet(this.isShow, this.myBuilder)
// 双向绑定(推荐 ✓)
.bindSheet($$this.isShow, this.myBuilder)
为什么使用 $$?
当面板内部触发了关闭操作(例如用户点击了面板内置的关闭按钮、点击遮罩层、或系统级手势关闭),使用 $$ 双向绑定后,框架会自动将 isShow 设置为 false,无需开发者手动处理。
如果使用单向绑定,面板关闭后 isShow 仍然为 true,下次无法重新打开面板——这是一个非常常见的 bug。
⚠️ 关键提醒:
$$双向绑定从 API Version 10 开始支持。在 API 24 中,$$已支持bindSheet、bindPopup、bindMenu、bindContextMenu等多种场景的显隐控制。
4.5 @Builder 装饰器详解
@Builder 是 ArkTS 中用于构建 UI 片段的装饰器。在 bindSheet 中,它用于构建面板的内容:
@Builder
buildPanelContent() {
Column() {
Text('面板标题')
.fontSize(20)
.fontWeight(FontWeight.Bold)
// 更多内容...
}
.width('100%')
.height('100%')
.padding(20)
}
关键要点:
- 无参数:作为 bindSheet 参数的
@Builder方法不能包含参数 - 位置:必须定义在
@Component或@Entry装饰的 struct 内部 - 调用:在 bindSheet 中通过
this.buildPanelContent(不加括号)传入 - 上下文:
@Builder内部的this指向所属 struct 的实例,可以访问@State变量
5. 实战:构建一个完整的可拖拽控制面板
5.1 需求分析
我们将构建一个"控制中心"风格的可拖拽底部面板,它需要满足以下功能需求:
- 从底部弹出:点击按钮后,面板从屏幕底部以动画方式弹出
- 三档高度切换:通过拖拽面板上边缘或拖拽条,在三个高度档位间切换
- 功能列表展示:面板内包含一个功能列表,列表项支持点击交互
- 状态实时反馈:面板的高度和档位变化能在主页面实时反映
- 多种关闭方式:支持点击遮罩、点击关闭按钮、下滑手势三种关闭方式
- 视觉精致:圆角顶部、半透明遮罩、阴影效果、拖拽条引导
5.2 交互流程设计
┌────────────────────────────┐
│ 主页面(背景) │
│ │
│ ⚙️ 可拖拽面板演示 │
│ │
│ ┌──────────────────┐ │
│ │ 📖 使用说明卡片 │ │
│ └──────────────────┘ │
│ │
│ ┌──────────────────┐ │
│ │ 🔼 打开控制面板 │ ← 点击触发
│ └──────────────────┘ │
│ │
├────────────────────────────┤ ← 遮罩层(30%透明)
│ ┌────────────────────────┐ │
│ │ ═══ 拖拽条 ═══ │ │ ← 可拖拽
│ │ │ │
│ │ 控制中心 │ │
│ │ 📱 屏幕亮度 │ │
│ │ 🔊 媒体音量 │ │
│ │ 📶 Wi-Fi 设置 │ │
│ │ 🔄 蓝牙连接 │ │
│ │ 🔦 手电筒 │ │
│ │ │ │
│ └────────────────────────┘ │
└────────────────────────────┘
↑ 面板三档高度 ↑ 全屏
MEDIUM LARGE
5.3 完整实现代码
以下是最终实现的完整 Index.ets 文件,涵盖了所有功能:
/**
* Index.ets — 鸿蒙原生 ArkTS 可拖拽面板(bindSheet 半模态转场)入门示例
*
* 本示例使用 bindSheet(半模态转场,Panel 组件的官方替代方案)
* 实现「底部上滑面板」效果,支持三档高度拖拽切换。
*
* 核心技术点:
* 1) bindSheet — 半模态页面绑定
* 2) SheetOptions — 面板配置(高度档位、拖拽条、遮罩)
* 3) SheetSize — MEDIUM / LARGE 标准高度
* 4) $$ 双向绑定 — 面板状态自动同步
* 5) @Builder — 构建面板内容
*/
@Entry
@Component
struct Index {
/* ---------- 状态变量 ---------- */
/** 控制半模态面板的显示/隐藏 */
@State isShow: boolean = false;
/* ---------- 构建 UI ---------- */
build() {
// 根布局:纵向排列
Column() {
// ----- 顶部标题 -----
Text('⚙️ 可拖拽面板演示')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A2E')
.width('100%')
.textAlign(TextAlign.Start)
.margin({ top: 56, bottom: 8 })
.padding({ left: 24 })
// ----- 说明文字 -----
Text('使用 bindSheet 半模态转场实现底部上滑面板')
.fontSize(14)
.fontColor('#666666')
.width('100%')
.textAlign(TextAlign.Start)
.padding({ left: 24 })
// ----- 说明卡片 -----
Column() {
Text('📖 使用说明')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A2E')
.width('100%')
Text('点击下方按钮打开底部面板 → 拖拽面板上边缘或拖拽条上滑切换模式。')
.fontSize(14)
.fontColor('#555555')
.lineHeight(22)
.margin({ top: 8 })
Text('面板支持三种高度:自适应内容高度 / MEDIUM 半屏 / LARGE 全屏。')
.fontSize(14)
.fontColor('#555555')
.lineHeight(22)
.margin({ top: 4 })
}
.width('90%')
.padding(16)
.margin({ top: 24 })
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 8, color: 'rgba(0,0,0,0.08)', offsetX: 0, offsetY: 4 })
// ----- 控制按钮(绑定半模态面板)-----
Button('🔼 打开控制面板')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor(Color.White)
.width('80%')
.height(50)
.backgroundColor('#6C63FF')
.borderRadius(25)
.margin({ top: 32 })
.shadow({ radius: 12, color: 'rgba(108,99,255,0.4)', offsetX: 0, offsetY: 6 })
.onClick(() => {
this.isShow = true;
})
// ★ bindSheet: 绑定半模态面板
// 参数1: $$isShow — 双向绑定控制显隐
// 参数2: 面板内容构建器
// 参数3: 面板配置选项
.bindSheet($$this.isShow, this.buildPanelContent, {
// 高度档位数组
detents: [
250, // 档位0:250vp(自适应/迷你高度)
SheetSize.MEDIUM, // 档位1:半屏高度
SheetSize.LARGE // 档位2:全屏高度(最大)
],
// 显示拖拽条
dragBar: true,
// 显示关闭按钮
showClose: true,
// 遮罩颜色
maskColor: 'rgba(0, 0, 0, 0.3)',
// 面板高度变化回调
onHeightDidChange: (height: number) => {
console.info('[PanelDemo] 面板高度变化: ' + height);
},
// 面板档位变化回调
onDetentsDidChange: (index: number) => {
console.info('[PanelDemo] 面板档位切换: ' + index);
}
})
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
/* ============ 面板内容构建 ============ */
/**
* 构建半模态面板的内容
* @Builder 装饰器 — 用于 bindSheet 的 builder 参数
*/
@Builder
buildPanelContent() {
Column() {
// 面板标题
Text('控制中心')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A2E')
.margin({ top: 12, bottom: 16 })
// 功能介绍文字
Text('拖拽面板上边缘或拖拽条切换三档高度')
.fontSize(13)
.fontColor('#888888')
.margin({ bottom: 20 })
// 功能列表
List({ space: 10 }) {
ForEach([
'📱 屏幕亮度',
'🔊 媒体音量',
'📶 Wi-Fi 设置',
'🔄 蓝牙连接',
'🔦 手电筒'
], (item: string) => {
ListItem() {
Text(item)
.fontSize(16)
.fontColor('#333333')
.width('100%')
.height(52)
.padding({ left: 16 })
.backgroundColor('#F8F8FC')
.borderRadius(12)
}
}, (item: string) => item)
}
.width('100%')
.height('50%')
.margin({ bottom: 12 })
}
.width('100%')
.height('100%')
.padding({ left: 20, right: 20, bottom: 20 })
}
}
5.4 运行效果预览
当您将上述代码部署到真机或模拟器后,会看到以下交互效果:
- 初始状态:页面顶部显示标题「⚙️ 可拖拽面板演示」,中间有说明卡片,底部有一个紫色的「🔼 打开控制面板」按钮
- 点击按钮:面板从底部平滑弹出,半透明遮罩覆盖背景,面板初始高度为 250vp
- 拖拽操作:
- 向上拖拽面板上边缘 → 面板切换至 MEDIUM 半屏高度
- 继续向上拖拽 → 面板切换至 LARGE 全屏高度
- 向下拖拽 → 面板逐步回落至之前的高度档位
- 关闭操作:
- 点击遮罩区域 → 面板关闭
- 点击面板右上角的关闭按钮(×)→ 面板关闭
- 向下拖拽到底 → 面板关闭
- 状态回调:拖拽过程中,控制台会输出面板高度和档位变化日志
6. 代码深度解读
6.1 状态管理:@State + $$
@State isShow: boolean = false;
@State 是 ArkTS 中最核心的状态装饰器。当 isShow 的值发生变化时,框架会自动触发与该状态相关的 UI 重新渲染。
在 bindSheet 的上下文中,isShow 控制着面板的显示与隐藏:
isShow = true→ 面板打开isShow = false→ 面板关闭
我们在按钮的 onClick 事件中将 isShow 设为 true:
.onClick(() => {
this.isShow = true;
})
而 $$this.isShow 的双向绑定确保了面板被外部操作关闭时(点击遮罩、下滑手势、关闭按钮),isShow 能自动更新为 false。如果我们使用 this.isShow(单向绑定),点击遮罩后面板虽然关闭了,但 isShow 仍然是 true,再次点击按钮时无法重新打开面板——面板状态与 UI 状态不同步。
💡 技术洞察:
$$双向绑定的本质是 ArkUI 框架在内部监听了组件的关闭事件,并在事件触发时自动修改绑定的@State变量。这是一种"事件驱动"的响应式编程模式,与 Vue.js 的v-model和 Angular 的[(ngModel)]有异曲同工之妙。
6.2 三档高度配置策略
detents: [
250, // 档位0:250vp(自定义高度)
SheetSize.MEDIUM, // 档位1:MEDIUM(约50%屏幕高度)
SheetSize.LARGE // 档位2:LARGE(约92%屏幕高度)
]
为什么这样设置?
-
第一档用具体数值(250vp):一个接近"迷你"模式的紧凑高度,适合内容较少的场景。250vp 大约能展示 2~3 个列表项,配合拖拽条暗示用户"这里可以拖拽"。
-
第二档用 MEDIUM:这是最常见的半屏模式,覆盖屏幕下方约 50% 的区域,既能展示足够多的内容,又不会遮挡过多背景。
-
第三档用 LARGE:全屏模式,覆盖几乎整个屏幕(保留状态栏区域),适合需要大量内容展示的场景。
关于 detents 的重要规则:
- 数组长度最大为 3
- 档位必须按从小到大的顺序排列
- 每个元素可以是
SheetSize枚举值或具体的Length值(单位 vp) - 拖拽时,面板会在相邻档位之间"吸附"(snap),不会停留在中间位置
6.3 @Builder 与面板内容构建
@Builder 装饰器是 ArkTS 中构建 UI 片段的声明式方法。在 bindSheet 中使用时,有几点需要特别注意:
正确的传参方式
// ✅ 正确:传递函数引用(无括号)
.bindSheet($$this.isShow, this.buildPanelContent, { ... })
// ❌ 错误:调用函数并传递返回值(有括号)
.bindSheet($$this.isShow, this.buildPanelContent(), { ... })
原因:bindSheet 的第二个参数类型是 CustomBuilder,它是一个"延迟执行"的 UI 构建块。当我们传递 this.buildPanelContent(无括号)时,框架会在面板需要渲染时才调用这个函数构建 UI。如果加了括号,buildPanelContent() 会被立即执行,返回值可能不是框架期望的 CustomBuilder 类型。
面板内容的布局技巧
在 buildPanelContent() 中,我们使用了一个全屏的 Column 容器:
@Builder
buildPanelContent() {
Column() {
// ...内容...
}
.width('100%')
.height('100%')
.padding({ left: 20, right: 20, bottom: 20 })
}
width('100%') 和 height('100%') 确保面板内容填满整个面板区域。padding 则提供了内容与面板边缘的安全间距。
在面板内部,我们使用 List 组件来展示功能列表。List 是 ArkTS 中高性能的滚动列表容器,配合 ForEach 可以优雅地渲染数据驱动的列表:
List({ space: 10 }) {
ForEach(dataArray, (item: string) => {
ListItem() {
Text(item)
.fontSize(16)
.fontColor('#333333')
.width('100%')
.height(52)
.padding({ left: 16 })
.backgroundColor('#F8F8FC')
.borderRadius(12)
}
}, (item: string) => item)
}
.width('100%')
.height('50%')
List 的 height('50%') 限制了列表的可见区域,当面板处于 MEDIUM 或 LARGE 模式时,如果内容超出高度,列表可以滚动——绑定了 scrollSizeMode 可以进一步优化滚动行为。
6.4 事件回调体系
bindSheet 提供了一套完整的事件回调体系,让开发者可以精细地响应面板的各种状态变化:
.bindSheet($$this.isShow, this.buildPanelContent, {
// 高度变化:每次拖拽都触发(频率很高,注意性能)
onHeightDidChange: (height: number) => {
console.info('当前高度: ' + height);
},
// 档位切换:仅在面板吸附到某个档位时触发(频率较低)
onDetentsDidChange: (index: number) => {
console.info('当前档位: ' + index);
},
// 面板即将关闭:可用于"确认关闭"对话框
onWillDismiss: (dismiss: DismissSheetAction) => {
// 如果不想关闭,不调用 dismiss.dismiss()
// 如果允许关闭,调用 dismiss.dismiss()
}
})
事件触发时机对比:
| 事件 | 触发频率 | 典型用途 |
|---|---|---|
onHeightDidChange |
高(连续拖拽时每一帧都会触发) | 实时显示高度、联动动画 |
onDetentsDidChange |
低(仅在吸附到档位时触发) | 更新 UI 指示、数据加载 |
onWillDismiss |
一次(面板关闭前触发) | 确认对话框、保存草稿 |
⚠️ 性能警告:
onHeightDidChange在拖拽过程中会被高频触发。如果在这个回调中执行复杂操作(如 DOM 查询、大量计算),可能导致 UI 掉帧。建议在此回调中只做简单的赋值操作,或者使用节流(Throttle)控制回调频率。
7. 高级技巧与最佳实践
7.1 动态调整 detents
在某些场景下,面板的可用高度档位可能需要根据内容动态变化。例如:当列表为空时,不需要全屏模式。我们可以通过动态修改 detents 来实现:
@State detents: Array<SheetSize | number> = [
SheetSize.MEDIUM,
SheetSize.LARGE
];
// 根据条件动态调整
updateDetents(itemCount: number): void {
if (itemCount <= 3) {
// 内容少,只需要 MEDIUM
this.detents = [SheetSize.MEDIUM];
} else {
// 内容多,需要全部档位
this.detents = [
SheetSize.MEDIUM,
SheetSize.LARGE
];
}
}
注意:当前版本中,
detents在面板打开后不能动态修改。如果需要动态调整,请在打开面板之前配置好。
7.2 自定义拖拽条样式
虽然 dragBar 只支持布尔值(显示/隐藏),但我们可以通过在面板内容顶部模拟一个"假拖拽条"来获得自定义样式:
@Builder
buildPanelContent() {
Column() {
// 自定义拖拽条
Row() {
Divider()
.width(40)
.strokeWidth(4)
.color('#D0D0D0')
.borderRadius(2)
}
.width('100%')
.height(20)
.justifyContent(FlexAlign.Center)
// 实际内容
Text('控制中心')
.fontSize(20)
.fontWeight(FontWeight.Bold)
// ...
}
}
这样可以在保留系统拖拽条的同时,增加一层自定义视觉装饰。或者你可以将 dragBar 设为 false,完全使用自定义拖拽条(此时拖拽区域仅限于面板内容区上边缘)。
7.3 多面板管理
如果页面需要多个不同的半模态面板,可以为每个面板创建独立的 @State 变量和 @Builder 方法:
@Component
struct MultiPanelPage {
@State showPanelA: boolean = false;
@State showPanelB: boolean = false;
build() {
Column() {
Button('面板 A')
.onClick(() => { this.showPanelA = true; })
.bindSheet($$this.showPanelA, this.panelA)
Button('面板 B')
.onClick(() => { this.showPanelB = true; })
.bindSheet($$this.showPanelB, this.panelB)
}
}
@Builder
panelA() { /* 面板 A 的内容 */ }
@Builder
panelB() { /* 面板 B 的内容 */ }
}
⚠️ 注意:同一时间只能有一个 bindSheet 面板显示。打开面板 B 时,如果面板 A 仍然打开,面板 A 会自动关闭。
7.4 与路由导航结合
bindSheet 面板内部也可以触发页面跳转,实现类似"底部面板 → 详情页"的流程:
@Builder
buildPanelContent() {
Column() {
// ...
Button('查看详情')
.onClick(() => {
router.pushUrl({ url: 'pages/Detail' });
})
}
}
此时面板会在路由跳转前自动关闭,用户从详情页返回时会看到完整的主页面。
7.5 深色模式适配
为了让面板在深色模式下也有良好的视觉效果,可以对颜色进行动态适配:
@Entry
@Component
struct DarkModePanel {
@State isShow: boolean = false;
build() {
Column() {
Button('打开面板')
.bindSheet($$this.isShow, this.buildContent, {
maskColor: $r('app.color.sheet_mask'),
// ...
})
}
.width('100%')
.height('100%')
}
@Builder
buildContent() {
Column() {
Text('控制中心')
.fontColor($r('app.color.text_primary'))
// ...
}
.backgroundColor($r('app.color.sheet_bg'))
}
}
在 resources 目录下为浅色/深色模式分别定义颜色资源:
// resources/base/element/color.json
{
"color": [
{ "name": "sheet_mask", "value": "#4D000000" },
{ "name": "sheet_bg", "value": "#FFFFFFFF" },
{ "name": "text_primary", "value": "#FF1A1A2E" }
]
}
// resources/dark/element/color.json
{
"color": [
{ "name": "sheet_mask", "value": "#80000000" },
{ "name": "sheet_bg", "value": "#FF2D2D3F" },
{ "name": "text_primary", "value": "#FFE0E0E0" }
]
}
8. 常见问题与调试指南
8.1 白屏问题
现象:应用启动后只显示白色背景,没有任何 UI 元素。
可能的原因和解决方案:
| 原因 | 排查方法 | 解决方案 |
|---|---|---|
| 页面路径错误 | 检查 EntryAbility 中的 loadContent 路径 |
确保路径 'pages/XXX' 与文件名大小写一致 |
| 使用了废弃 API | 查阅 API 参考文档 | 将 Panel 替换为 bindSheet |
| 运行时异常 | 查看 DevEco Studio 的 Logcat 输出 | 根据错误堆栈修复代码 |
| 未签名部署 | 检查构建日志中的签名警告 | 配置签名证书或使用调试模式 |
调试步骤:
1. 在 DevEco Studio 中打开 Logcat 窗口
2. 过滤关键词 "ERROR" 或 "Crash"
3. 搜索 "Failed to load the content"
4. 检查 hilog 输出中的错误代码
8.2 面板无法打开
现象:点击按钮后面板没有弹出。
排查步骤:
- 检查
isShow是否正确设置为true
// 在 onClick 中添加日志验证
.onClick(() => {
console.info('点击按钮,isShow = ' + this.isShow);
this.isShow = true;
console.info('设置后,isShow = ' + this.isShow);
})
- 检查是否使用了
$$双向绑定
// ❌ 错误:面板关闭后 isShow 无法自动重置
.bindSheet(this.isShow, this.buildPanelContent)
// ✅ 正确:面板关闭后 isShow 自动设为 false
.bindSheet($$this.isShow, this.buildPanelContent)
- 检查面板内容构建器是否存在运行时错误
如果 buildPanelContent() 内部抛出异常,面板将无法渲染。可以在 @Builder 中加上 try-catch 或简化内容来排查:
@Builder
buildPanelContent() {
// 先放一个最简单的文本,确认面板能打开
Text('Test')
.fontSize(20)
}
8.3 拖拽不流畅
现象:拖拽面板时感觉卡顿、掉帧。
优化建议:
- 减少
onHeightDidChange中的计算量
// ❌ 不推荐:在回调中执行耗时操作
onHeightDidChange: (height: number) => {
const result = this.heavyComputation(height); // 耗时操作
this.someState = result;
}
// ✅ 推荐:仅做简单赋值,复杂操作延后处理
onHeightDidChange: (height: number) => {
this.currentHeight = height;
}
// 在 onDetentsDidChange(低频)中执行复杂操作
onDetentsDidChange: (index: number) => {
const result = this.heavyComputation(index);
this.someState = result;
}
- 避免在面板内容中使用大量嵌套组件
// ❌ 不推荐:过多的嵌套层级
Column() {
Row() {
Column() {
Row() {
// ...过深的嵌套
}
}
}
}
// ✅ 推荐:扁平化布局,使用 Flex / Stack 替代深层嵌套
Flex() {
// 扁平结构
}
- 使用懒加载(LazyForEach)替代 ForEach
当面板列表数据量较大时(超过 100 项),使用 LazyForEach 代替 ForEach 可以显著提升性能。
8.4 面板高度异常
现象:面板打开时高度不正确,或拖拽到某个档位后无法正常吸附。
常见原因:
- detents 数组顺序错误:必须从小到大排列
// ❌ 错误:MEDIUM 比 FIT_CONTENT 高
detents: [SheetSize.MEDIUM, SheetSize.FIT_CONTENT]
// ✅ 正确:从小到大
detents: [SheetSize.FIT_CONTENT, SheetSize.MEDIUM]
-
高度值超出屏幕范围:自定义高度不要超过屏幕高度
-
面板内容溢出:如果面板内容尺寸超过了面板高度,可能导致布局异常。确保
@Builder中的内容容器设置了合适的高度约束
8.5 关闭面板时页面闪烁
现象:面板关闭时页面内容出现短暂闪烁。
解决方案:
// 在 onWillDismiss 中添加弹性回弹效果
onWillSpringBackWhenDismiss: {
// 配置弹性回弹参数
}
8.6 键盘弹出遮挡面板
当面板处于打开状态时,如果弹出软键盘,可能会导致面板被键盘遮挡。
API 24 解决方案:
.bindSheet($$this.isShow, this.buildPanelContent, {
keyboardAvoidMode: SheetKeyboardAvoidMode.RESIZE, // 面板自动避让键盘
// 或使用
keyboardAvoidMode: SheetKeyboardAvoidMode.PAN, // 面板整体上移
})
9. 性能优化与内存管理
9.1 状态变量最小化原则
在 ArkTS 中,每次 @State 变量的变化都会触发组件重新渲染。为了提高性能,应遵循"状态变量最小化"原则:
// ❌ 不推荐:作为状态变量存储大量数据
@State largeData: Data[] = this.loadHugeData();
// ✅ 推荐:仅将影响 UI 的数据作为状态变量
@State isShow: boolean = false;
@State currentIndex: number = 0;
// 大数据存储在普通成员变量中,不触发 UI 更新
private largeData: Data[] = this.loadHugeData();
9.2 合理使用 ForEach 与 LazyForEach
当面板列表数据量较少(小于 50 项)时,ForEach 足够胜任。当数据量较大时,应使用 LazyForEach:
// 小数据量:使用 ForEach
ForEach(smallList, (item) => {
ListItem() { /* ... */ }
}, (item) => item.id)
// 大数据量:使用 LazyForEach
LazyForEach(this.dataSource, (item) => {
ListItem() { /* ... */ }
}, (item) => item.id)
LazyForEach 的核心优势是"按需渲染"——只创建当前可见区域内的列表项,不可见的项不创建,从而大幅降低内存占用和渲染开销。
9.3 面板内容的内存管理
每次面板打开时,@Builder 函数会被调用并创建新的组件树。面板关闭后,这些组件树会被销毁。但如果面板内容中引用了外部对象(闭包捕获),可能会导致内存泄漏:
// ❌ 可能的内存泄漏:闭包捕获了大型对象
@Builder
buildPanelContent() {
Column() {
Button('操作')
.onClick(() => {
// 这里捕获了 this,而 this 持有整个页面的引用
this.processLargeData();
})
}
}
// ✅ 更安全:使用局部变量传递必要数据
@Builder
buildPanelContent() {
Column() {
Button('操作')
.onClick(() => {
// 只捕获需要的参数,不依赖 this
this.processItem(this.currentItemId);
})
}
}
9.4 减少不必要的重新渲染
当面板打开时,如果主页面还有其他 @State 变量发生变化,会导致主页面和面板同时重新渲染。可以使用 @ComponentV2 的冻结功能来避免:
// 使用 @ComponentV2 的 freezeWhenInactive 选项
@ComponentV2({ freezeWhenInactive: true })
struct PanelDemo {
// 当面板处于非激活状态时,状态变化不会触发渲染
}
注意:该功能从 API 12 开始支持,仅对使用
@ComponentV2装饰的组件生效,对传统的@Component不生效。
10. 从 Panel 迁移到 bindSheet 的完整指南
10.1 API 对照表
对于正在使用 Panel 组件的存量项目,以下是 API 迁移的完整对照表:
| Panel API | 状态 | bindSheet 替代方案 |
|---|---|---|
Panel(show: boolean) |
构造参数 | bindSheet(isShow, builder) |
.mode(PanelMode.Half) |
模式控制 | detents: [..., SheetSize.MEDIUM, ...] |
.dragBar(true) |
拖拽条 | dragBar: true(SheetOptions 内) |
.show(value: boolean) |
展示控制 | $$ 双向绑定 |
.onChange(width, height, mode) |
变化回调 | onHeightDidChange + onDetentsDidChange |
.miniHeight(value) |
迷你高度 | detents 第一个元素设为具体值 |
.halfHeight(value) |
半屏高度 | detents[1] = 具体值 |
.fullHeight(value) |
全屏高度 | detents[2] = SheetSize.LARGE |
.backgroundColor() |
背景色 | 在 @Builder 内容中设置 |
.borderRadius() |
圆角 | borderRadius(SheetOptions 内) |
.shadow() |
阴影 | shadow(SheetOptions 内) |
.transition() |
过渡动画 | 系统内置动画,无需额外配置 |
10.2 迁移步骤
以下是将 Panel 代码迁移到 bindSheet 的步骤模板:
步骤 1:替换构造方式
// 迁移前
Panel(this.show) {
// 面板内容
}
// 迁移后
// (将 bindSheet 挂载到触发按钮上)
Button('打开')
.bindSheet($$this.show, this.buildContent, { ... })
步骤 2:转换模式配置
// 迁移前
.mode(PanelMode.Half)
.miniHeight('80px')
.halfHeight('400px')
.fullHeight('700px')
// 迁移后
detents: [
80, // 对应 miniHeight
400, // 对应 halfHeight
SheetSize.LARGE // 对应 fullHeight
]
步骤 3:转换事件监听
// 迁移前
.onChange((width, height, mode) => {
// 处理面板变化
})
// 迁移后
onHeightDidChange: (height) => {
// 处理高度变化
},
onDetentsDidChange: (index) => {
// 处理档位切换
}
步骤 4:提取面板内容为 @Builder
// 迁移前:面板内容直接写在 Panel 闭包内
Panel(this.show) {
Column() {
// 内容
}
}
// 迁移后:提取为 @Builder 方法
@Builder
buildContent() {
Column() {
// 内容
}
}
步骤 5:删除废弃的 API 调用
// 以下 API 在 bindSheet 中不再需要
// .type(PanelType.Custom) → 废弃
// .transition(...) → 由框架自动处理
// .onAnimationStart(...) → 不需要显式调用
// .onAnimationEnd(...) → 不需要显式调用
10.3 迁移检查清单
- 所有
Panel组件已被替换为bindSheet - 面板内容已提取为
@Builder方法 - 使用
$$双向绑定控制面板状态 -
detents配置正确(从小到大排列) -
dragBar已配置 - 面板关闭逻辑正常(所有关闭方式均可工作)
- 迁移后运行测试通过
10.4 迁移后测试要点
迁移完成后,建议进行以下测试:
- 功能测试:面板能否正常打开/关闭/拖拽
- 边界测试:快速连续点击、快速拖拽、在档位之间高速切换
- 兼容测试:在不同屏幕尺寸的设备上测试面板高度表现
- 性能测试:面板打开/关闭的动画是否流畅
- 回归测试:其他依赖 Panel 状态的页面功能是否受影响
11. 总结与展望
11.1 本文要点回顾
通过这篇文章,我们完成了一次完整的鸿蒙原生可拖拽半模态面板开发之旅。以下是核心要点:
- 认识变迁:
Panel组件自 API 12 起已废弃,bindSheet是官方推荐的替代方案 - 掌握 API:
bindSheet(isShow, builder, options)是核心调用,三个参数各有其责 - 理解双向绑定:
$$语法确保面板状态与 UI 状态始终同步,避免状态不一致的 bug - 灵活配置:
detents数组支持SheetSize枚举和具体数值混合使用,实现三档高度 - 精细控制:
dragBar、showClose、maskColor、borderRadius等选项提供了丰富的视觉定制能力 - 响应式监听:
onHeightDidChange、onDetentsDidChange、onWillDismiss等回调构建了完整的事件响应体系 - 性能意识:高频回调避免复杂计算,大数据列表使用
LazyForEach,状态变量最小化 - 平滑迁移:Panel 到 bindSheet 的迁移路径清晰,API 对应关系明确
11.2 bindSheet 的未来演进
随着鸿蒙系统的持续迭代,bindSheet 也在不断进化。展望未来,我们可以期待以下方向的发展:
- 更丰富的动画控制:自定义进入/退出动画曲线
- 多面板同时展示:支持分屏场景下多个半模态面板共存
- 更智能的键盘避让:结合输入法场景的深度优化
- 跨设备协同:在平板、折叠屏等大屏设备上的自适应布局
- 无障碍优化:对屏幕阅读器、语音控制等辅助功能的增强支持
11.3 进一步学习推荐
如果您希望继续深入学习鸿蒙原生 UI 开发,以下方向值得探索:
| 学习方向 | 核心组件/API | 推荐场景 |
|---|---|---|
| 导航与路由 | @Navigation, @NavDestination |
多页面应用 |
| 弹窗体系 | AlertDialog, CustomDialog, bindPopup |
各类弹窗需求 |
| 动画系统 | animateTo, animation, transition |
交互动效 |
| 手势处理 | Gesture, PanGesture, SwipeGesture |
自定义手势 |
| 自定义布局 | LazyLayout, WaterFlow |
复杂布局 |
| 状态管理进阶 | @Provide/@Consume, @Local, @ComponentV2 |
大型应用架构 |
11.4 写在最后
bindSheet 作为鸿蒙原生半模态转场的标准方案,兼具声明式的简洁性和灵活的配置能力。从 Panel 到 bindSheet 的迁移,不仅仅是 API 的替换,更是对鸿蒙 UI 开发理念的深入理解——声明式、响应式、组件化。
希望这篇文章能帮助您在鸿蒙 NEXT 开发之路上少走一些弯路。技术的价值在于分享,知识的深度在于实践——拿起键盘,打开 DevEco Studio,把本文的代码跑起来,然后尝试扩展它、改变它,最终超越它。
Happy Coding!🛠️
12. 附录:完整源代码
12.1 Index.ets(完整版)
以下为完整可运行的源代码。将以下内容直接复制到
entry/src/main/ets/pages/Index.ets即可运行。
/**
* Index.ets — 鸿蒙原生 ArkTS 可拖拽面板(bindSheet 半模态转场)入门示例
*
* 本示例使用 bindSheet(半模态转场,Panel 组件的官方替代方案)
* 实现「底部上滑面板」效果,支持三档高度拖拽切换。
*
* 目标平台:HarmonyOS NEXT (SDK 6.1.0(24) / API 24)
*
* 核心技术点:
* 1) bindSheet — 半模态页面绑定
* 2) SheetOptions — 面板配置(高度档位、拖拽条、遮罩)
* 3) SheetSize — MEDIUM / LARGE 标准高度
* 4) $$ 双向绑定 — 面板状态自动同步
* 5) @Builder — 构建面板内容
*/
@Entry
@Component
struct Index {
/** 控制半模态面板的显示/隐藏 */
@State isShow: boolean = false;
build() {
Column() {
// ----- 顶部标题 -----
Text('⚙️ 可拖拽面板演示')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A2E')
.width('100%')
.textAlign(TextAlign.Start)
.margin({ top: 56, bottom: 8 })
.padding({ left: 24 })
// ----- 说明文字 -----
Text('使用 bindSheet 半模态转场实现底部上滑面板')
.fontSize(14)
.fontColor('#666666')
.width('100%')
.textAlign(TextAlign.Start)
.padding({ left: 24 })
// ----- 说明卡片 -----
Column() {
Text('📖 使用说明')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A2E')
.width('100%')
Text('点击下方按钮打开底部面板 → 拖拽面板上边缘或拖拽条上滑切换模式。')
.fontSize(14)
.fontColor('#555555')
.lineHeight(22)
.margin({ top: 8 })
Text('面板支持三种高度:自适应内容高度 / MEDIUM 半屏 / LARGE 全屏。')
.fontSize(14)
.fontColor('#555555')
.lineHeight(22)
.margin({ top: 4 })
}
.width('90%')
.padding(16)
.margin({ top: 24 })
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 8, color: 'rgba(0,0,0,0.08)', offsetX: 0, offsetY: 4 })
// ----- 控制按钮(绑定半模态面板)-----
Button('🔼 打开控制面板')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor(Color.White)
.width('80%')
.height(50)
.backgroundColor('#6C63FF')
.borderRadius(25)
.margin({ top: 32 })
.shadow({ radius: 12, color: 'rgba(108,99,255,0.4)', offsetX: 0, offsetY: 6 })
.onClick(() => {
this.isShow = true;
})
// bindSheet: 绑定半模态面板
.bindSheet($$this.isShow, this.buildPanelContent, {
// 高度档位数组(三档:自定义250 / MEDIUM半屏 / LARGE全屏)
detents: [
250,
SheetSize.MEDIUM,
SheetSize.LARGE
],
// 显示拖拽条
dragBar: true,
// 显示关闭按钮
showClose: true,
// 遮罩颜色
maskColor: 'rgba(0, 0, 0, 0.3)',
// 面板高度变化回调
onHeightDidChange: (height: number) => {
console.info('[PanelDemo] 面板高度变化: ' + height);
},
// 面板档位变化回调
onDetentsDidChange: (index: number) => {
console.info('[PanelDemo] 面板档位切换: ' + index);
}
})
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
/**
* 构建半模态面板的内容
* @Builder 装饰器 — 用于 bindSheet 的 builder 参数
*/
@Builder
buildPanelContent() {
Column() {
// 面板标题
Text('控制中心')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A2E')
.margin({ top: 12, bottom: 16 })
// 功能介绍文字
Text('拖拽面板上边缘或拖拽条切换三档高度')
.fontSize(13)
.fontColor('#888888')
.margin({ bottom: 20 })
// 功能列表
List({ space: 10 }) {
ForEach([
'📱 屏幕亮度',
'🔊 媒体音量',
'📶 Wi-Fi 设置',
'🔄 蓝牙连接',
'🔦 手电筒'
], (item: string) => {
ListItem() {
Text(item)
.fontSize(16)
.fontColor('#333333')
.width('100%')
.height(52)
.padding({ left: 16 })
.backgroundColor('#F8F8FC')
.borderRadius(12)
}
}, (item: string) => item)
}
.width('100%')
.height('50%')
.margin({ bottom: 12 })
}
.width('100%')
.height('100%')
.padding({ left: 20, right: 20, bottom: 20 })
}
}
12.2 EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
console.error('Failed to load page:', JSON.stringify(err));
return;
}
console.info('Page loaded successfully.');
});
}
}
12.3 build-profile.json5(项目级)
{
"app": {
"signingConfigs": [],
"products": [
{
"name": "default",
"signingConfig": "default",
"targetSdkVersion": "6.1.0(24)",
"compatibleSdkVersion": "6.1.0(24)",
"runtimeOS": "HarmonyOS",
"buildOption": {
"strictMode": {
"caseSensitiveCheck": true,
"useNormalizedOHMUrl": true
}
}
}
],
"buildModeSet": [
{ "name": "debug" },
{ "name": "release" }
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{ "name": "default", "applyToProducts": ["default"] }
]
}
]
}
更多推荐


所有评论(0)