引言

对话框(Dialog)和操作菜单(ActionSheet)是移动应用中不可或缺的交互组件。从删除确认到选项选择,从表单填写到操作反馈,对话框承载着用户与应用之间的关键决策点。一个设计良好的对话框交互系统不仅要视觉清晰、反馈及时,更要处理好多弹窗之间的切换逻辑、遮罩层级和状态清理。

HarmonyOS NEXT 提供了多种对话框实现方式,包括 AlertDialogActionSheetCustomDialog 以及 promptAction 系列 API。本文将通过对这些 API 的深入剖析,结合一个"智能提醒管理"实战项目,带你构建一套健壮的对话框交互体系。

读完本文你将能够:

  • 掌握 AlertDialog 的创建、配置和事件处理
  • 理解 ActionSheet 的适用场景和定制方法
  • 学会用 Stack + 条件渲染构建可靠的弹窗系统
  • 理解弹窗层级管理、状态清理和交互闭环的设计方法
  • 获得一个包含 5 种交互模式的生产级 Dialog 方案

为什么不用 CustomDialogController?

在 HarmonyOS 的早期版本中,CustomDialogController 是构建自定义对话框的标准方式。它通过创建一个控制器实例,在需要时打开和关闭对话框。但在实际开发中,这种模式存在几个痛点:

生命周期不可控

CustomDialogController 在组件初始化时创建,它的生命周期由框架管理。当对话框需要根据用户输入动态调整内容时,状态同步变得复杂而脆弱。特别是当对话框内有 @State 变量需要从父组件传入时,数据流向容易混乱。

多弹窗场景复杂性

当一个页面中存在多个不同类型的弹窗(确认框、表单框、底部菜单等),使用 CustomDialogController 需要创建多个控制器实例,每个都需要独立的配置和状态管理。弹窗之间的切换、互斥和层级管理变得非常繁琐。

本文方案:Stack + 条件渲染

本文采用一种更直观和可靠的方式——使用 Stack 组件配合条件渲染:

Stack() {
  // 主内容
  Column() { ... }
  
  // 弹窗层:按需渲染,自动处理层级
  if (this.showAlert) { /* AlertDialog */ }
  if (this.showSheet) { /* ActionSheet */ }
  if (this.showForm)  { /* CustomDialog */ }
}

这种方案的核心优势在于:

  1. 状态驱动:弹窗的显隐完全由 @State 变量控制,数据流单一清晰
  2. 自动层级Stack 中后声明的子组件自然覆盖在前,无需手动管理 z-index
  3. 互斥简单:切换弹窗前先关闭其他弹窗,只需修改几个布尔状态
  4. 状态清理确定:弹窗关闭时立即重置相关临时变量,不会残留脏数据
  5. 调试友好:所有弹窗逻辑在同一文件内,一目了然

五种对话框交互模式

本文的 Demo 是一个"智能提醒管理"页面,用户可以管理一组提醒规则。它包含五种典型的对话框交互模式:

模式一:开关切换(即时反馈)

Toggle 开关是最简单的交互模式。用户点击开关后,状态即时更新,无需二次确认。这种模式适用于轻量级、可逆的操作。

Toggle({ type: ToggleType.Switch, isOn: rule.enabled })
  .selectedColor('#1677FF')
  .onChange((val: boolean) => {
    this.toggleRule(rule.id);
  })

实现要点:

  • 使用 slice() 创建数组副本,修改后整体替换以触发 UI 刷新
  • 由于 ArkTS 的响应式要求,不能直接 arr[i].enabled = false,需要创建新的对象
toggleRule(id: number): void {
  let arr = this.rules.slice();
  for (let i = 0; i < arr.length; i++) {
    if (arr[i].id === id) {
      arr[i] = new ReminderRule(
        arr[i].id, arr[i].title, arr[i].subtitle, arr[i].time, !arr[i].enabled
      );
      break;
    }
  }
  this.rules = arr;
}

模式二:长按弹出菜单(上下文操作)

长按某个规则后,底部弹出操作菜单。这是典型的内容相关操作模式,用户需要先选择操作目标,再选择操作类型。

实现时使用 LongPressGesture 手势识别器:

.gesture(
  LongPressGesture({ repeat: false, duration: 400 })
    .onAction(() => {
      this.openActionSheet(rule.id);
    })
)

400ms 的长按识别时长是一个合理的折中:太短容易误触,太长会让用户感觉迟钝。

打开菜单前要先关闭其他可能打开的弹窗,确保同一时间只有一个弹窗处于活跃状态:

