【HarmonyOS Next】鸿蒙应用弹框和提示气泡详解(二)
一、前言
上期围绕 HarmonyOS Next 最新API趋势,介绍了鸿蒙应用中最新的自定义弹框和提示气泡的使用。
在鸿蒙ArkUI响应式布局中,早期弹框 Dialog 和提示气泡 Toast 与 UI 绑定,在纯逻辑类文件中使用不便,后续 API 迭代实现了解耦,且与 UI 强绑定的方式已不推荐。接着详细讲解了鸿蒙中弹框的使用,弹框有系统定制弹框(包括基础弹框如警告弹框、列表弹窗,以及带业务性质的 PickerDialog 弹框如日历选择器弹窗等)和自定义弹框两种方式,并给出了相应示例代码。
详细内容,可参见【HarmonyOS Next】鸿蒙应用弹框和提示气泡详解(一)
本期主要讲解浮层(OverlayManager),半模态页面(bindSheet),全模态页面(bindContentCover)。
二、OverlayManager,bindSheet,bindContentCover详解
(1)OverlayManager,bindSheet,bindContentCover分别是什么?
上期提到,在自定义弹框的API延伸中,为了实现UI解耦,官方特意在page界面之上添加,UI框架层预留挂靠节点。

这样的设计很好,可以在page界面之上,做自定义UI的处理。根据业务使用的不同,page之上是OverlayManager(浮层),再之上就是各种弹框气泡的层级,bindSheet,bindContentCover也在其中,这个层级默认为应用内顶层。
例如page页面切换,最上层不会受影响。浮层的效果,就是和page页面绑定在一起,页面消失,浮层也会。
而所谓的模态和半模态的概念,可以理解为全屏覆盖下方page界面的自定义UI即模板,反之则是半模态。
(2)OverlayManager

