找回密码
 立即注册
首页 业界区 安全 【拥抱鸿蒙】HarmonyOS之构建一个自定义弹框 ...

【拥抱鸿蒙】HarmonyOS之构建一个自定义弹框

馏栩梓 2025-5-30 15:16:46


弹窗是一种模态窗口,通常用来展示用户当前需要的或用户必须关注的信息或操作。在UI开发中,弹框是重要且不可忽视的组件。
HarmonyOS内置了多种系统弹框,分别有AlertDialog 、TextPickerDialog 、DatePickerDialog以及TimePickerDialog等。

本文将详细介绍系统弹框的封装和使用,并着重展现自定义弹框的实现。


系统弹框

AlertDialog

AlertDialog是警告弹窗,一般由App主动弹出,用于警告和确认用户的操作行为,需用户手动点击操作按钮来取消或进行下一步。

AlertDialog的实现

如下图中的“删除联系人”弹框,一个AlertDialog包含标题、内容和操作区三个部分组成,操作区包含两个按钮,我们可以在按钮的点击事件里添加对应响应逻辑。

1.png


以上弹框的实现代码如下:
  1. AlertDialog.show(
  2.       {
  3.         title: '删除联系人', // 标题
  4.         message: '是否需要删除所选联系人?', // 内容
  5.         autoCancel: false, // 点击遮障层时,是否关闭弹窗。
  6.         alignment: DialogAlignment.Bottom, // 弹窗在竖直方向的对齐方式
  7.         offset: { dx: 0, dy: -20 }, // 弹窗相对alignment位置的偏移量
  8.         primaryButton: {
  9.           value: '取消',
  10.           action: () => {
  11.             console.info('Callback when the first button is clicked');
  12.           }
  13.         },
  14.         secondaryButton: {
  15.           value: '删除',
  16.           fontColor: '#D94838',
  17.           action: () => {
  18.             console.info('Callback when the second button is clicked');
  19.           }
  20.         },
  21.         cancel: () => { // 点击遮障层关闭dialog时的回调
  22.           console.info('Closed callbacks');
  23.         }
  24.       }
  25.     )
  26.   })
复制代码

AlertDialog的封装

我们可以对AlertDialog进行封装,作为工具类调用。
  1. export class CommonUtils {
  2. /\*\*
  3.    \* Common alert dialog
  4.    \* @param title 标题
  5.    \* @param msg 提示信息
  6.    \* @param context 需要保存状态的UIAbility所对应的context
  7.    \* @param primaryCallback 第一个按钮点击事件的回调
  8.    \* @param secondCallback 第二个按钮点击事件的回调
  9.    \*/
  10.   commonAlertDialog(title:ResourceStr, msg: ResourceStr, context: common.UIAbilityContext, primaryCallback: Function, secondCallback: Function) {
  11.     AlertDialog.show({
  12.       title: title,
  13.       message: msg,
  14.       alignment: DialogAlignment.Bottom,
  15.       offset: {
  16.         dx: 0,
  17.         dy: CommonConstants.DY\_OFFSET
  18.       },
  19.       primaryButton: {
  20.         value: $r('app.string.cancel\_button'),
  21.         action: () => {
  22.           primaryCallback();
  23.         }
  24.       },
  25.       secondaryButton: {
  26.         value: $r('app.string.definite\_button'),
  27.         action: () => {
  28.           context.terminateSelf()
  29.           secondCallback();
  30.         }
  31.       }
  32.     });
  33.   }
  34. }
复制代码
这里创建了CommonUtils的工具类,把标题、提示信息作为创建自定义弹框的参数,按钮的点击事件可在回调里分别实现。

有了这种封装,我们就能很容易地在App里调用一个风格统一的AlertDialog弹框了。
  1. CommonUtils.commonAlertDialog("提示", "是否退出登录", context, () => {
  2.   // 取消
  3.   
  4. }, () => {
  5.   // 确认
  6.   
  7. });
复制代码

TextPickerDialog

这是一种文本滑动选择弹窗,一般用于从多个选项中单选内容,再将用户所选的内容返回给调用方。
如下图所示,这里实现了一个选择“足球主队”的弹窗,用户上下滑动滑块再点击“确认”就可以完成选择。
2.png