openActionSheet(id: number): void {
  this.actionTargetId = id;
  this.showMethodSheet = false;
  this.showDeleteConfirm = false;
  this.showForm = false;
}

模式三:删除确认(AlertDialog 模式)

执行不可逆操作前,应该弹出确认对话框。这是 AlertDialog 最经典的用途——保护用户数据不被意外删除。

if (this.showDeleteConfirm) {
  // 半透明遮罩
  Column()
    .width('100%').height('100%')
    .backgroundColor('#00000055')
    .onClick(() => {
      this.showDeleteConfirm = false;
      this.actionTargetId = -1;
    })
  
  // 居中对话框
  Column() {
    Text('确认删除')
      .fontSize(17)
      .fontWeight(FontWeight.Bold)
    Text('删除后无法恢复,确定要删除这个提醒吗?')
      .fontSize(13)
      .fontColor('#888899')
    Row() {
      Text('取消').onClick(() => { /* 关闭 */ })
      Text('确认删除')
        .backgroundColor('#FF4D4F')
        .onClick(() => { this.confirmDelete(); })
    }
  }
  .padding(20)
  .backgroundColor('#FFFFFF')
  .borderRadius(14)
  .position({ top: '35%', left: 0, right: 0 })
  .padding({ left: 40, right: 40 })
}

设计要点:

  • 遮罩可点击关闭:点击半透明背景区域等同于取消操作
  • 危险操作用红色:确认删除按钮使用 #FF4D4F(红色),形成视觉警示
  • 提供取消入口:始终给用户留一条"退出"的路径
  • 操作后及时反馈:删除成功后使用 Toast 告知用户结果
    在这里插入图片描述
    在这里插入图片描述

模式四:选项切换(底部菜单 / ActionSheet)

当用户需要从多个预设选项中选择一个时,底部菜单是最自然的交互模式。它从屏幕底部滑出,用户可以单手轻松触达。

本文 Demo 中的"提醒方式"设置使用了底部菜单:

if (this.showMethodSheet) {
  // 遮罩
  Column()
    .width('100%').height('100%')
    .backgroundColor('#00000055')
    .onClick(() => { this.showMethodSheet = false; })
  
  // 底部菜单内容
  Column() {
    ForEach(this.methodLabels, (label: string, idx: number) => {
      Row() {
        Text(label)
          .fontColor(idx === this.methodIdx ? '#1677FF' : '#1a1a2e')
          .fontWeight(idx === this.methodIdx ? FontWeight.Medium : FontWeight.Normal)
          .layoutWeight(1)
        if (idx === this.methodIdx) {
          Text('✓').fontColor('#1677FF')
        }
      }
      .onClick(() => {
        this.methodIdx = idx;
        this.showMethodSheet = false;
      })
    })
  }
  .backgroundColor('#FFFFFF')
  .borderRadius({ topLeft: 16, topRight: 16 })
  .position({ bottom: 0, left: 0, right: 0 })
}

设计要点:

  • 当前选中标记:使用 符号 + 蓝色高亮标识当前选中项
  • 选中立即关闭:点击选项后立即更新状态并关闭菜单,减少操作步数
  • 顶部分离的取消按钮:与选项组之间有 8vp 间距,视觉上区分"操作区"和"退出区"
  • 顶部圆角borderRadius({ topLeft: 16, topRight: 16 }) 让底部菜单有浮层感

模式五:表单对话框(新增 / 编辑)

当需要用户输入信息时,居中显示的表单对话框是最佳选择。它将用户注意力聚焦在表单上,同时保持背景内容的可见性(通过遮罩),让用户知道自己"在哪里"。

本文 Demo 中,新增和编辑提醒共用一个表单对话框,通过 formMode 区分模式:

if (this.showForm) {
  // 遮罩
  Column()
    .width('100%').height('100%')
    .backgroundColor('#00000055')
    .onClick(() => { /* 关闭并清理 */ })
  
  // 表单
  Column() {
    Text(this.formMode === 'add' ? '新建提醒' : '编辑提醒')
      .fontSize(17).fontWeight(FontWeight.Bold)
    
    Text('提醒标题').fontSize(12).fontColor('#BBBBCC')
    TextInput({ text: this.formTitle, placeholder: '输入提醒标题' })
      .onChange((value: string) => { this.formTitle = value; })
    
    Text('提醒时间').fontSize(12).fontColor('#BBBBCC')
    TextInput({ text: this.formTime, placeholder: '例如 09:00' })
      .onChange((value: string) => { this.formTime = value; })
    
    Row() {
      Text('取消').onClick(() => { /* 关闭并清理 */ })
      Text('保存').onClick(() => { this.submitForm(); })
    }
  }
  .backgroundColor('#FFFFFF')
  .borderRadius(14)
}

