请添加图片描述
![请添加图片描述](https://i-blog.csdnimg.cn/direct/251528afd7d9401896bb164b2751f32c.p](https://i-blog.csdnimg.cn/direct/240705f859a841bbaa1c791dbd78c197.png)

1. 引言:从 Panel 到 bindSheet 的进化之路

在移动应用开发中,"从底部弹出的面板"是一种不可或缺的交互范式。无论是分享内容、选择操作,还是配置设置,底部弹窗都提供了比跳转页面更轻量、比对话框更灵活的解决方案。

在 HarmonyOS 生态中,ArkUI 框架对底部弹窗的支持经历了两个阶段:

第一阶段:Panel 组件(API 7 - API 23)

Panel 是 ArkUI 早期的底部面板解决方案。它作为一个独立的容器组件,通过 Panel(show: boolean) 构造函数控制显隐,配合 .mode(PanelMode.Half).dragBar(true) 等属性链实现底部面板功能。在 API 7 到 API 23 的漫长时期里,Panel 是唯一的选择。

但 Panel 存在几个设计上的局限:

  • 必须作为组件树的一部分:Panel 需要显式地放在组件树的某个位置(通常放在 Stack 中)
  • 遮罩层 API 不稳定:不同 SDK 版本中 mask / maskColor 的存在与否不一致
  • 构造参数类型随 SDK 变化:从 PanelMode 变为 boolean,导致跨版本兼容性问题

第二阶段:bindSheet 通用属性(API 12+)

bindSheet 是 ArkUI 在 API 12 引入的新方案。它不是独立的组件,而是一个通用属性——可以绑定到任意组件上。这意味着任何组件(Button、Text、Image、甚至一个 Column)都可以成为 Sheet 的"触发器"。

bindSheet 相比 Panel 的核心优势:

维度 Panel 组件 bindSheet 属性
使用方式 独立组件,需放在组件树中 通用属性,绑定到任意组件
显隐控制 通过构造函数或 .show() $$ 双向绑定 @State 变量
高度模式 PanelMode (Mini/Half/Full) SheetSize (MEDIUM/LARGE) + detents
层级控制 固定在内容之上 SheetMode (OVERLAY/EMBEDDED)
生命周期 onAppear / onDisAppear onAppear / onDisappear
滚动控制 ScrollSizeMode
多级高度 不支持 detents 数组支持多个高度档位
当前状态 API 12 起已弃用 推荐的替代方案

本文将通过一个完整的"Sheet 底部弹窗三合一"演示应用,系统讲解 bindSheet 的完整用法——从 $$ 双向绑定到 detents 多级高度,从 Grid 网格布局到三个 Sheet 的独立状态管理。


2. 项目全景——Sheet 底部弹窗三合一演示

2.1 需求描述

构建一个演示页面,包含三个不同类型的底部 Sheet:

  1. 分享面板(MEDIUM 高度):以 4×2 网格展示 8 个分享目标(微信、朋友圈、QQ、短信、邮件等),点击后关闭 Sheet 并显示 Toast 反馈
  2. 操作菜单(MEDIUM 高度):列出 6 个操作项(编辑、收藏、下载、刷新、举报、删除),点击后执行对应操作并关闭 Sheet
  3. 设置面板(LARGE 高度):包含三组设置项(通知设置、隐私设置、其他),展示 Switch 开关和点击列表项的交互

同时,页面顶部提供操作反馈区域,实时显示最近的操作、分享次数等信息。

2.2 页面布局架构

Column(根容器)
 ├── Row(顶部标题栏 "📋 Sheet 底部弹窗演示")
 │
 └── Scroll(主内容区,layoutWeight:1)
      └── Column
           ├── buildIntroSection()     — 功能介绍卡片
           ├── buildSheetTriggers()    — 三个 Sheet 触发按钮
           │    ├── Button (📤 分享面板)  → bindSheet → shareSheetBuilder()
           │    ├── Button (⚙️ 操作菜单) → bindSheet → actionSheetBuilder()
           │    └── Button (🔧 设置面板) → bindSheet → settingsSheetBuilder()
           ├── buildFeedbackSection()  — 操作反馈展示区
           ├── buildFeatureSection()   — bindSheet 特性速览
           └── Blank(底部留白)

2.3 三个 Sheet 的配置对比

属性 分享面板 操作菜单 设置面板
Mode OVERLAY OVERLAY OVERLAY
Type BOTTOM BOTTOM BOTTOM
detents [MEDIUM] [MEDIUM] [LARGE]
dragBar true true true
maskColor ‘#33000000’ ‘#33000000’ ‘#44000000’
scrollSizeMode FOLLOW_DETENT
onAppear 记录日志 记录日志
onDisappear 记录日志 记录日志
高度 中等(约 50%) 中等(约 50%) 较大(约 75%)

3. bindSheet 通用属性——ArkUI 声明式弹窗的核心 API

3.1 语法

.bindSheet(isShow: $$boolean, builder: CustomBuilder, options?: SheetOptions)

三个参数:

参数 类型 必填 说明
isShow $$boolean 使用 $$ 双向绑定语法的 @State 变量,控制 Sheet 显隐
builder CustomBuilder @Builder 方法引用,定义 Sheet 内容
options SheetOptions Sheet 的配置选项对象

3.2 最简单的示例

@State isOpen: boolean = false;

Button('打开 Sheet')
  .bindSheet($$this.isOpen, () => {
    this.mySheetBuilder();
  }, {
    mode: SheetMode.OVERLAY,
    preferType: SheetType.BOTTOM,
    detents: [SheetSize.MEDIUM],
    dragBar: true,
  })

3.3 核心 SheetOptions 属性

interface SheetOptions {
  mode?: SheetMode;                    // 显示层级:OVERLAY / EMBEDDED
  preferType?: SheetType;              // 弹出方向:BOTTOM / CENTER
  detents?: [SheetSize?, SheetSize?, SheetSize?]; // 高度档位数组
  dragBar?: boolean;                   // 是否显示拖拽条
  maskColor?: ResourceColor;           // 遮罩颜色(设置即显示遮罩)
  blurStyle?: BlurStyle;               // 背景模糊效果
  title?: SheetTitleOptions | CustomBuilder; // 标题
  showClose?: boolean | Resource;      // 是否显示关闭按钮
  scrollSizeMode?: ScrollSizeMode;     // 滚动跟随模式
  backgroundColor?: ResourceColor;     // 背景色
  borderWidth?: EdgeWidths;            // 边框宽度
  borderColor?: ResourceColor;         // 边框颜色
  borderStyle?: BorderStyle;           // 边框样式
  width?: Dimension;                   // 宽度
  height?: Dimension;                  // 高度
  shadow?: ShadowOptions | ShadowStyle; // 阴影
  uiContext?: UIContext;               // UI 上下文
  onAppear?: () => void;              // 出现回调
  onDisappear?: () => void;           // 消失回调
  shouldDismiss?: (dismiss: SheetDismiss) => void; // 关闭前拦截
  onWillDismiss?: (action: DismissSheetAction) => void; // 关闭决策
  // 更多回调...
}

3.4 bindSheet 与 Panel 的核心差异

// Panel 方式(已弃用):独立组件,放在组件树中
Stack() {
  Column() { /* 主内容 */ }
  Panel(this.show) { /* 面板内容 */ }
    .mode(this.mode)
    .dragBar(true)
}

// bindSheet 方式:作为 Button 的属性
Button('打开')
  .bindSheet($$this.show, () => {
    this.sheetContentBuilder();
  }, {
    detents: [SheetSize.MEDIUM],
    dragBar: true,
  })

bindSheet 的优势在于:

  1. 不需要手动管理组件层级:Sheet 自动浮动在 UI 顶层
  2. 不需要额外的 Stack 包裹:绑定在触发按钮上即可
  3. 不需要手动处理遮罩:设置 maskColor 即可显示遮罩

4. $$ 双向绑定——@State 与 Sheet 显隐的自动同步

4.1 基本语法

@State shareSheetShow: boolean = false;

Button()
  .bindSheet($$this.shareSheetShow, () => { ... })

$$ 是 ArkUI 的双向绑定语法。将 $$this.shareSheetShow 传递给 bindSheet 后,Sheet 的显隐状态与 @State shareSheetShow 实现了自动双向同步

shareSheetShow = true   →   Sheet 显示
shareSheetShow = false  →   Sheet 关闭
Sheet 被拖拽关闭        →   shareSheetShow 自动变为 false
Sheet 被代码关闭        →   shareSheetShow 自动变为 false

4.2 通过代码控制 Sheet 关闭

// 在 Sheet 内部的某个操作中关闭 Sheet
GridItem() {
  Column() {
    Text('微信')
  }
  .onClick(() => {
    this.shareCount++;
    this.shareSheetShow = false;  // 设置为 false → Sheet 自动关闭
  })
}

只需要给 @State 变量赋值为 false,Sheet 会自动播放退场动画并关闭。不需要调用任何 “close()” 方法。

4.3 点击遮罩层关闭

当设置了 maskColor 后,用户点击遮罩层(Sheet 外部的半透明区域)Sheet 会自动关闭,同时 @State 变量自动变为 false。这是 $$ 双向绑定的另一体现——用户操作导致的关闭也会同步更新状态变量。

4.4 $$ 双向绑定的使用条件

$$ 语法只能用于以下场景:

  1. bindSheet 的 isShow 参数$$this.sheetShow
  2. bindContentCover 的 isShow 参数$$this.coverShow
  3. bindMenu 的 isShow 参数$$this.menuShow
  4. TextInput / TextArea 的 text 参数$$this.inputText
  5. Toggle 的 isOn 参数$$this.isOn

在所有这些场景中,$$ 都用于建立一个"框架组件 ↔ @State 变量"之间的自动同步通道。


5. SheetMode 层级控制——OVERLAY 与 EMBEDDED 的区别

5.1 SheetMode 枚举

enum SheetMode {
  OVERLAY  = 0,  // 在当前 UIContext 顶层显示,在所有页面之上
  EMBEDDED = 1,  // 在当前页面内顶层显示,在 Page / NavDestination 之上
}

5.2 OVERLAY 模式(默认值)

.bindSheet($$this.show, builder, {
  mode: SheetMode.OVERLAY,  // 默认值
})

OVERLAY 模式下,Sheet 显示在当前 Window 的顶层,位于所有页面之上,和 Dialog 处于同一层级。这意味着:

  • Sheet 会覆盖当前页面的所有内容
  • 即使页面内嵌了 Navigation 或 Router,Sheet 仍然在最上层
  • 适用于"全局性"的弹窗,如分享面板、全局操作菜单

5.3 EMBEDDED 模式

.bindSheet($$this.show, builder, {
  mode: SheetMode.EMBEDDED,
})

EMBEDDED 模式下,Sheet 显示在当前 Page 或 NavDestination 的顶层,但位于页面层级之内。这意味着:

  • Sheet 不会覆盖 NavigationBar 或其他页面级别的 UI
  • 导航到新页面时,Sheet 会被新页面覆盖
  • 返回上一页时,Sheet 保持原来的状态
  • 适用于"页面级"的弹窗,如评论详情、商品规格选择

5.4 动态切换的限制

SheetMode 不支持在 Sheet 显示期间动态切换。 这意味着你不能先以 OVERLAY 模式打开 Sheet,然后在不关闭的情况下切换为 EMBEDDED 模式。

// 错误:显示期间不能切换 mode
.bindSheet($$this.show, builder, {
  mode: this.isGlobal ? SheetMode.OVERLAY : SheetMode.EMBEDDED,  // ×
})

正确的做法是在创建时固定 mode 值:

// 正确:mode 在创建时固定
.bindSheet($$this.show, builder, {
  mode: SheetMode.OVERLAY,  // 固定为 OVERLAY
})

6. SheetSize 与 detents——多级高度档位的灵活控制

6.1 SheetSize 枚举

enum SheetSize {
  MEDIUM = 0,  // 中等高度,约屏幕高度的 50%
  LARGE  = 1,  // 较大高度,约屏幕高度的 75%  
}

6.2 detents 数组

detents 是 bindSheet 中控制 Sheet 高度的核心属性。它接受一个最多三个元素的数组,定义 Sheet 可以停留的高度档位:

// 单档位:Sheet 只能在 MEDIUM 高度
detents: [SheetSize.MEDIUM]

// 双档位:Sheet 可以在 MEDIUM 和 LARGE 之间切换
detents: [SheetSize.MEDIUM, SheetSize.LARGE]

// 混合档位:结合枚举和具体数值
detents: [SheetSize.MEDIUM, 600]

在 SheetDemo 中:

  • 分享面板和操作菜单使用 [SheetSize.MEDIUM](单档位,约 50% 高度)
  • 设置面板使用 [SheetSize.LARGE](单档位,约 75% 高度)

6.3 自定义高度

除了 SheetSize 枚举,detents 也支持传入具体的数值(单位 vp):

// 自定义 400vp 高度
detents: [400]

// 混合:MEDIUM 或 600vp
detents: [SheetSize.MEDIUM, 600]

// 三档位
detents: [SheetSize.MEDIUM, SheetSize.LARGE, 700]

6.4 多档位的用户交互

当 detents 传入两个或更多元素时,用户可以通过拖拽 dragBar 在不同的高度档位之间切换。每次切换到新的档位时,Sheet 会通过弹性动画吸附到对应的高度。

如果只传入一个元素,Sheet 只能固定在该高度,用户无法通过拖拽改变高度——这适用于内容量固定、不需要高度切换的场景。


7. preferType——底部弹出与居中弹出的选择

7.1 SheetType 枚举

enum SheetType {
  BOTTOM = 0,  // 从屏幕底部弹出(默认)
  CENTER = 1,  // 从屏幕中央弹出
}

7.2 BOTTOM 类型

.bindSheet($$this.show, builder, {
  preferType: SheetType.BOTTOM,
})

BOTTOM 是默认的 Sheet 类型。Sheet 从屏幕底部滑入,停在底部位置。这是最常见的底部弹窗模式,适用于分享面板、操作菜单、评论区域等场景。

7.3 CENTER 类型

.bindSheet($$this.show, builder, {
  preferType: SheetType.CENTER,
})

CENTER 类型使 Sheet 从屏幕中央弹出,类似于 Dialog 的显示方式。但 Sheet 仍然保留了 dragBar、遮罩层等 Sheet 特性。

7.4 两种类型的对比

特性 BOTTOM CENTER
弹出位置 底部 中央
入场动画 从底部滑入 从中央淡入
默认高度 MEDIUM 或 LARGE 根据内容自适应
适用场景 分享/操作/评论 提示/确认/选择

8. Grid + GridItem——分享面板的网格布局实现

8.1 Grid 组件

Grid 是 ArkUI 提供的网格布局容器,通过 columnsTemplaterowsTemplate 定义行列数量:

Grid() {
  // 子组件必须为 GridItem
}
.columnsTemplate('1fr 1fr 1fr 1fr')  // 4 列等宽
.rowsTemplate('1fr 1fr')              // 2 行等高
.width('100%')
.height(200)

columnsTemplate 的语法与 CSS Grid 的 grid-template-columns 类似:

  • '1fr 1fr 1fr 1fr':4 列,每列等宽
  • '1fr 2fr 1fr':3 列,中间列是两倍宽
  • '100px 1fr':2 列,第一列 100px,第二列自适应

8.2 GridItem 的必要性

Grid 的直接子组件必须是 GridItem。 这是 ArkTS 的编译时约束——如果你在 Grid 中直接使用 Column 或 Row,构建会报错:

ERROR: The component 'Grid' can only have the child component 'GridItem'.

正确的写法:

Grid() {
  ForEach(this.shareItems, (item: ShareItem, index: number) => {
    GridItem() {               // ← 必须使用 GridItem 包裹
      Column() {
        Text(item.icon).fontSize(32)
        Text(item.name).fontSize(13)
      }
      .onClick(() => { /* 点击事件 */ })
    }
  })
}

8.3 aspectRatio——保持网格项为正方形

GridItem() {
  Column() { /* 图标 + 文字 */ }
    .aspectRatio(1)  // 宽高比 1:1,确保网格项为正方形
}

.aspectRatio(1) 强制 Column 的宽高比为 1:1,即使 Grid 的行高和列宽设置不一致,GridItem 也会保持正方形外观。


9. @Builder 组件架构——八大 Builder 方法的设计与职责

9.1 SheetDemo 中的全部 @Builder

方法名 所属模块 用途 参数
buildIntroSection() 主页面 功能介绍卡片
buildSheetTriggers() 主页面 三个 Sheet 触发按钮
buildFeedbackSection() 主页面 操作反馈展示区
buildFeatureSection() 主页面 特性速览列表
buildFeatureRow(icon, title, desc) 主页面 特性列表的每一行 icon, title, desc
shareSheetBuilder() Sheet 内容 分享面板网格布局
actionSheetBuilder() Sheet 内容 操作菜单列表
buildActionItem(icon, title, desc, color, onClick) 操作菜单 操作菜单的每一行 icon, title, desc, color, onClick
settingsSheetBuilder() Sheet 内容 设置面板完整布局
buildSettingsGroupHeader(icon, title) 设置面板 设置分组标题 icon, title
buildSettingsToggle(title, desc, defaultOn) 设置面板 设置开关项 title, desc, defaultOn
buildSettingsItem(title, desc) 设置面板 设置点击项 title, desc

9.2 三级 Builder 嵌套结构

主页面 Builder(3 个)
 ├── buildIntroSection()         — 最上层,无子 Builder
 ├── buildSheetTriggers()        — 中层,内部包含三个 bindSheet 绑定
 │    ├── shareSheetBuilder()    — Sheet 内容,最底层
 │    ├── actionSheetBuilder()   — Sheet 内容,调用 buildActionItem
 │    └── settingsSheetBuilder() — Sheet 内容,调用 buildSettings*
 └── buildFeedbackSection()      — 最上层,无子 Builder

功能列表 Builder(1 个)
 └── buildFeatureSection()
      └── buildFeatureRow() (×7) — 复用 7 次

操作菜单 Builder(1+1 个)
 ├── actionSheetBuilder()
 └── buildActionItem() (×6)     — 复用 6 次

设置面板 Builder(1+3 个)
 ├── settingsSheetBuilder()
 ├── buildSettingsGroupHeader() (×3)  — 复用 3 次
 ├── buildSettingsToggle() (×6)       — 复用 6 次
 └── buildSettingsItem() (×3)         — 复用 3 次

9.3 @Builder 复用示例:buildFeatureRow

@Builder
buildFeatureRow(icon: string, title: string, desc: string): void {
  Row() {
    Text(icon).fontSize(18).margin({ right: 10 })
    Column() {
      Text(title).fontSize(14).fontWeight(FontWeight.Medium).fontColor('#2C3E50')
        .width('100%')
      Text(desc).fontSize(12).fontColor('#888888')
        .width('100%').margin({ top: 2 })
    }
    .layoutWeight(1)
  }
  .width('100%')
  .padding({ top: 8, bottom: 8 })
  .border({ width: { bottom: 1 }, color: '#F0F0F0', style: BorderStyle.Solid })
}

被调用 7 次:

this.buildFeatureRow('📐', 'detents: [MEDIUM, LARGE]', '多级高度控制...')
this.buildFeatureRow('🎯', '$$ 双向绑定', '@State 变量与 Sheet 显隐自动同步')
this.buildFeatureRow('🧩', '任意组件绑定', 'bindSheet 是通用属性...')
// ...

9.4 @Builder 传函数作为参数

@Builder
buildActionItem(icon: string, title: string, desc: string, color: string,
  onClick: () => void): void {
  Row() {
    Text(icon).fontSize(22)
    Column() {
      Text(title).fontColor(color)
      Text(desc)
    }
  }
  .onClick(() => { onClick(); })
}

调用时传入不同的操作函数:

this.buildActionItem('✏️', '编辑', '修改当前内容', '#2C3E50', () => {
  this.actionLog = '执行了编辑操作';
  this.actionSheetShow = false;
  this.safeToast('已执行编辑操作');
})

10. 多 Sheet 并存——三个独立 Sheet 的状态管理与切换

10.1 三个独立的 @State

@State shareSheetShow: boolean = false;
@State actionSheetShow: boolean = false;
@State settingsSheetShow: boolean = false;

三个布尔型 @State 变量分别控制三个 Sheet 的显隐。它们彼此独立,互不影响。

10.2 三个独立的 bindSheet

// 按钮 1:分享面板
Button('分享面板')
  .bindSheet($$this.shareSheetShow, () => { this.shareSheetBuilder(); }, {
    detents: [SheetSize.MEDIUM],
    // ...
  })

// 按钮 2:操作菜单
Button('操作菜单')
  .bindSheet($$this.actionSheetShow, () => { this.actionSheetBuilder(); }, {
    detents: [SheetSize.MEDIUM],
    // ...
  })

// 按钮 3:设置面板
Button('设置面板')
  .bindSheet($$this.settingsSheetShow, () => { this.settingsSheetBuilder(); }, {
    detents: [SheetSize.LARGE],
    // ...
  })

10.3 互斥与共存

默认情况下,三个 Sheet 可以同时显示。但由于它们都使用了 maskColor,当多个 Sheet 同时出现时,遮罩层会叠加,视觉效果可能不太理想。

如果需要实现"互斥显示"(同一时间只能打开一个 Sheet),可以在打开一个 Sheet 前关闭其他 Sheet:

Button('分享面板')
  .onClick(() => {
    // 先关闭其他 Sheet
    this.actionSheetShow = false;
    this.settingsSheetShow = false;
    // 再打开自己
    this.shareSheetShow = true;
  })

在 SheetDemo 中,我们没有实现互斥逻辑,而是让三个 Sheet 独立工作,方便用户对比不同配置的效果。


11. dragBar 与 maskColor——拖拽条与遮罩层的视觉配置

11.1 dragBar

dragBar: true,   // 显示拖拽条
dragBar: false,  // 隐藏拖拽条

dragBar 是 Sheet 顶部的水平细条,默认为灰色圆角矩形,水平居中。它有两个作用:

  1. 视觉暗示:告诉用户"这个面板是可以拖拽的"
  2. 交互入口:用户可以通过拖拽 dragBar 在不同高度档位之间切换

dragBar: true 时,用户可以通过拖拽手势切换 detents 中定义的不同高度档位。当 dragBar: false 时,拖拽条隐藏,用户只能通过点击按钮或代码切换高度。

11.2 maskColor

maskColor: '#33000000',   // 半透明黑色遮罩
maskColor: '#44000000',   // 略深的半透明黑色遮罩

maskColor 设置 Sheet 外部遮罩层的颜色。在 Sheet 中,不需要像 Panel 一样额外设置 mask: boolean——只需要设置 maskColor,遮罩就会自动显示。

maskColor 的格式是 8 位十六进制颜色值:#AARRGGBB(Alpha 两位 + Red 两位 + Green 两位 + Blue 两位)。

颜色值 透明度 效果
'#00000000' 完全透明 相当于无遮罩
'#33000000' 20% 不透明 轻微遮罩
'#44000000' 27% 不透明 中等遮罩
'#88000000' 53% 不透明 明显遮罩
'#CC000000' 80% 不透明 强烈遮罩

12. onAppear / onDisappear 生命周期回调

12.1 基本用法

.onAppear(() => {
  // Sheet 出现时触发
  this.actionLog = '操作菜单已打开';
})
.onDisappear(() => {
  // Sheet 消失时触发
  this.actionLog = '操作菜单已关闭';
})

12.2 触发时机

回调 触发时机 典型场景
onAppear Sheet 入场动画开始时 记录日志、数据加载、埋点上报
onDisappear Sheet 退场动画结束时 保存状态、释放资源、埋点上报

12.3 在 SheetDemo 中的应用

在操作菜单和设置面板的 bindSheet 配置中,我们使用 onAppear 和 onDisappear 来更新操作日志:

.bindSheet($$this.actionSheetShow, () => { ... }, {
  onAppear: () => { this.actionLog = '操作菜单已打开'; },
  onDisappear: () => { this.actionLog = '操作菜单已关闭'; },
})

当你打开操作菜单时,反馈区的"最近操作"会显示"操作菜单已打开";关闭后变为"操作菜单已关闭"。这让用户能直观地感知 Sheet 的生命周期变化。


13. ScrollSizeMode——滚动行为的精细控制

13.1 枚举值

enum ScrollSizeMode {
  FOLLOW_DETENT = 0,  // 跟随 detents 设置的高度
  CONTINUOUS     = 1,  // 连续滑动
}

13.2 FOLLOW_DETENT 模式(默认)

scrollSizeMode: ScrollSizeMode.FOLLOW_DETENT,

Sheet 的滚动行为严格遵循 detents 中定义的高度档位。用户拖拽经过每个高度档位时,Sheet 会"吸附"到该档位。这种模式提供了明确的高度「档位感」,让用户清楚地知道 Sheet 处于哪个高度层级。

13.3 CONTINUOUS 模式

scrollSizeMode: ScrollSizeMode.CONTINUOUS,

Sheet 的滚动行为是连续的,不会在特定档位停留。用户拖拽时 Sheet 高度平滑变化,松手后停在当前位置。这种模式提供了更自由的交互体验,适用于没有严格高度档位要求的场景。


14. 构建中修复的 12 个编译错误全记录

在开发 SheetDemo 的过程中,我们遇到了 12 个编译错误。这些错误全部与 HarmonyOS SDK 6.1.1 (API 24) 的 API 约束相关。记录如下:

14.1 SheetMode 枚举值错误(3 个错误)

ERROR: Property 'MEDIUM' does not exist on type 'typeof SheetMode'.
ERROR: Property 'AUTO' does not exist on type 'typeof SheetMode'.
ERROR: Property 'LARGE' does not exist on type 'typeof SheetMode'.

原因:SheetMode 用于控制显示层级,而不是控制高度。其枚举值为 OVERLAYEMBEDDED

修复:将 SheetMode.MEDIUM 改为 SheetMode.OVERLAY。高度控制通过 detents + SheetSize 实现。

14.2 borderRadius 不在 SheetOptions 中(2 个错误)

ERROR: 'borderRadius' does not exist in type 'SheetOptions'.

原因:SheetOptions 中没有 borderRadius 属性。

修复:移除 borderRadius。Sheet 的圆角效果可以通过 Builder 内容的容器样式实现,或者使用 borderRadius 在 Column 上设置。

14.3 title 属性格式错误

ERROR: Type '{ text: string; icon: string; }' is not assignable to type 
       'CustomBuilder | SheetTitleOptions'.

原因title 属性接受 SheetTitleOptions 类型,不支持 { text, icon } 格式。正确的格式是 { title: string }

修复:移除 title 配置,在 Builder 内部手动添加标题。

14.4 SheetScrollSizeMode 不存在

ERROR: Cannot find name 'SheetScrollSizeMode'. Did you mean 'ScrollSizeMode'?

原因:正确的枚举名是 ScrollSizeMode,不是 SheetScrollSizeMode

修复SheetScrollSizeMode.FOLLOW_CONTENTScrollSizeMode.FOLLOW_DETENT

14.5 FOLLOW_CONTENT 不存在

ERROR: Property 'FOLLOW_CONTENT' does not exist on type 'typeof ScrollSizeMode'.
       Did you mean 'FOLLOW_DETENT'?

原因ScrollSizeMode 的枚举值只有 FOLLOW_DETENTCONTINUOUS

修复ScrollSizeMode.FOLLOW_CONTENTScrollSizeMode.FOLLOW_DETENT

14.6 mask 属性不在 SheetOptions 中(3 个错误)

ERROR: 'mask' does not exist in type 'SheetOptions'.

原因:SheetOptions 中没有独立的 mask: boolean 属性。遮罩的控制方式是通过 maskColor 来实现的——设置颜色即显示遮罩。

修复:移除 mask: true,仅保留 maskColor

14.7 Grid 子组件必须为 GridItem

ERROR: The component 'Grid' can only have the child component 'GridItem'.

原因:Grid 的直接子组件必须使用 GridItem() 包裹。

修复:在 ForEach 中为每个 Column 外包裹 GridItem()

14.8 错误汇总表

# 错误信息 错误原因 修复方式
1-3 SheetMode.MEDIUM/AUTO/LARGE does not exist SheetMode 控制层级而非高度 改用 SheetMode.OVERLAY
4-5 borderRadius does not exist in SheetOptions SheetOptions 无此属性 移除
6 title text+icon 格式错误 title 类型为 SheetTitleOptions 移除,Builder 内加标题
7 SheetScrollSizeMode not found 枚举名错误 改为 ScrollSizeMode
8 FOLLOW_CONTENT not found 枚举值错误 改为 FOLLOW_DETENT
9-11 mask does not exist in SheetOptions 遮罩通过 maskColor 控制 移除 mask: true
12 Grid needs GridItem child Grid 子组件约束 GridItem() 包裹

这些错误的本质是:新版的 Sheet API(API 12+)在设计上做了 Clean-up——移除了一些冗余属性,规范了枚举命名,加强了组件约束。 理解这些变化是顺利使用 bindSheet 的关键。


15. Panel 到 bindSheet 的迁移指南

15.1 属性映射表

Panel 写法 bindSheet 写法
Panel(this.show) Button().bindSheet($$this.show, builder)
.mode(PanelMode.Half) detents: [SheetSize.MEDIUM]
.mode(PanelMode.Full) detents: [SheetSize.LARGE]
.dragBar(true) dragBar: true
.mask(true) (通过 maskColor 隐式控制)
.maskColor('#33000000') maskColor: '#33000000'
.onChange((w,h,m)=>{}) (使用 @State 双向绑定替代)
.onAppear(()=>{}) onAppear: ()=>{}
.onDisAppear(()=>{}) onDisappear: ()=>{}
.borderRadius({topLeft:20}) (在 Builder 中自行设置圆角)
Stack 包裹管理层级 mode: SheetMode.OVERLAY

15.2 迁移步骤

将现有的 Panel 代码迁移到 bindSheet 可以按以下步骤进行:

步骤 1:替换显隐控制

// Before: Panel
@State showPanel: boolean = true;
Panel(this.showPanel) { ... }

// After: bindSheet
@State showSheet: boolean = true;
Button('打开').bindSheet($$this.showSheet, () => { ... })

步骤 2:替换模式控制

// Before: PanelMode
.mode(PanelMode.Half)

// After: SheetSize + detents
detents: [SheetSize.MEDIUM]

步骤 3:迁移内容

将 Panel 的子组件移动到 @Builder 方法中:

// Before: Panel 子组件
Panel(this.show) {
  Column() { /* 评论列表 */ }
}

// After: @Builder
@Builder
commentSheetBuilder(): void {
  Column() { /* 评论列表 */ }
}

Button('评论').bindSheet($$this.show, () => { this.commentSheetBuilder(); })

步骤 4:移除 Stack 层叠容器

由于 bindSheet 自动在 OVERLAY 层级显示,不再需要 Stack 容器来管理 Panel 的层叠位置。

15.3 不可迁移的 Panel 特性

以下 Panel 功能在 bindSheet 中没有直接对应的 API:

  1. Mini 模式:Panel 的 Mini 模式(最小化条状显示)在 bindSheet 中没有等价物。bindSheet 的最小显示状态由 detents 数组的第一个元素决定。

  2. 自定义半屏比例:Panel 的 .halfHeight(0.5) 允许自定义半屏高度的比例。bindSheet 中可以通过在 detents 中传入具体数值来实现:detents: [400](固定 400vp 高度)。

  3. backgroundMask:Panel 的背景遮罩属性。bindSheet 不需要此属性,因为遮罩行为由 maskColor 控制。


16. 总结与最佳实践

16.1 核心要点回顾

通过 SheetDemo 的完整实现,我们系统学习了以下核心技术:

  1. bindSheet 通用属性:绑定到任意组件,通过 $$ + @State 实现双向绑定的显隐控制
  2. SheetMode 层级控制:OVERLAY(顶层显示)和 EMBEDDED(页面内显示)的区别与选择
  3. SheetSize + detents 高度控制:MEDIUM(约 50%)和 LARGE(约 75%)两种预设高度档位
  4. preferType 弹出方向:BOTTOM(底部弹出)和 CENTER(中央弹出)的选择
  5. Grid + GridItem 网格布局:4×2 的分享面板网格,columnsTemplate 和 rowsTemplate 的配置
  6. @Builder 组件复用:12 个 @Builder 方法的三级嵌套复用结构
  7. 多 Sheet 共存管理:3 个独立的 @State 变量分别控制 3 个不同内容的 Sheet
  8. dragBar 与 maskColor:拖拽条的显隐控制与遮罩层的颜色配置
  9. onAppear / onDisappear 生命周期:Sheet 显示/隐藏时的回调处理
  10. ScrollSizeMode 滚动控制:FOLLOW_DETENT 和 CONTINUOUS 两种滚动模式

16.2 最佳实践建议

1. 使用 bindSheet 替代 Panel

对于 API 12+ 的新项目,应当优先使用 bindSheet 而非 Panel。bindSheet 是官方推荐的方案,有更清晰的 API 设计和更活跃的维护。

2. 合理选择 OVERLAY / EMBEDDED

  • 全局性弹窗(分享、全局操作菜单)→ OVERLAY
  • 页面级弹窗(评论、商品详情)→ EMBEDDED

3. @Builder 命名规范

Sheet 内容的 @Builder 方法命名建议带 Sheet 后缀:

shareSheetBuilder()       // 分享面板
actionSheetBuilder()      // 操作菜单
settingsSheetBuilder()    // 设置面板

4. 善用 detents 多档位

对于内容量不确定的场景,使用双档位 detents:

detents: [SheetSize.MEDIUM, SheetSize.LARGE]

用户可以根据需要拖拽切换高度。

5. 独立 @State 管理

每个 Sheet 使用独立的 @State 变量:

@State sheet1Show: boolean = false;
@State sheet2Show: boolean = false;
@State sheet3Show: boolean = false;

避免使用一个变量控制多个 Sheet,这会导致状态管理的混乱。

6. 使用 try/catch 包装 showToast

在 API 24 中,promptAction.showToast 已弃用,但仍有可用。使用 try/catch 包装可以避免可能出现的运行时异常:

safeToast(message: string): void {
  try {
    promptAction.showToast({ message, duration: 2000 });
  } catch (_) { /* ignore */ }
}

16.3 扩展方向

SheetDemo 可以轻松扩展到以下实际项目场景:

  • 商品详情底部面板:展示商品规格、价格、优惠信息、购买按钮
  • 评论详情弹窗:展示评论列表、输入框、表情选择
  • 筛选面板:多条件筛选器(价格范围、品牌、分类)
  • 地址选择器:省市区三级联动选择
  • 日历/时间选择器:日期和时间的快捷选择界面
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