摘要

CustomDialog 是 ArkUI 体系中实现自定义弹窗、提示浮层、操作确认框的专用组件,摆脱系统默认弹窗样式限制,可自由实现提示弹窗、确认弹窗、表单输入弹窗、底部弹出弹窗、图文弹窗等交互浮层。API Version23 重构弹窗渲染层级、遮罩点击判定、入场退场动画、生命周期回调、窗口权限管控逻辑,修复低版本弹窗层级被页面覆盖、遮罩点击穿透、动画闪烁、弹窗尺寸自适应失效、多次打开控制器错乱等兼容问题。低版本项目升级至 API23 + 后常出现:弹窗无法居中、遮罩失效、关闭弹窗页面组件不刷新、多弹窗控制器冲突、底部弹窗高度溢出屏幕等故障。本文基于 DevEco Studio,适配 OpenHarmony API Version23 及以上标准,系统讲解 CustomDialog 控制器、弹窗生命周期、遮罩配置、自定义动画、多类型弹窗封装,结合提示弹窗、确认删除弹窗、表单输入弹窗、底部弹出弹窗四大业务场景提供完整可运行代码,输出弹窗层级适配、动画性能、内存释放优化规范,汇总版本升级兼容问题解决方案,为鸿蒙全局浮层弹窗交互开发提供标准化实操模板。

关键词

OpenHarmony;ArkUI;API Version23;CustomDialog;自定义弹窗;弹窗动画;遮罩;浮层交互;DialogController

一、引言

1.1 弹窗组件开发背景

原生简易弹窗能力单一,仅支持简单文字与按钮,业务中表单填写、多行提示、底部选择、自定义圆角样式均依赖 CustomDialog 自定义弹窗实现,覆盖删除确认、新增编辑、权限提示、操作警告、底部菜单等高频交互场景。

OpenHarmony API Version23 针对弹窗底层窗口引擎做全面升级,核心变更:

  1. 统一弹窗 zIndex 顶层渲染规则,弹窗默认高于页面所有组件,不会被列表、按钮遮挡;
  2. 重构遮罩点击逻辑,区分自动关闭 / 禁止关闭两种模式,修复点击穿透底层页面问题;
  3. 规范 CustomDialog 生命周期 aboutToAppear/aboutToDisappear 执行顺序;
  4. 优化 animateAttr 弹窗入场动画渲染管线,消除动画闪烁、卡顿;
  5. 强化 CustomDialogController 单例管控,多个弹窗独立控制器互不干扰;
  6. 增加弹窗尺寸边界约束,自动适配小屏设备,防止弹窗超出可视区域。

旧版本随意创建控制器、多层弹窗嵌套、动画模拟遮罩的写法升级 API23 后大量失效,因此掌握高版本标准 CustomDialog 开发规范是交互开发必备技能。

1.2 开发环境与测试场景

开发工具:DevEco Studio 5.0 及以上 适配系统:OpenHarmony API Version23、HarmonyOS NEXT 开发语言:ArkTS 测试场景:基础提示弹窗、删除二次确认弹窗、表单输入弹窗、底部上滑弹窗、带动画遮罩弹窗、多弹窗共存页面

二、API23+ CustomDialog 核心能力与版本变更

2.1 CustomDialogController 弹窗控制器核心参数

创建控制器时支持配置全局弹窗行为(API23 新增标准化参数)

ets

CustomDialogController({
  builder: 弹窗自定义组件,
  autoCancel: boolean, // true点击遮罩自动关闭弹窗;false遮罩无响应
  alignment: DialogAlignment, // 弹窗对齐位置:Center居中/Bottom底部/Top顶部
  offset: {x: vp, y: vp}, // 弹窗偏移距离
  customStyle: boolean // true自定义无边框无背景,自己实现圆角遮罩
})

2.2 弹窗生命周期回调(API23 执行顺序稳定)

  1. aboutToAppear:弹窗打开前执行,适合初始化输入框、加载数据
  2. aboutToDisappear:弹窗关闭销毁前执行,适合清空临时状态变量

2.3 弹窗入场退场动画规范

API23 支持在 CustomDialog 内部组件绑定 animateAttr 实现入场动画,系统自动处理退场反向动画,废弃旧版手动延时隐藏写法。

