1# 模态弹窗 (ModelDialog) 2 3## 概述 4 5模态(Modal)是UI组件或视图的一种状态。其在消失之前,用户只能对处于模态的组件或视图进行响应,不能操作其他非模态的组件或视图,干扰性比较强。 6 7ArkUI中可通过使用[AlertDialog](../reference/apis-arkui/arkui-ts/ts-methods-alert-dialog-box.md)、[CustomDialog](../reference/apis-arkui/arkui-ts/ts-methods-custom-dialog-box.md)、[ActionSheet](../reference/apis-arkui/arkui-ts/ts-methods-action-sheet.md)、[Popup](../reference/apis-arkui/arkui-ts/ts-universal-attributes-popup.md)、[Menu](../reference/apis-arkui/arkui-ts/ts-basic-components-menu.md)、[ContextMenu](../reference/apis-arkui/arkui-ts/ts-methods-menu.md)等组件实现模态类弹窗能力。 8 9| 名称 | 使用场景 | 10| ------------------------------------ | ------------------------------------------------------------ | 11| AlertDialog | 通常用来展示用户当前需要或必须关注的信息或操作。如用户操作一个敏感行为时响应一个二次确认的弹窗。 | 12| ActionSheet | 当需要用户关注或确认的信息存在列表选择时使用。 | 13| CustomDialog | 当用户需要自定义弹窗内的组件和内容时使用。 | 14| Popup | 用于为指定的组件做信息提示。如点击一个问号图标弹出一段气泡提示。 | 15| Menu/ContextMenu | 用于给指定的组件绑定用户可执行的操作,如长按图标展示操作选项等。 | 16 17> **说明:** 18> 19> - 本指导介绍模态弹窗,开发者可通过配置参数(例如:CustomDialog的isModal)调整弹窗为非模态弹窗,从而满足不同的使用场景。 20> 21> - 移动设备中,子窗模式的弹窗当前无法超出主窗口。 22> 23> - 多个弹窗组件先后弹出时,后弹出的组件的层级高于先弹出的层级,退出时按照层级从高到低的顺序逐次退出。 24 25## 使用全局弹窗 26 27全局弹窗不与任何组件绑定,一般用于针对用户触发的操作进行必要提示时使用。ArkUI当前提供了定制和自定义两类弹窗组件。 28 29定制:AlertDialog、ActionSheet、promptAction.showDialog、promptAction.showActionMenu。开发者可使用此类组件,指定需要显示的文本内容和按钮操作即可完成简单的交互效果。 30 31自定义:CustomDialog、promptAction.openCustomDialog。开发者需要根据场景传入自定义组件填充在弹窗中实现自定义的弹窗内容。 32 33下面以AlertDialog、ActionSheet 和 CustomDialog 为例说明相应的弹窗效果与使用方法。 34 35- **AlertDialog:** 警告弹窗,需要向用户提问或得到用户的许可。 36 - 警告弹窗用来提示重要信息,但会中断当前任务,尽量提供必要的信息和有用的操作。 37 - 避免仅使用警告弹窗提供信息,用户不喜欢被信息丰富但不可操作的警告打断。 38 - 必选内容包含:标题、可选信息文本、最多3个按钮。 39 - 可选内容包含:输入框、icon、checkBox和HelpButton。 40 41  42 43 ```ts 44 @Entry 45 @Component 46 struct AlertDialogExample { 47 build() { 48 Column({ space: 5 }) { 49 Button('two button dialog') 50 .onClick(() => { 51 AlertDialog.show( 52 { 53 title: 'title', 54 subtitle: 'subtitle', 55 message: 'text', 56 autoCancel: true, 57 alignment: DialogAlignment.Bottom, 58 gridCount: 4, 59 offset: { dx: 0, dy: -20 }, 60 primaryButton: { 61 value: 'cancel', 62 action: () => { 63 console.info('Callback when the first button is clicked') 64 } 65 }, 66 secondaryButton: { 67 enabled: true, 68 defaultFocus: true, 69 style: DialogButtonStyle.HIGHLIGHT, 70 value: 'ok', 71 action: () => { 72 console.info('Callback when the second button is clicked') 73 } 74 } 75 } 76 ) 77 }).backgroundColor(0x317aff) 78 }.width('100%').margin({ top: 5 }) 79 } 80 } 81 ``` 82 83- **ActionSheet:** 列表选择弹窗。 84 85 适合展示多个操作项,尤其是除了操作列表以外没有其他的展示内容。 86 87  88 89 90 ```ts 91 @Entry 92 @Component 93 struct ActionSheetExample { 94 build() { 95 Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { 96 Button('Click to Show ActionSheet') 97 .onClick(() => { 98 ActionSheet.show({ 99 title: 'ActionSheet title', 100 subtitle: 'ActionSheet subtitle', 101 message: 'message', 102 autoCancel: true, 103 confirm: { 104 defaultFocus: true, 105 value: 'Confirm button', 106 action: () => { 107 console.log('Get Alert Dialog handled') 108 } 109 }, 110 alignment: DialogAlignment.Bottom, 111 offset: { dx: 0, dy: -10 }, 112 sheets: [ 113 { 114 title: 'apples', 115 action: () => { 116 console.log('apples') 117 } 118 }, 119 { 120 title: 'bananas', 121 action: () => { 122 console.log('bananas') 123 } 124 }, 125 { 126 title: 'pears', 127 action: () => { 128 console.log('pears') 129 } 130 } 131 ] 132 }) 133 }) 134 }.width('100%') 135 .height('100%') 136 } 137 } 138 ``` 139 140- **CustomDialog:** 自定义弹窗。 141 142 当开发者需要自定义弹窗的内容和样式时,可选择CustomDialog。更建议使用[promptAction.openCustomDialog](../reference/apis-arkui/js-apis-promptAction.md#promptactionopencustomdialog11)。 143 144  145 146 ```ts 147 // xxx.ets 148 @CustomDialog 149 @Component 150 struct CustomDialogExample { 151 @Link textValue: string 152 @Link inputValue: string 153 controller?: CustomDialogController 154 cancel: () => void = () => { 155 } 156 confirm: () => void = () => { 157 } 158 159 build() { 160 Column() { 161 Text('Change text').fontSize(20).margin({ top: 10, bottom: 10 }) 162 TextInput({ placeholder: '', text: this.textValue }).height(60).width('90%') 163 .onChange((value: string) => { 164 this.textValue = value 165 }) 166 Text('Whether to change a text?').fontSize(16).margin({ bottom: 10 }) 167 Flex({ justifyContent: FlexAlign.SpaceAround }) { 168 Button('cancel') 169 .onClick(() => { 170 if (this.controller != undefined) { 171 this.controller.close() 172 this.cancel() 173 } 174 }).backgroundColor(0xffffff).fontColor(Color.Black) 175 Button('confirm') 176 .onClick(() => { 177 if (this.controller != undefined) { 178 this.inputValue = this.textValue 179 this.controller.close() 180 this.confirm() 181 } 182 }).backgroundColor(0xffffff).fontColor(Color.Red) 183 }.margin({ bottom: 10 }) 184 }.borderRadius(10) 185 } 186 } 187 188 @Entry 189 @Component 190 struct CustomDialogUser { 191 @State textValue: string = '' 192 @State inputValue: string = 'click me' 193 dialogController: CustomDialogController | null = new CustomDialogController({ 194 builder: CustomDialogExample({ 195 cancel: () => { 196 this.onCancel() 197 }, 198 confirm: () => { 199 this.onAccept() 200 }, 201 textValue: $textValue, 202 inputValue: $inputValue 203 }), 204 cancel: this.exitApp, 205 autoCancel: true, 206 onWillDismiss: (dismissDialogAction: DismissDialogAction) => { 207 console.info("reason=" + JSON.stringify(dismissDialogAction.reason)) 208 console.log("dialog onWillDismiss") 209 if (dismissDialogAction.reason == DismissReason.PRESS_BACK) { 210 dismissDialogAction.dismiss() 211 } 212 if (dismissDialogAction.reason == DismissReason.TOUCH_OUTSIDE) { 213 dismissDialogAction.dismiss() 214 } 215 }, 216 alignment: DialogAlignment.Bottom, 217 offset: { dx: 0, dy: -20 }, 218 gridCount: 4, 219 customStyle: false, 220 cornerRadius: 10, 221 }) 222 223 // 在自定义组件即将析构销毁时将dialogController置空 224 aboutToDisappear() { 225 this.dialogController = null // 将dialogController置空 226 } 227 228 onCancel() { 229 console.info('Callback when the first button is clicked') 230 } 231 232 onAccept() { 233 console.info('Callback when the second button is clicked') 234 } 235 236 exitApp() { 237 console.info('Click the callback in the blank area') 238 } 239 240 build() { 241 Column() { 242 Button(this.inputValue) 243 .onClick(() => { 244 if (this.dialogController != null) { 245 this.dialogController.open() 246 } 247 }).backgroundColor(0x317aff) 248 }.width('100%').margin({ top: 5 }) 249 } 250 } 251 ``` 252 253## 使用气泡Popup 254 255当点击目标组件或交互区时,弹出内容在其他内容之上,可使用Popup来指示当前功能如何操作。 256 257 258 259```ts 260@Entry 261@Component 262struct PopupExample { 263 @State handlePopup: boolean = false 264 265 build() { 266 Flex({ direction: FlexDirection.Column }) { 267 // PopupOptions 类型设置弹框内容 268 Button('PopupOptions') 269 .onClick(() => { 270 this.handlePopup = !this.handlePopup 271 }) 272 .bindPopup(this.handlePopup, { 273 message: 'This is a popup with PopupOptions', 274 placementOnTop: true, 275 showInSubWindow: false, 276 primaryButton: { 277 value: 'confirm', 278 action: () => { 279 this.handlePopup = !this.handlePopup 280 console.info('confirm Button click') 281 } 282 }, 283 // 第二个按钮 284 secondaryButton: { 285 value: 'cancel', 286 action: () => { 287 this.handlePopup = !this.handlePopup 288 console.info('cancel Button click') 289 } 290 }, 291 onStateChange: (e) => { 292 console.info(JSON.stringify(e.isVisible)) 293 if (!e.isVisible) { 294 this.handlePopup = false 295 } 296 } 297 }) 298 .position({ x: 100, y: 150 }) 299 }.width('100%').padding({ top: 5 }) 300 } 301} 302``` 303 304## 使用菜单Menu 305 306很多时候需要通过交互弹出一些菜单选项,用于用户操作。此时可通过Menu和MenuItem组件组合成需要弹出的菜单选项内容,然后借助bindMenu和bindContextMenu方法将菜单和组件绑定。 307 308| 方法名 | 使用场景 | 309| -------------------------- | -------------------------- | 310| bindMenu | 无需预览图场景,需要在非子窗场景显示。 | 311| bindContextMenu | 需要预览图场景使用,只能在子窗中显示。 | 312 313- **bindMenu**:一种临时性弹出组件,用于展示用户可执行的操作。 314 315 | 一级Menu | 多级Menu | 316 | -------------------------- | -------------------------- | 317 |  |  | 318 319 ```ts 320 @Entry 321 @Component 322 struct Index { 323 @State select: boolean = true 324 325 @Builder 326 MyMenu() { 327 Menu() { 328 MenuItem({ content: "菜单选项" }) 329 MenuItem({ content: "菜单选项" }) 330 MenuItem({ content: "菜单选项" }) 331 MenuItem({ content: "菜单选项" }) 332 } 333 } 334 335 build() { 336 Row() { 337 Column() { 338 Text('click to show menu') 339 .fontSize(50) 340 .fontWeight(FontWeight.Bold) 341 } 342 .bindMenu(this.MyMenu) 343 .width('100%') 344 } 345 .height('100%') 346 } 347 } 348 ``` 349 350 351- **bindContextMenu:** 内容包括菜单、预览图、蒙层,通常在长按桌面图标时使用。 352 353 | 相对父组件区域弹出 | 相对点击位置弹出 | 354 | --------------------------------- | --------------------------------- | 355 |  |  | 356 357 ```ts 358 @Entry 359 @Component 360 struct Index { 361 @State select: boolean = true 362 363 @Builder 364 MyMenu(){ 365 Menu() { 366 MenuItem({ content: "菜单选项" }) 367 MenuItem({ content: "菜单选项" }) 368 MenuItem({ content: "菜单选项" }) 369 MenuItem({ content: "菜单选项" }) 370 } 371 } 372 373 build() { 374 Row() { 375 Column() { 376 Text('click to show menu') 377 .fontSize(50) 378 .fontWeight(FontWeight.Bold) 379 } 380 .bindContextMenu(this.MyMenu, ResponseType.LongPress,{ 381 placement: Placement.Left, 382 preview: MenuPreviewMode.IMAGE 383 }) 384 .width('100%') 385 } 386 .height('100%') 387 } 388 } 389 ``` 390 391 392## 超出应用界面 393 394在2in1设备上,使用模态类弹窗时,会出现超出主窗口显示的场景,如下图所示。 395 396 397 398开发者可通过[支持子窗口](#支持子窗口)和[默认子窗口](#默认子窗口)实现超出应用界面效果。 399 400### 支持子窗口 401 402自定义弹窗(CustomDialog)、警告弹窗(AlertDialog)、列表选择弹窗(ActionSheet)、气泡提示(Popup)可通过showInSubWindow,设置弹窗或气泡在子窗口中,从而实现超出主窗口的显示效果。 403 404```ts 405// xxx.ets 406@CustomDialog 407struct CustomDialogExample { 408 controller?: CustomDialogController 409 cancel: () => void = () => { 410 } 411 confirm: () => void = () => { 412 } 413 build() { 414 Column() { 415 Text('可展示在主窗口外的弹窗') 416 .fontSize(30) 417 .height(100) 418 Button('点我关闭弹窗') 419 .onClick(() => { 420 if (this.controller != undefined) { 421 this.controller.close() 422 } 423 }) 424 .margin(20) 425 } 426 } 427} 428@Entry 429@Component 430struct CustomDialogUser { 431 dialogController: CustomDialogController | null = new CustomDialogController({ 432 builder: CustomDialogExample({ 433 cancel: ()=> { this.onCancel() }, 434 confirm: ()=> { this.onAccept() } 435 }), 436 cancel: this.existApp, 437 autoCancel: true, 438 alignment: DialogAlignment.Center, 439 offset: { dx: 0, dy: -20 }, 440 gridCount: 4, 441 showInSubWindow: true, 442 isModal: true, 443 customStyle: false, 444 cornerRadius: 10, 445 }) 446 // 在自定义组件即将析构销毁时将dialogController置空 447 aboutToDisappear() { 448 this.dialogController = null // 将dialogController置空 449 } 450 451 onCancel() { 452 console.info('Callback when the first button is clicked') 453 } 454 455 onAccept() { 456 console.info('Callback when the second button is clicked') 457 } 458 459 existApp() { 460 console.info('Click the callback in the blank area') 461 } 462 463 build() { 464 Column() { 465 Button('click me') 466 .onClick(() => { 467 if (this.dialogController != null) { 468 this.dialogController.open() 469 } 470 }).backgroundColor(0x317aff) 471 }.width('100%').margin({ top: 5 }) 472 } 473} 474``` 475 476### 默认子窗口 477 478可使用 bindContextMenu为组件绑定菜单,触发方式为长按或者右键点击,弹出菜单项需要自定义。 479 480```ts 481@Entry 482@Component 483struct ContextMenuExample { 484 @Builder MenuBuilder() { 485 Flex({ direction: FlexDirection.Column, justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) { 486 Text('Test menu item 1') 487 .fontSize(20) 488 .width(100) 489 .height(50) 490 .textAlign(TextAlign.Center) 491 Divider().height(10) 492 Text('Test menu item 2') 493 .fontSize(20) 494 .width(100) 495 .height(50) 496 .textAlign(TextAlign.Center) 497 }.width(100) 498 } 499 500 build() { 501 Column() { 502 Text('LongPress for menu') 503 } 504 .width('100%') 505 .margin({ top: 5 }) 506 .bindContextMenu(this.MenuBuilder, ResponseType.LongPress) 507 } 508} 509``` 510 511