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


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

目录

  1. 前言:为什么你需要读这篇文章
  2. 背景:从 Panel 到 bindSheet 的演进
  3. 项目初始化与工程结构
  4. bindSheet 核心 API 详解
  5. 实战:构建一个完整的可拖拽控制面板
  6. 代码深度解读
  7. 高级技巧与最佳实践
  8. 常见问题与调试指南
  9. 性能优化与内存管理
  10. 从 Panel 迁移到 bindSheet 的完整指南
  11. 总结与展望
  12. 附录:完整源代码

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.MiniPanelMode.HalfPanelMode.Full 三档模式,配合 dragBar 拖拽条和 onChange 事件,能够快速构建一个可拖拽的底部面板。

然而,随着鸿蒙系统的快速迭代和 API 的不断演进,Panel 组件逐渐暴露出一些设计上的局限:

  1. 布局受限:Panel 必须作为独立的层级叠加在页面之上,无法灵活地嵌入到复杂的布局结构中
  2. 样式定制能力有限:圆角、阴影、遮罩效果的控制不够精细化
  3. 动画控制不够灵活:内置的过渡动画难以自定义
  4. 与其他转场机制重叠:鸿蒙后续引入了更完善的半模态转场体系,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 中,$$ 已支持 bindSheetbindPopupbindMenubindContextMenu 等多种场景的显隐控制。

4.5 @Builder 装饰器详解

@Builder 是 ArkTS 中用于构建 UI 片段的装饰器。在 bindSheet 中,它用于构建面板的内容:

@Builder
buildPanelContent() {
  Column() {
    Text('面板标题')
      .fontSize(20)
      .fontWeight(FontWeight.Bold)
    
    // 更多内容...
  }
  .width('100%')
  .height('100%')
  .padding(20)
}

关键要点:

  1. 无参数:作为 bindSheet 参数的 @Builder 方法不能包含参数
  2. 位置:必须定义在 @Component@Entry 装饰的 struct 内部
  3. 调用:在 bindSheet 中通过 this.buildPanelContent(不加括号)传入
  4. 上下文@Builder 内部的 this 指向所属 struct 的实例,可以访问 @State 变量

5. 实战:构建一个完整的可拖拽控制面板

5.1 需求分析

我们将构建一个"控制中心"风格的可拖拽底部面板,它需要满足以下功能需求:

  1. 从底部弹出:点击按钮后,面板从屏幕底部以动画方式弹出
  2. 三档高度切换:通过拖拽面板上边缘或拖拽条,在三个高度档位间切换
  3. 功能列表展示:面板内包含一个功能列表,列表项支持点击交互
  4. 状态实时反馈:面板的高度和档位变化能在主页面实时反映
  5. 多种关闭方式:支持点击遮罩、点击关闭按钮、下滑手势三种关闭方式
  6. 视觉精致:圆角顶部、半透明遮罩、阴影效果、拖拽条引导

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 运行效果预览

当您将上述代码部署到真机或模拟器后,会看到以下交互效果:

  1. 初始状态:页面顶部显示标题「⚙️ 可拖拽面板演示」,中间有说明卡片,底部有一个紫色的「🔼 打开控制面板」按钮
  2. 点击按钮:面板从底部平滑弹出,半透明遮罩覆盖背景,面板初始高度为 250vp
  3. 拖拽操作
    • 向上拖拽面板上边缘 → 面板切换至 MEDIUM 半屏高度
    • 继续向上拖拽 → 面板切换至 LARGE 全屏高度
    • 向下拖拽 → 面板逐步回落至之前的高度档位
  4. 关闭操作
    • 点击遮罩区域 → 面板关闭
    • 点击面板右上角的关闭按钮(×)→ 面板关闭
    • 向下拖拽到底 → 面板关闭
  5. 状态回调:拖拽过程中,控制台会输出面板高度和档位变化日志

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%屏幕高度)
]

为什么这样设置?

  1. 第一档用具体数值(250vp):一个接近"迷你"模式的紧凑高度,适合内容较少的场景。250vp 大约能展示 2~3 个列表项,配合拖拽条暗示用户"这里可以拖拽"。

  2. 第二档用 MEDIUM:这是最常见的半屏模式,覆盖屏幕下方约 50% 的区域,既能展示足够多的内容,又不会遮挡过多背景。

  3. 第三档用 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%')