2.4 @DialogParam 传参规范

弹窗与主页面双向传参统一使用 @DialogParam 装饰器,支持普通变量、回调函数传递,API23 修复函数回调丢失作用域 bug。

2.5 API23 废弃与强制约束

  1. 废弃多层弹窗循环嵌套,最多支持两层弹窗,三层嵌套触发性能警告;
  2. 废弃页面 Stack 手动模拟弹窗遮罩,优先使用 controller 自带遮罩;
  3. 禁止弹窗内大量复杂列表、懒加载组件,容易造成弹窗关闭内存泄漏;
  4. 每个弹窗必须独立创建专属 CustomDialogController,禁止复用控制器。

三、API23 标准基础示例代码

3.1 基础确认弹窗

ets

// 弹窗组件
@CustomDialog
struct TipDialog {
  controller: CustomDialogController
  @DialogParam msg: string
  @DialogParam confirmCb: ()=>void
  build() {
    Column({space:20}) {
      Text(this.msg).fontSize(16)
      Row({space:15}) {
        Button("取消").layoutWeight(1).backgroundColor("#eee").fontColor("#333")
          .onClick(()=>this.controller.close())
        Button("确认").layoutWeight(1).backgroundColor("#007DFF")
          .onClick(()=>{this.confirmCb();this.controller.close()})
      }
    }
    .width(300)
    .padding(24)
    .backgroundColor(Color.White)
    .borderRadius(16)
    // 入场缩放透明动画 API23稳定兼容
    .animateAttr({duration:260, curve:Curve.EaseOut})
    .scale({x:0.85,y:0.85})
    .opacity(0)
  }
}