TextPickerDialog的实现

  1. @Entry
  2. @Component
  3. struct TextPickerDialogDemo {
  4.   @State select: number = 2;
  5.   private fruits: string[] = ['巴塞罗那', '曼城', '利物浦', '迈阿密国际', '拜仁慕尼黑', '多特蒙德', 'AC米兰', '那不勒斯'];
  6.   build() {
  7.     Column() {
  8.       Button('TextPickerDialog')
  9.         .margin(20)
  10.         .onClick(() => {
  11.           TextPickerDialog.show({
  12.             range: this.fruits, // 设置文本选择器的选择范围
  13.             selected: this.select, // 设置初始选中项的索引值。
  14.             onAccept: (value: TextPickerResult) => { // 点击弹窗中的“确定”按钮时触发该回调。
  15.               // 设置select为按下确定按钮时候的选中项index,这样当弹窗再次弹出时显示选中的是上一次确定的选项
  16.               this.select = value.index;
  17.               console.info("TextPickerDialog:onAccept()" + JSON.stringify(value));
  18.             },
  19.             onCancel: () => { // 点击弹窗中的“取消”按钮时触发该回调。
  20.               console.info("TextPickerDialog:onCancel()");
  21.             },
  22.             onChange: (value: TextPickerResult) => { // 滑动弹窗中的选择器使当前选中项改变时触发该回调。
  23.               console.info('TextPickerDialog:onChange()' + JSON.stringify(value));
  24.             }
  25.           })
  26.         })
  27.     }
  28.     .width('100%')
  29.   }
  30. }
复制代码

TextPickerDialog的封装


我们可以将选项作为参数进行TextPickerDialog的封装,并提供用户确认选项的回调。
这里的range的类型为:string[] | string[][] | Resource | TextPickerRangeContent[] | TextCascadePickerRangeContent[],提供了多种数据源类型,我们一般使用Resource方便多语言适配。
  1. export class CommonUtils {
  2.   /\*\*
  3.    \* Text picker dialog
  4.    \* @param items 选项
  5.    \* @param textCallback 选中返回
  6.    \*/
  7.   textPickerDialog(items: Resource, textCallback: Function) {
  8.     if (this.isEmpty(items)) {
  9.       Logger.error(CommonConstants.TAG\_COMMON\_UTILS, 'item is null')
  10.       return;
  11.     }
  12.     TextPickerDialog.show({
  13.       range: items,
  14.       canLoop: false,
  15.       selected: 0,
  16.       onAccept: (result: TextPickerResult) => {
  17.         textCallback(result.value);
  18.       },
  19.       onCancel: () => {
  20.         Logger.info(CommonConstants.TAG\_COMMON\_UTILS, 'TextPickerDialog canceled')
  21.       }
  22.     });
  23.   }
  24. }
复制代码

对工具类中的TextPickerDialog的调用如下:
  1. CommonUtils.textPickerDialog($r('app.strarray.club\_array'), (selectedValue: string) => {
  2.             this.club = selectedValue;
  3.           })
  4.         }
复制代码

这里的app.strarray.club\_array指向resources中的配置文件stringarray.json5,其内容如下:
  1. {
  2.     "strarray": [
  3.         {
  4.             "name": "club\_array",
  5.             "value": [
  6.                 {
  7.                     "value": "巴塞罗那"
  8.                 },
  9.                 {
  10.                     "value": "曼城"
  11.                 },
  12.                 {
  13.                     "value": "利物浦"
  14.                 },
  15.                 {
  16.                     "value": "迈阿密国际"
  17.                 },
  18.                 {
  19.                     "value": "拜仁慕尼黑"
  20.                 },
  21.                 {
  22.                     "value": "AC米兰"
  23.                 },
  24.                 {
  25.                     "value": "多特蒙德"
  26.                 },
  27.                 {
  28.                     "value": "阿贾克斯"
  29.                 }
  30.             ]
  31.         }
  32.     ]
  33. }
复制代码

DatePickerDialog

DatePickerDialog是日期选择器弹框,用于选择特定格式的日期,并返回给调用方。

3.png




DatePickerDialog的实现

以“出生日期”选择器弹框为例,我们通过如下代码可以实现:
  1. let selectedDate = new Date('1949-10-1');
  2. DatePickerDialog.show({
  3.             start: new Date('1900-1-1'), // 设置选择器的起始日期
  4.             end: new Date('2000-12-31'), // 设置选择器的结束日期
  5.             selected: selectedDate, // 设置当前选中的日期
  6.             lunar: false,
  7.             onDateAccept: (value: Date) => { // 点击弹窗中的“确定”按钮时触发该回调
  8.               // 通过Date的setFullYear方法设置按下确定按钮时的日期,这样当弹窗再次弹出时显示选中的是上一次确定的日期
  9.               selectedDate.setFullYear(value.getFullYear(), value.getMonth() + 1, value.getDate())
  10.               console.info('DatePickerDialog:onDateAccept()' + JSON.stringify(value))
  11.             },
  12.             onCancel: () => { // 点击弹窗中的“取消”按钮时触发该回调
  13.               console.info('DatePickerDialog:onCancel()')
  14.             },
  15.             onDateChange: (value: Date) => { // 滑动弹窗中的滑动选择器使当前选中项改变时触发该回调
  16.               console.info('DatePickerDialog:onDateChange()' + JSON.stringify(value))
  17.             }
  18.           })
  19.         })