Listheight('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 面板无法打开

现象:点击按钮后面板没有弹出。

排查步骤:

  1. 检查 isShow 是否正确设置为 true
// 在 onClick 中添加日志验证
.onClick(() => {
  console.info('点击按钮,isShow = ' + this.isShow);
  this.isShow = true;
  console.info('设置后,isShow = ' + this.isShow);
})
  1. 检查是否使用了 $$ 双向绑定
// ❌ 错误:面板关闭后 isShow 无法自动重置
.bindSheet(this.isShow, this.buildPanelContent)

// ✅ 正确:面板关闭后 isShow 自动设为 false
.bindSheet($$this.isShow, this.buildPanelContent)
  1. 检查面板内容构建器是否存在运行时错误

如果 buildPanelContent() 内部抛出异常,面板将无法渲染。可以在 @Builder 中加上 try-catch 或简化内容来排查:

@Builder
buildPanelContent() {
  // 先放一个最简单的文本,确认面板能打开
  Text('Test')
    .fontSize(20)
}

8.3 拖拽不流畅

现象:拖拽面板时感觉卡顿、掉帧。

优化建议:

  1. 减少 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;
}
  1. 避免在面板内容中使用大量嵌套组件
// ❌ 不推荐:过多的嵌套层级
Column() {
  Row() {
    Column() {
      Row() {
        // ...过深的嵌套
      }
    }
  }
}

// ✅ 推荐:扁平化布局,使用 Flex / Stack 替代深层嵌套
Flex() {
  // 扁平结构
}
  1. 使用懒加载(LazyForEach)替代 ForEach

当面板列表数据量较大时(超过 100 项),使用 LazyForEach 代替 ForEach 可以显著提升性能。

8.4 面板高度异常

现象:面板打开时高度不正确,或拖拽到某个档位后无法正常吸附。

常见原因:

  1. detents 数组顺序错误:必须从小到大排列
// ❌ 错误:MEDIUM 比 FIT_CONTENT 高
detents: [SheetSize.MEDIUM, SheetSize.FIT_CONTENT]

// ✅ 正确:从小到大
detents: [SheetSize.FIT_CONTENT, SheetSize.MEDIUM]
  1. 高度值超出屏幕范围:自定义高度不要超过屏幕高度

  2. 面板内容溢出:如果面板内容尺寸超过了面板高度,可能导致布局异常。确保 @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 迁移后测试要点

迁移完成后,建议进行以下测试:

  1. 功能测试:面板能否正常打开/关闭/拖拽
  2. 边界测试:快速连续点击、快速拖拽、在档位之间高速切换
  3. 兼容测试:在不同屏幕尺寸的设备上测试面板高度表现
  4. 性能测试:面板打开/关闭的动画是否流畅
  5. 回归测试:其他依赖 Panel 状态的页面功能是否受影响

11. 总结与展望

11.1 本文要点回顾

通过这篇文章,我们完成了一次完整的鸿蒙原生可拖拽半模态面板开发之旅。以下是核心要点:

  1. 认识变迁Panel 组件自 API 12 起已废弃,bindSheet 是官方推荐的替代方案
  2. 掌握 APIbindSheet(isShow, builder, options) 是核心调用,三个参数各有其责
  3. 理解双向绑定$$ 语法确保面板状态与 UI 状态始终同步,避免状态不一致的 bug
  4. 灵活配置detents 数组支持 SheetSize 枚举和具体数值混合使用,实现三档高度
  5. 精细控制dragBarshowClosemaskColorborderRadius 等选项提供了丰富的视觉定制能力
  6. 响应式监听onHeightDidChangeonDetentsDidChangeonWillDismiss 等回调构建了完整的事件响应体系
  7. 性能意识:高频回调避免复杂计算,大数据列表使用 LazyForEach,状态变量最小化
  8. 平滑迁移: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"] }
      ]
    }
  ]
}

Logo

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

更多推荐