// 页面调用
@Entry
@Component
struct DialogBaseDemo {
  dialogCtrl:CustomDialogController = new CustomDialogController({
    builder:TipDialog({msg:"确定执行该操作?",confirmCb:()=>console.log("确认操作")}),
    autoCancel:true,
    alignment:DialogAlignment.Center
  })
  build() {
    Column() {
      Button("打开确认弹窗").onClick(()=>this.dialogCtrl.open())
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}

四、四大业务完整实战案例(API23 + 可直接运行)

4.1 实战一:删除确认警告弹窗(危险操作二次校验)

ets

@CustomDialog
struct DeleteDialog {
  controller: CustomDialogController
  @DialogParam deleteCallback: ()=>void
  build() {
    Column({space:22}) {
      Text("删除提示")
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
      Text("删除后数据无法恢复,是否确认删除?")
        .fontSize(15)
        .fontColor("#666")
      Row({space:16}) {
        Button("取消")
          .layoutWeight(1)
          .height(44)
          .backgroundColor("#F0F0F0")
          .fontColor("#333")
          .onClick(()=>this.controller.close())
        Button("确认删除")
          .layoutWeight(1)
          .height(44)
          .backgroundColor("#f56c6c")
          .onClick(()=>{
            this.deleteCallback()
            this.controller.close()
          })
      }
    }
    .width(320)
    .padding(26)
    .backgroundColor(Color.White)
    .borderRadius(18)
    .animateAttr({duration:280, curve:Curve.EaseOut})
    .scale({x:0.8,y:0.8})
    .opacity(0)
  }
}

@Entry
@Component
struct DeleteDialogPage {
  delDialog:CustomDialogController
  build() {
    Column({space:30}) {
      Text("删除弹窗实战 API23")
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
      Button("打开删除确认弹窗")
        .width(220)
        .height(48)
        .backgroundColor("#f56c6c")
        .fontColor(Color.White)
        .onClick(()=>{
          this.delDialog = new CustomDialogController({
            builder:DeleteDialog({deleteCallback:()=>promptAction.showToast({message:"删除成功"})}),
            autoCancel:true
          })
          this.delDialog.open()
        })
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}

4.2 实战二:表单输入弹窗(新增 / 编辑笔记通用弹窗)

ets

@CustomDialog
struct InputEditDialog {
  controller: CustomDialogController
  @DialogParam oldTitle: string
  @DialogParam submit: (title:string)=>void
  @State inputText: string = ""
  aboutToAppear() {
    this.inputText = this.oldTitle
  }
  build() {
    Column({space:20}) {
      Text(this.oldTitle ? "编辑内容" : "新增内容")
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
      TextInput({text:this.inputText, placeholder:"请输入文字内容"})
        .width("100%")
        .height(46)
        .onChange(v=>this.inputText = v)
      Row({space:15}) {
        Button("取消").layoutWeight(1).height(44).backgroundColor("#eee")
          .onClick(()=>this.controller.close())
        Button("保存").layoutWeight(1).height(44).backgroundColor("#007DFF")
          .onClick(()=>{
            if(!this.inputText) {
              promptAction.showToast({message:"内容不能为空"})
              return
            }
            this.submit(this.inputText)
            this.controller.close()
          })
      }
    }
    .width(340)
    .padding(24)
    .backgroundColor(Color.White)
    .borderRadius(16)
    .animateAttr({duration:260, curve:Curve.EaseOut})
    .scale({x:0.85,y:0.85})
    .opacity(0)
  }
}

@Entry
@Component
struct InputDialogPage {
  build() {
    Column({space:25}) {
      Button("新增内容弹窗").width(220).height(48)
        .onClick(()=>{
          let ctrl = new CustomDialogController({
            builder:InputEditDialog({oldTitle:"", submit:(t)=>promptAction.showToast({message:"新增:"+t})}),
            autoCancel:true
          })
          ctrl.open()
        })
      Button("编辑内容弹窗").width(220).height(48)
        .onClick(()=>{
          let ctrl = new CustomDialogController({
            builder:InputEditDialog({oldTitle:"原始测试文字", submit:(t)=>promptAction.showToast({message:"修改为:"+t})}),
            autoCancel:true
          })
          ctrl.open()
        })
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}

4.3 实战三:底部上滑弹窗(底部菜单选择弹窗)

API23 支持 alignment 设置 Bottom 实现底部弹窗,搭配向上滑入动画。

ets

@CustomDialog
struct BottomSheetDialog {
  controller: CustomDialogController
  @DialogParam selectCb: (idx:number)=>void
  build() {
    Column({space:0}) {
      Rect().width(40).height(5).borderRadius(3).fill("#ddd").margin({top:8})
      Text("请选择操作").fontSize(20).margin({top:15,bottom:15})
      List({space:0}) {
        ListItem() {Text("分享").width("100%").textAlign(TextAlign.Center).height(54).fontSize(17)}
          .onClick(()=>{this.selectCb(1);this.controller.close()})
        ListItem() {Text("收藏").width("100%").textAlign(TextAlign.Center).height(54).fontSize(17)}
          .onClick(()=>{this.selectCb(2);this.controller.close()})
        ListItem() {Text("举报").width("100%").textAlign(TextAlign.Center).height(54).fontSize(17).fontColor("#f56c6c")}
          .onClick(()=>{this.selectCb(3);this.controller.close()})
      }
      Blank().height(15)
      Button("取消")
        .width("90%")
        .height(48)
        .margin({bottom:20})
        .backgroundColor("#eee")
        .fontColor("#333")
        .onClick(()=>this.controller.close())
    }
    .width("100%")
    .backgroundColor(Color.White)
    .borderRadius({topLeft:16,topRight:16})
    // 底部弹窗向上滑入动画
    .animateAttr({duration:300, curve:Curve.EaseOut})
    .offset({y:300})
  }
}

@Entry
@Component
struct BottomDialogPage {
  build() {
    Column() {
      Button("打开底部弹窗菜单").width(220).height(48).backgroundColor("#007DFF").fontColor(Color.White)
        .onClick(()=>{
          let ctrl = new CustomDialogController({
            builder:BottomSheetDialog({selectCb:(num)=>promptAction.showToast({message:"选中操作"+num})}),
            autoCancel:true,
            alignment:DialogAlignment.Bottom
          })
          ctrl.open()
        })
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}

4.4 实战四:禁止遮罩关闭弹窗(重要提示弹窗)

autoCancel 设置 false,点击灰色遮罩无法关闭,仅按钮可关闭,用于强制阅读提示。

ets

@CustomDialog
struct ForbidMaskDialog {
  controller: CustomDialogController
  build() {
    Column({space:20}) {
      Text("重要提示")
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
        .fontColor("#f56c6c")
      Text("该功能仅登录用户可用,请前往登录页面完成账号登录后再使用全部功能。")
        .fontSize(16)
        .fontColor("#444")
      Button("前往登录")
        .width("100%")
        .height(46)
        .backgroundColor("#007DFF")
        .onClick(()=>this.controller.close())
    }
    .width(310)
    .padding(24)
    .backgroundColor(Color.White)
    .borderRadius(16)
    .animateAttr({duration:260, curve:Curve.EaseOut})
    .scale({x:0.85,y:0.85})
    .opacity(0)
  }
}

@Entry
@Component
struct ForbidMaskDialogPage {
  build() {
    Column() {
      Button("打开强制提示弹窗")
        .width(220)
        .height(48)
        .backgroundColor("#f56c6c")
        .fontColor(Color.White)
        .onClick(()=>{
          let ctrl = new CustomDialogController({
            builder:ForbidMaskDialog(),
            autoCancel:false // 遮罩点击无响应,只能按钮关闭
          })
          ctrl.open()
        })
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}

五、API23 + 弹窗专属适配与性能优化规范

5.1 多端尺寸适配规范

  1. 居中弹窗宽度固定 300~340vp,不使用 100% 宽度,平板大屏不会过宽;
  2. 底部弹窗宽度 100%,仅顶部圆角,适配手机全屏底部菜单;
  3. 弹窗内部内容超长必须嵌套 Scroll,防止小屏内容溢出屏幕;
  4. 所有弹窗内按钮统一高度 44~48vp,保证点击热区充足。

5.2 动画性能优化准则

  1. 入场动画仅使用 scale+opacity+offset 三种轻量动画,禁用渐变、阴影动画;
  2. 动画时长统一 260~300ms,曲线固定 Curve.EaseOut,系统原生流畅节奏;
  3. 弹窗关闭无需手动编写退场动画,API23 自动反向执行入场动画;
  4. 弹窗内避免嵌套 List、LazyForEach 大量列表,弹窗销毁会造成内存开销。

5.3 内存与控制器规范

  1. 每个弹窗单独实例化 CustomDialogController,禁止全局单例复用;
  2. 弹窗内临时输入变量使用 @State,弹窗关闭自动销毁,无内存残留;
  3. 多层弹窗最多两层,第三层弹窗会触发渲染性能告警;
  4. 页面退出时关闭所有已打开弹窗,避免悬浮弹窗阻塞页面销毁。

六、API23 升级高频兼容问题与解决方案

问题 1:弹窗点击灰色遮罩直接消失,业务需要禁止遮罩关闭 解决:创建控制器时设置 autoCancel: false,仅弹窗内部按钮可关闭。

问题 2:底部弹窗动画失效,直接弹出无滑入效果 解决:弹窗组件绑定 offset (y: 300) 初始偏移,搭配 animateAttr,alignment 设置 DialogAlignment.Bottom。

问题 3:弹窗打开后页面列表、按钮无法点击 解决:属于正常遮罩拦截逻辑,如需底层可交互使用 Stack 手动浮层,不使用 CustomDialog 全局弹窗。

问题 4:弹窗传参回调函数执行无响应,丢失逻辑 解决:使用 @DialogParam 完整接收回调,创建 controller 时直接传入 builder 参数,不延迟赋值。

问题 5:多次打开弹窗,界面状态错乱、输入框保留旧内容 解决:弹窗 aboutToAppear 生命周期重置所有 @State 变量,每次打开重新初始化。

问题 6:升级 API23 后弹窗层级被页面组件遮挡 解决:API23 原生弹窗自动最高层级,删除页面内高 zIndex 遮挡组件,不要自定义 Stack 模拟弹窗。

七、总结

CustomDialog 是鸿蒙自定义浮层弹窗标准实现方案,API Version23 全面优化弹窗窗口层级、遮罩控制、动画渲染、生命周期、控制器管理,解决低版本弹窗层级错乱、动画闪烁、遮罩穿透、传参失效等大量兼容问题。开发中分为居中确认弹窗、底部菜单弹窗、表单输入弹窗、强制提示弹窗四大通用模板,统一动画、尺寸、按钮规范,严格区分 autoCancel 遮罩关闭模式。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