复制代码

DatePickerDialog的封装


日期选择器包含起始日期、截止日期和默认选中日期三个参数,我们只需对用户确认选择后的回调里响应即可。
  1. export class CommonUtils {
  2.   /\*\*
  3.    \* Date picker dialog
  4.    \* @param dateCallback 确认选中日期回调
  5.    \*/
  6.   datePickerDialog(dateCallback: Function) {
  7.     DatePickerDialog.show({
  8.       start: new Date(CommonConstants.START\_TIME),
  9.       end: new Date(),
  10.       selected: new Date(CommonConstants.SELECT\_TIME),
  11.       lunar: false,
  12.       onDateAccept: (value: Date) => {
  13.         let year: number = value.getFullYear();
  14.         let month: number = value.getMonth() + 1;
  15.         let day: number = value.getDate();
  16.         let selectedDate: string = `${year}${CommonConstants.DATE\_YEAR}`+`${month}${CommonConstants.DATE\_MONTH}`+`${day}${CommonConstants.DATE\_DAY}`;
  17.         dateCallback(selectedDate);
  18.       }
  19.     });
  20.   }
  21. }
复制代码

基于以上封装,datePickerDialog的调用可以简单地实现如下:
  1. CommonUtils.datePickerDialog((dateValue: string) => {
  2.     this.birthdate = dateValue;
  3. })
复制代码

自定义弹框

除了系统弹框,还可以对弹框进行自定义。自定义弹框更加灵活,适用于更多的业务场景。

这里,我们实现一个包含多选器的自定义弹框,其实现效果如下图所示。
4.png

不难看出,这个弹框由标题、选择列表和按钮操作区构成。

自定义弹框需要使用装饰器@CustomDialog,
我们创建一个名为CustomDialogWidget的struct,并添加三个属性。

* items是数据源;
* selectedContent是选中结果拼接而成的字符串;
* controller是自定义弹框的控制器,其类型为CustomDialogController。
  1. export default struct CustomDialogWidget {
  2.   @State items: Array<CustomItem> = [];
  3.   @Link selectedContent: string;
  4.   private controller?: CustomDialogController;
  5. }
复制代码

在组件的aboutToAppear()中实现数据源的获取,使用到resmgr.ResourceManager的getStringArrayValue方法。
  1. aboutToAppear(): void {
  2.     let context: Context = getContext(this);
  3.     if (CommonUtils.isEmpty(context) || CommonUtils.isEmpty(context.resourceManager)) {
  4.       Logger.error(CommonConstants.TAG\_CUSTOM, 'context or resourceManager is null');
  5.       return;
  6.     }
  7.     let manager = context.resourceManager;
  8.     manager.getStringArrayValue($r('app.strarray.hobbies\_data').id, (error, hobbyArray) => {
  9.       if (!CommonUtils.isEmpty(error)) {
  10.         Logger.error(CommonConstants.TAG\_CUSTOM, 'error = ' + JSON.stringify(error));
  11.       } else {
  12.         hobbyArray.forEach((itemTitle: string) => {
  13.           let item = new CustomItem();
  14.           item.title = itemTitle;
  15.           item.isChecked = false;
  16.           this.items.push(item);
  17.           Logger.info(item.title);
  18.         });
  19.       }
  20.     });
  21.   }
复制代码