可以看到浮层的设置很简单,通过ComponentContent的形式,将需要的自定义View进行包裹。操作浮层对象进行添加,删除,显示,隐藏等操作。
浮层对象也放置到了上下文中,这样使用起来,也会和UI解耦,可以在纯业务类中处理调用时机。
例如在首页,添加活动icon入口,就可以使用浮层实现。
其他接口操作同理,调用很简单。接口调用详情参见官方API文档:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-arkui-uicontext#overlaymanager12
(2)bindSheet,bindContentCover
绑定半模态或者模态,实际上是在控件上添加一个组合式的自定义UI。
通过开关参数,UI界面Builder,Anim动画控制其显示或者隐藏。
我们有很多场景,需要在应用中用户进行额外的操作或确认,但又不想打断当前任务时,可使用bindSheet或者bindContentCover弹出半模态or模态自定义UI,来获取用户反馈。
比如在设置界面中,当用户点击某个设置项需要进一步确认修改时,通过bindSheet弹出包含确认和取消按钮的半模态弹窗,让用户进行选择,而当前的设置界面仍保持可见,用户可以清晰地看到之前的设置内容,便于对比和操作。
bindContentCover使用同理,只不过效果是全屏遮挡。
三、源码示例Demo:
import { curves, ComponentContent, OverlayManager } from '@kit.ArkUI';
// 定义图片信息接口
interface PictureInfo {
name: string;
picNum: string;
}
// 定义借阅人信息接口
interface BorrowerInfo {
name: string;
cardNum: string;
}
class Params {
context: UIContext;
offset: Position;
constructor(context: UIContext, offset: Position) {
this.context = context;
this.offset = offset;
}
}
@Builder
function builderOverlay(params: Params) {
Column() {
Stack() {
}.width(50).height(50).backgroundColor(Color.Yellow).position(params.offset).borderRadius(50)
.onClick(() => {
params.context.showAlertDialog(
{
title: 'title',
message: 'Text',
autoCancel: true,
alignment: DialogAlignment.Center,
gridCount: 3,
confirm: {
value: 'Button',
action: () => { }
},
cancel: () => { }
}
);
});
}.focusable(false).width('100%').height('100%').hitTestBehavior(HitTestMode.Transparent);
}
@Entry
@Component
struct PictureLibraryDemo {
// 图片馆的图片列表
private pictureList: Array<PictureInfo> = [
{ name: '图片1', picNum: 'PIC001' },
{ name: '图片2', picNum: 'PIC002' },
{ name: '图片3', picNum: 'PIC003' },
{ name: '图片4', picNum: 'PIC004' }
];
// 借阅人列表
private borrowerList: Array<BorrowerInfo> = [
{ name: '张三', cardNum: '123456789' },
{ name: '李四', cardNum: '987654321' },
{ name: '王五', cardNum: '555555555' },
{ name: '赵六', cardNum: '666666666' }
];
// 半模态转场控制变量
@State isSheetShow: boolean = false;
// 全模态转场控制变量,用于选择借阅人
@State isPresentForBorrower: boolean = false;
// 全模态转场控制变量,用于选择图片
@State isPresentForPicture: boolean = false;
// 用于存储当前选择的图片信息
@State currentPicture: PictureInfo | null = null;
// 用于存储当前选择的借阅人信息
@State currentBorrower: BorrowerInfo | null = null;
private uiContext: UIContext = this.getUIContext();
private overlayNode: OverlayManager = this.uiContext.getOverlayManager();
private overlayContent: ComponentContent<Params>[] = [];
controller: TextInputController = new TextInputController();
aboutToAppear(): void {
let uiContext = this.getUIContext();
let componentContent = new ComponentContent(
this.uiContext, wrapBuilder<[Params]>(builderOverlay),
new Params(uiContext, { x: 0, y: 100 })
);
this.overlayNode.addComponentContent(componentContent, 0);
this.overlayContent.push(componentContent);
}
aboutToDisappear(): void {
let componentContent = this.overlayContent.pop();
this.overlayNode.removeComponentContent(componentContent);
}
@Builder
PictureSelectionBuilder() {
Column() {
Row() {
Text('选择图片')
.fontSize(20)
.fontColor(Color.White)
.width('100%')
.textAlign(TextAlign.Center)
.padding({ top: 30, bottom: 15 });
}
.backgroundColor(0x007dfe);
Row() {
Text('+ 添加图片')
.fontSize(16)
.fontColor(0x333333)
.margin({ top: 10 })
.padding({ top: 20, bottom: 20 })
.width('92%')
.borderRadius(10)
.textAlign(TextAlign.Center)
.backgroundColor(Color.White);
}
Column() {
ForEach(this.pictureList, (item: PictureInfo, index: number) => {
Row() {
Column() {
if (index % 2 == 0) {
Column()
.width(20)
.height(20)
.border({ width: 1, color: 0x007dfe })
.backgroundColor(0x007dfe);
} else {
Column()
.width(20)
.height(20)
.border({ width: 1, color: 0x007dfe });
}
}
.width('20%');
Column() {
Text(item.name)
.fontColor(0x333333)
.fontSize(18);
Text(item.picNum)
.fontColor(0x666666)
.fontSize(14);
}
.width('60%')
.alignItems(HorizontalAlign.Start);
Column() {
Text('选择')
.fontColor(0x007dfe)
.fontSize(16)
.onClick(() => {
this.currentPicture = item;
this.isPresentForBorrower = true;
});
}
.width('20%');
}
.padding({ top: 10, bottom: 10 })
.border({ width: { bottom: 1 }, color: 0xf1f1f1 })
.width('92%')
.backgroundColor(Color.White);
});
}
.padding({ top: 20, bottom: 20 });
Text('确认选择图片')
.width('90%')
.height(40)
.textAlign(TextAlign.Center)
.borderRadius(10)
.fontColor(Color.White)
.backgroundColor(0x007dfe)
.onClick(() => {
// 这里可以添加确认选择图片后的逻辑,比如关闭模态等
this.isPresentForPicture = false;
});
}
.size({ width: '100%', height: '100%' })
.backgroundColor(0xf5f5f5);
}
@Builder
BorrowerSelectionBuilder() {
Column() {
Row() {
Text('选择借阅人')
.fontSize(20)
.fontColor(Color.White)
.width('100%')
.textAlign(TextAlign.Center)
.padding({ top: 30, bottom: 15 });
}
.backgroundColor(0x007dfe);
Row() {
Text('+ 添加借阅人')
.fontSize(16)
.fontColor(0x333333)
.margin({ top: 10 })
.padding({ top: 20, bottom: 20 })
.width('92%')
.borderRadius(10)
.textAlign(TextAlign.Center)
.backgroundColor(Color.White);
}
Column() {
ForEach(this.borrowerList, (item: BorrowerInfo, index: number) => {
Row() {
Column() {
if (index % 2 == 0) {
Column()
.width(20)
.height(20)
.border({ width: 1, color: 0x007dfe })
.backgroundColor(0x007dfe);
} else {
Column()
.width(20)
.height(20)
.border({ width: 1, color: 0x007dfe });
}
}
.width('20%');
Column() {
Text(item.name)
.fontColor(0x333333)
.fontSize(18);
Text(item.cardNum)
.fontColor(0x666666)
.fontSize(14);
}
.width('60%')
.alignItems(HorizontalAlign.Start);
Column() {
Text('选择')
.fontColor(0x007dfe)
.fontSize(16)
.onClick(() => {
this.currentBorrower = item;
// 这里可以添加选择借阅人后的逻辑,比如记录借阅信息等
console.log(`借阅人 ${this.currentBorrower.name} 选择了图片 ${this.currentPicture?.name}`);
this.isPresentForBorrower = false;
});
}
.width('20%');
}
.padding({ top: 10, bottom: 10 })
.border({ width: { bottom: 1 }, color: 0xf1f1f1 })
.width('92%')
.backgroundColor(Color.White);
});
}
.padding({ top: 20, bottom: 20 });
Text('确认选择借阅人')
.width('90%')
.height(40)
.textAlign(TextAlign.Center)
.borderRadius(10)
.fontColor(Color.White)
.backgroundColor(0x007dfe)
.onClick(() => {
// 这里可以添加确认选择借阅人后的逻辑,比如关闭模态等
this.isPresentForBorrower = false;
});
}
.size({ width: '100%', height: '100%' })
.backgroundColor(0xf5f5f5);
}
@Builder
PictureLibraryMain() {
Column() {
Row() {
Text('图片馆借阅系统')
.fontSize(20)
.fontColor(Color.White)
.width('100%')
.textAlign(TextAlign.Center)
.padding({ top: 30, bottom: 15 });
}
.backgroundColor(0x007dfe);
Row() {
Text('+ 借阅图片')
.fontSize(16)
.fontColor(0x333333)
.margin({ top: 10 })
.padding({ top: 20, bottom: 20 })
.width('92%')
.borderRadius(10)
.textAlign(TextAlign.Center)
.backgroundColor(Color.White)
.onClick(() => {
this.isPresentForPicture = true;
});
}
// 可以在这里显示当前借阅的信息等
}
.size({ width: '100%', height: '100%' })
.backgroundColor(0xf5f5f5);
}
// 第二步:定义半模态展示界面
// 通过@Builder构建模态展示界面
@Builder
MySheetBuilder() {
Column() {
Column() {
// 这里可以添加一些图片馆的基本信息或其他相关内容
Text('图片馆信息')
.fontSize(18)
.fontColor(0x333333)
.padding({ top: 10, bottom: 10 });
}
.width('92%')
.margin(15)
.backgroundColor(Color.White)
.shadow({ radius: 30, color: '#aaaaaa' })
.borderRadius(10);
Column() {
Text('+ 选择图片/借阅人')
.fontSize(18)
.fontColor(Color.Orange)
.fontWeight(FontWeight.Bold)
.padding({ top: 10, bottom: 10 })
.width('60%')
.textAlign(TextAlign.Center)
.borderRadius(15)
.onClick(() => {
// 这里可以根据具体情况决定是先选择图片还是借阅人,或者同时选择等逻辑
this.isPresentForPicture = true;
})
// 通过全模态接口,绑定模态展示界面MyContentCoverBuilder。transition属性支持自定义转场效果,此处定义了x轴横向入场
.bindContentCover($$this.isPresentForPicture, this.PictureSelectionBuilder(), {
transition: TransitionEffect.translate({ x: 500 }).animation({ curve: curves.springMotion(0.6, 0.8) })
});
}
.padding({ top: 60 });
}
}
build() {
Column() {
Row() {
// 这里可以添加一些页面顶部的信息,比如图片馆的标志等
Text('图片馆')
.fontSize(20)
.fontColor(Color.White)
.width('100%')
.textAlign(TextAlign.Center)
.padding({ top: 20, bottom: 10 });
}
.backgroundColor(0x007dfe);
this.PictureLibraryMain();
Row() {
Text("点击显示图片馆信息")
}
.width('100%')
.margin({ top: 200, bottom: 30 })
.borderRadius(10)
.backgroundColor(Color.White)
.onClick(() => {
this.isSheetShow = !this.isSheetShow;
})
// 第一步:定义半模态转场效果
.bindSheet($$this.isSheetShow, this.MySheetBuilder(), {
height: SheetSize.MEDIUM,
title: { title: "图片馆操作" },
});
}
.width('100%')
.height('100%')
.backgroundColor('#30aaaaaa');
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
- 165.
- 166.
- 167.
- 168.
- 169.
- 170.
- 171.
- 172.
- 173.
- 174.
- 175.
- 176.
- 177.
- 178.
- 179.
- 180.
- 181.
- 182.
- 183.
- 184.
- 185.
- 186.
- 187.
- 188.
- 189.
- 190.
- 191.
- 192.
- 193.
- 194.
- 195.
- 196.
- 197.
- 198.
- 199.
- 200.
- 201.
- 202.
- 203.
- 204.
- 205.
- 206.
- 207.
- 208.
- 209.
- 210.
- 211.
- 212.
- 213.
- 214.
- 215.
- 216.
- 217.
- 218.
- 219.
- 220.
- 221.
- 222.
- 223.
- 224.
- 225.
- 226.
- 227.
- 228.
- 229.
- 230.
- 231.
- 232.
- 233.
- 234.
- 235.
- 236.
- 237.
- 238.
- 239.
- 240.
- 241.
- 242.
- 243.
- 244.
- 245.
- 246.
- 247.
- 248.
- 249.
- 250.
- 251.
- 252.
- 253.
- 254.
- 255.
- 256.
- 257.
- 258.
- 259.
- 260.
- 261.
- 262.
- 263.
- 264.
- 265.
- 266.
- 267.
- 268.
- 269.
- 270.
- 271.
- 272.
- 273.
- 274.
- 275.
- 276.
- 277.
- 278.
- 279.
- 280.
- 281.
- 282.
- 283.
- 284.
- 285.
- 286.
- 287.
- 288.
- 289.
- 290.
- 291.
- 292.
- 293.
- 294.
- 295.
- 296.
- 297.
- 298.
- 299.
- 300.
- 301.
- 302.
- 303.
- 304.
- 305.
- 306.
- 307.
- 308.
- 309.
- 310.
- 311.
- 312.
- 313.
- 314.
- 315.
- 316.
- 317.
- 318.
- 319.
- 320.
- 321.
- 322.
- 323.
- 324.
- 325.
- 326.
- 327.
- 328.
- 329.
- 330.
- 331.
- 332.
- 333.
- 334.
- 335.
- 336.
- 337.
- 338.
- 339.
- 340.
- 341.
- 342.
- 343.
- 344.
- 345.
- 346.
- 347.
- 348.
- 349.
- 350.
- 351.
- 352.
- 353.
- 354.
- 355.
- 356.
- 357.
- 358.
- 359.
- 360.
- 361.
- 362.
- 363.
- 364.
- 365.
- 366.
- 367.
- 368.
- 369.
- 370.
- 371.
- 372.
- 373.
- 374.
- 375.
- 376.
- 377.
- 378.
- 379.
- 380.
- 381.
- 382.
- 383.
- 384.
- 385.
- 386.
- 387.
- 388.
- 389.
- 390.
- 391.