新增与编辑复用的关键设计:

  • 打开时为 formModeformTitleformTimeformId 设置正确的初始值
  • add 模式:空标题、默认时间 09:00formId = -1
  • edit 模式:从目标规则读取现有标题和时间、formId = 目标规则 id
  • 提交时根据 formMode 执行不同的逻辑(新增 push / 编辑替换)

弹窗叠加层设计模式

层级结构

Stack 中,弹窗叠加层遵循固定的结构:

Stack
├── Column (主内容,始终可见)
├── if (showActionSheet)  → Column (遮罩) + Column (底部菜单)
├── if (showAlert)        → Column (遮罩) + Column (居中对话框)
└── if (showFormDialog)   → Column (遮罩) + Column (居中表单)

遮罩层通用模式

每个弹窗都包含一个半透明遮罩层:

Column()
  .width('100%')
  .height('100%')
  .backgroundColor('#00000055')
  .onClick(() => { this.closeDialog(); })

遮罩层的作用:

  1. 视觉聚焦:将背景压暗,让用户注意力集中在弹窗上
  2. 交互拦截:阻止点击穿透到主内容
  3. 关闭入口:点击遮罩等同于取消操作(对所有弹窗适用)

弹窗互斥模式

当一个弹窗打开时,必须关闭其他弹窗。这通过在所有打开弹窗的方法中重置其他弹窗的状态来实现:

openDeleteConfirm(): void {
  this.showMethodSheet = false;    // 关闭底部菜单
  this.showDeleteConfirm = true;   // 打开删除确认
  this.showForm = false;           // 关闭表单弹窗
}

这种互斥机制确保了:

  • 同一时间只有一个弹窗处于活跃状态
  • 用户不会面对"弹窗叠弹窗"的混乱局面
  • 状态管理简单可预测

关闭时清理模式

弹窗关闭时必须重置所有临时状态,防止下次打开时出现脏数据:

// 关闭表单弹窗的清理
this.showForm = false;
this.formTitle = '';
this.formTime = '09:00';
this.formId = -1;

// 关闭删除确认的清理
this.showDeleteConfirm = false;
this.actionTargetId = -1;

不清理的后果可能包括:

  • 编辑弹窗中残留上一个规则的标题和时间
  • 删除确认弹窗删除了错误的目标
  • 长按菜单显示了上一个目标的信息

数据结构与状态管理

ReminderRule 类

使用显式 class 定义数据模型(ArkTS 不支持对象字面量展开):

class ReminderRule {
  id: number;
  title: string;
  subtitle: string;
  time: string;
  enabled: boolean;

  constructor(id: number, title: string, subtitle: string,
              time: string, enabled: boolean) {
    this.id = id;
    this.title = title;
    this.subtitle = subtitle;
    this.time = time;
    this.enabled = enabled;
  }
}

状态变量清单

@State rules: ReminderRule[] = [];      // 规则列表
@State methodIdx: number = 0;           // 提醒方式索引
@State showMethodSheet: boolean = false; // 方式选择菜单显隐
@State showDeleteConfirm: boolean = false; // 删除确认框显隐
@State showForm: boolean = false;       // 表单对话框显隐
@State formMode: string = 'add';        // 表单模式
@State formTitle: string = '';          // 表单标题输入
@State formTime: string = '09:00';      // 表单时间输入
@State formId: number = -1;             // 编辑目标 id
@State actionTargetId: number = -1;     // 操作目标 id

设计理念:

  • 显隐状态用独立 boolean:每个弹窗一个 showXxx,互不干扰
  • 上下文状态用 id 引用actionTargetIdformId 记录"当前在操作哪个规则"
  • 表单状态独立管理formTitleformTimeformMode 与规则列表状态分离

完整交互流程分析

以一个典型的"删除提醒"操作为例,跟踪整个交互链路:

步骤 1:长按规则

用户长按"喝水提醒" → LongPressGesture 触发
→ openActionSheet(3) 被调用
→ actionTargetId = 3 (记录目标)
→ 其他弹窗状态全部置 false (互斥)
→ UI 渲染:出现半透明遮罩 + 底部操作菜单

步骤 2:选择"删除"