然后在Build()中实现其界面的搭建:
  1.   build() {
  2.     Column() {
  3.       // 标题
  4.       Text($r('app.string.title\_hobbies'))
  5.         .fontSize($r('app.float.title\_hobbies\_size'))
  6.         .fontColor($r('app.color.custom\_color'))
  7.         .lineHeight($r('app.float.title\_line\_height'))
  8.         .fontWeight(CommonConstants.BIGGER)
  9.         .alignSelf(ItemAlign.Start)
  10.         .margin({ left: $r('app.float.title\_left\_distance') })
  11.       // 选项列表
  12.       List() {
  13.        ForEach(this.items, (item: CustomItem) => {
  14.          ListItem() {
  15.           Row() {
  16.             Text(item.title)
  17.               .fontSize($r('app.float.label\_size'))
  18.               .fontColor($r('app.color.custom\_color'))
  19.               .layoutWeight(CommonConstants.WEIGHT\_ONE)
  20.               .textAlign(TextAlign.Start)
  21.               .fontWeight(CommonConstants.BIGGER)
  22.               .margin({ left: $r('app.float.label\_left\_distance') })
  23.             Toggle({ type: ToggleType.Checkbox, isOn: false })
  24.               .onChange((isCheck) => {
  25.                 item.isChecked = isCheck;
  26.               })
  27.               .width($r('app.float.toggle\_size'))
  28.               .height($r('app.float.toggle\_size'))
  29.               .margin({ right: $r('app.float.toggle\_right\_distance') })
  30.           }
  31.          }
  32.          .height($r('app.float.options\_height'))
  33.          .margin({
  34.            top: $r('app.float.options\_top\_distance'),
  35.            bottom:$r('app.float.options\_bottom\_distance')
  36.          })
  37.        }, (item: CustomItem) => JSON.stringify(item.title))
  38.       }
  39.       .margin({
  40.         top: $r('app.float.list\_top\_distance'),
  41.         bottom: $r('app.float.list\_bottom\_distance')
  42.       })
  43.       .divider({
  44.         strokeWidth: $r('app.float.divider\_height'),
  45.         color: $r('app.color.divider\_color')
  46.       })
  47.       .listDirection(Axis.Vertical)
  48.       .edgeEffect(EdgeEffect.None)
  49.       .width(CommonConstants.FULL\_WIDTH)
  50.       .height($r('app.float.options\_list\_height'))
  51.       // 操作按钮
  52.       Row() {
  53.         Button($r('app.string.cancel\_button'))
  54.           .dialogButtonStyle()
  55.           .onClick(() => {
  56.             this.controller?.close();
  57.           })
  58.         Blank()
  59.           .backgroundColor($r('app.color.custom\_blank\_color'))
  60.           .width($r('app.float.blank\_width'))
  61.           .opacity($r('app.float.blank\_opacity'))
  62.           .height($r('app.float.blank\_height'))
  63.         Button($r('app.string.definite\_button'))
  64.           .dialogButtonStyle()
  65.           .onClick(() => {
  66.             this.setSelectedItems(this.items);
  67.             this.controller?.close();
  68.           })
  69.       }
  70.     }
  71.   }
复制代码

在确定按钮的回调中,我们调用setSelectedItems(),其实现如下:
  1.   setSelectedItems(items: CustomItem[]) {
  2.     if (CommonUtils.isEmpty(items)) {
  3.       Logger.error(CommonConstants.TAG\_HOME, "Items is empty")
  4.       return;
  5.     }
  6.     let selectedText: string = items.filter((isCheckedItem: CustomItem) => isCheckedItem?.isChecked)
  7.       .map<string>((checkedItem: CustomItem) => {
  8.         return checkedItem.title!;
  9.       })
  10.       .join(CommonConstants.COMMA);
  11.     if (!CommonUtils.isEmpty(selectedText)) {
  12.       this.selectedContent = selectedText;
  13.     }
  14.   }
  15. }
复制代码

这里我们还用到了组件的属性扩展方法封装(用于提取重复的属性代码进行复用):
  1. @Extend(Button)
  2. function dialogButtonStyle() {
  3.   .fontSize($r('app.float.button\_text\_size'))
  4.   .fontColor(Color.Blue)
  5.   .layoutWeight(CommonConstants.WEIGHT\_ONE)
  6.   .height($r('app.float.button\_height'))
  7.   .backgroundColor(Color.White)
  8. }
复制代码

自定义弹框的调用


自定义弹框的调用基于CustomDialogController,将CustomDialogWidget作为它的参数builder即可实现控制器调出我们预期的自定义弹框。
  1. @State birthdate: string = '';
  2. @State sex: string = '';
  3. @State hobbies: string = '';
  4. private sexArray: Resource = $r('app.strarray.sex\_array');
  5. customDialogController: CustomDialogController = new CustomDialogController({
  6.     builder: CustomDialogWidget({
  7.       selectedContent: this.hobbies
  8.     }),
  9.     alignment: DialogAlignment.Bottom,
  10.     customStyle: true,
  11.     offset: {
  12.       dx: 0,
  13.       dy: CommonConstants.DY\_OFFSET
  14.     }
  15.   });
复制代码

以上,我们总结了HarmonyOS系统弹框和自定义弹框的实现、封装及调用。


我是郑知鱼
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册