用户在菜单中点击"删除"按钮
→ openDeleteConfirm() 被调用
→ ActionSheet 关闭
→ showDeleteConfirm = true
→ UI 渲染:遮罩 + 居中确认对话框

注意这里的关键设计:ActionSheet 并未直接执行删除,而是先关闭自己再打开确认框。这给了用户一个"确认"的机会,同时也避免了"弹窗叠弹窗"的问题。

步骤 3:确认删除

用户点击"确认删除"
→ confirmDelete() 被调用
→ 从 rules 数组中 filter 掉 id=3 的规则
→ rules = arr (slice 替换)
→ showDeleteConfirm = false
→ actionTargetId = -1
→ promptAction.showToast({ message: '已删除提醒' })
→ UI 渲染:弹窗消失,列表从 3 项变为 2 项,Toast 提示

步骤 4:状态回顾

操作完成后,所有临时状态已清理:
- showDeleteConfirm = false
- actionTargetId = -1
- rules 已更新(不包含已删除的规则)

自定义 Button 样式 vs Toggle 组件

在本文 Demo 中,提醒规则的开关使用了 ArkUI 原生的 Toggle 组件而非自定义 Button 模拟。这是一个有意为之的选择:

Toggle({ type: ToggleType.Switch, isOn: rule.enabled })
  .selectedColor('#1677FF')
  .onChange((val: boolean) => {
    this.toggleRule(rule.id);
  })

使用原生 Toggle 的好处:

  • 系统级交互一致性:与系统设置中的开关体验完全一致
  • 内置动画:Toggle 自带平滑的状态切换动画,无需手动处理
  • 无障碍支持:原生组件自带无障碍标签,符合可访问性要求
  • 减少代码量:无需自己管理 on/off 状态切换和动画逻辑

常见问题与解决方案

问题一:弹窗无法关闭

原因:点击事件被弹窗内容消费后冒泡到遮罩层,或遮罩层的 onClick 未被正确设置。

解决方案:确保遮罩层是一个独立的 Column(或 Row),且其 onClick 直接指向关闭逻辑。不要在弹窗内容中使用会在事件流中"吞掉"点击的组件。

问题二:Array 更新后 UI 不刷新

原因:直接修改了数组元素的属性(如 this.rules[0].enabled = false),这在 ArkTS 的响应式系统中不会触发 UI 更新。

解决方案:始终使用 slice() 创建数组副本,创建新的元素对象,然后整体替换数组:

// 错误做法
this.rules[0].enabled = false;

// 正确做法
let arr = this.rules.slice();
arr[0] = new ReminderRule(arr[0].id, arr[0].title, arr[0].subtitle, arr[0].time, false);
this.rules = arr;

问题三:多个弹窗同时显示

原因:打开新弹窗前没有关闭已打开的弹窗。

解决方案:在每个弹窗的打开方法中,显式关闭其他所有弹窗:

openXxx(): void {
  this.showA = false;
  this.showB = false;
  this.showC = true;
  // ...
}

问题四:编辑弹窗中残留旧数据

原因:关闭弹窗时未重置表单状态。

解决方案:在"取消"按钮和遮罩点击事件中,彻底清理表单状态:

.onClick(() => {
  this.showForm = false;
  this.formTitle = '';
  this.formTime = '09:00';
  this.formId = -1;
})

总结

本文通过一个"智能提醒管理"实战 Demo,系统讲解了 HarmonyOS 中 AlertDialog、ActionSheet 和自定义对话框的构建方法。我们没有使用 CustomDialogControllerpromptAction 等封装好的 API,而是选择了更底层但也更可控的 Stack + 条件渲染方案。

这种方案虽然代码量略多,但换来了:

  • 完全的状态可控性:弹窗的每一次显隐都是明确的、可追溯的
  • 无生命周期黑盒:不会出现"弹窗关不掉"或"弹窗内容不更新"的问题
  • 清晰的交互闭环:打开 → 操作 → 反馈 → 清理,每一步都有明确的责任归属
  • 易于扩展:新增一种弹窗只需添加一个 if 块和对应的状态变量

在实际项目中,你可以将这种弹窗叠加层模式封装成可复用的工具方法或组件,在保持可控性的同时减少重复代码。

Dialog 是用户与 App 之间最重要的"对话"形式。一个设计良好的 Dialog 系统,让用户感到安全和被尊重;而一个有 bug 的 Dialog(比如关不掉的弹窗、点不到的按钮、残留的脏数据),则会让用户沮丧和不安。希望本文的实践能帮助你在 HarmonyOS 项目中构建值得信赖的对话框交互体验。


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