HarmonyOS 实战 | 弹窗/提示框怎么做——Alert、Dialog、Toast 详解

一、我们想解决什么问题
1.1 生活中的"弹窗思维"
想象一下,你走进一家自助餐厅,拿了一大堆菜正准备开吃,服务员突然冲过来说:"先生,您刚才拿的那盘三文鱼是隔壁桌的。"这时候你面临一个选择:放下那盘鱼,或者假装没听见继续吃。
这个场景放到 App 里,就是一个二选一的确认弹窗——“是"还是"否”,你得给个说法。
再想象另一个场景:你在电商 App 里买了一双鞋,订单提交成功了,页面跳转的一瞬间,屏幕上弹出一个大大的绿色对勾,旁边写着"下单成功啦!"这个对勾停留了两秒就自动消失了,你甚至还没来得及仔细看。
这就是Toast 提示——轻量、短暂、用户不需要做任何操作。
还有一个场景:你去银行柜台办卡,工作人员递给你一张表格,表格上面密密麻麻列着各种条款,你必须把每一项都填完才能继续下一步。这就是有输入框的对话框——用户需要参与,而不仅仅是"知道了"。
你看,弹窗的世界其实和现实生活一一对应。我们需要解决的问题,归结起来就三句话:
- 需要用户明确做选择 → AlertDialog / ConfirmDialog
- 需要告诉用户操作结果,不需要用户做任何事 → Toast
- 需要用户在弹窗里输入信息 → TextPickerDialog、CustomDialog 等高级玩法
1.2 HarmonyOS 里的弹窗家族
HarmonyOS(或者说 ArkUI)为我们准备了一整套弹窗工具箱,用起来其实很顺手,主要分为三大类:
第一类:内置对话框(AlertDialog / ConfirmDialog)。这类弹窗是 ArkUI 内置的,不需要额外 import,拿来就能用。适合那种"确认/取消"、简单的列表选择等标准场景。缺点是样式相对固定,定制空间有限。
第二类:ActionSheet 和 promptAction。promptAction 是 HarmonyOS 提供的全局操作行为服务,可以控制从底部弹出的 ActionSheet(操作菜单)、showToast(轻提示)、showLoading(加载遮罩)等等。这类弹窗功能更丰富,但需要先 import { promptAction } from '@kit.ArkUI' 才能用。
第三类:自定义弹窗(CustomDialog)。当内置的几种弹窗样式满足不了你的需求时,CustomDialog 登场了。你可以往里面塞任意的 ArkUI 组件——输入框、轮播图、甚至一个小游戏。灵活性最高,但代码量也最大。
理解了这三类之后,我们再往下走,就不容易迷失在 API 的海洋里了。
二、数据模型设计
2.1 为什么要先设计数据模型
很多人在写弹窗代码的时候,喜欢直接对着业务逻辑硬写——“这里要弹窗,那就写一个 showToast”,“那里要确认,那就写一个 AlertDialog”。结果就是代码到处散落,样式不统一,改起来牵一发动全身。
更好的做法是先定义一套弹窗配置的数据结构,把弹窗的"样子"和"行为"抽离出来统一管理。就像装修房子之前先画图纸,而不是拎着锤子直接砸墙。
我们就用一个简单的 TypeScript interface 来定义弹窗的基本配置:
// DialogConfig.ets
// 弹窗配置文件,统一管理所有弹窗的样式和行为
export interface DialogConfig {
// 弹窗标题
title: string;
// 弹窗内容/描述
message: string;
// 确认按钮文字,null 表示不显示
confirmText?: string;
// 取消按钮文字,null 表示不显示
cancelText?: string;
// 弹窗类型:基础对话框 / Toast 轻提示 / 操作菜单
type: 'alert' | 'confirm' | 'toast';
// Toast 持续时间(毫秒),仅 type 为 toast 时生效
duration?: number;
// 是否点击遮罩可关闭(仅 ActionSheet 生效)
autoCancel?: boolean;
}
这段代码非常直接:一个弹窗有标题、有内容、有两个按钮(确认/取消)、有类型、有些弹窗还需要控制显示时长。你可能会想:Toast 根本不需要标题和按钮,为什么要用同一个 interface 来定义?
答案很简单:统一管理。在你的弹窗服务(DialogService)里,所有弹窗走同一个分发逻辑,根据 type 字段判断该调用哪个 API。后续如果要加统一行为(比如埋点统计所有弹窗的展示次数),只需要改一处代码。
2.2 业务层数据举例
在实际业务中,弹窗的配置通常由业务场景决定。我们在业务层定义具体的弹窗实例:
// 业务场景举例:用户未登录时点击收藏按钮
const loginPrompt: DialogConfig = {
title: '登录后即可收藏',
message: '登录后你可以收藏喜欢的仓库,随时查看更新动态',
confirmText: '去登录',
cancelText: '稍后再说',
type: 'confirm'
};
// 操作成功后的轻提示
const collectSuccessToast: DialogConfig = {
message: '收藏成功',
type: 'toast',
duration: 2000
};
这样设计之后,你的页面代码里就不再出现大段大段的弹窗调用了,只需要传一个配置对象过去:
// 页面中调用
DialogService.show(loginPrompt);
干不干净?整洁的代码是改出来的,不是凑出来的。
三、核心设计决策
3.1 三种弹窗方案横向对比
HarmonyOS 提供了多种弹窗实现方式,各有各的性格。以下是一个直观的横向对比,帮助你在合适的场景选对合适的工具:
| 特性 | AlertDialog | promptAction | CustomDialog |
|---|---|---|---|
| 使用门槛 | 直接用,不用 import | 需要 import promptAction | 需要单独写组件 |
| 适用场景 | 确认/取消、标准提示 | Toast、ActionSheet、Loading | 自定义样式、复杂交互 |
| 样式定制 | 固定,标题+内容+按钮 | 半固定,可控制时长/位置 | 完全自定义 |
| 交互方式 | 按钮点击 | 自动消失/手势滑出 | 按钮、手势均可 |
| 生命周期 | 同步,调用即显示 | 异步,自动管理时长 | 手动控制显隐 |
| 遮罩层 | 有(深色半透明) | Toast 无;ActionSheet 有 | 可配置 |
3.2 选型原则:简单优先,按需升级
这里我总结了一个"三步选型法",每次写弹窗之前问自己三个问题:
第一步:需要用户操作吗?
如果不需要用户做任何事,只是通知一声 → 选 Toast。如果需要用户做选择 → 继续第二步。
第二步:是标准二选一吗?
“确定/取消”、"是/否"这种标准二选一 → 选 AlertDialog 就够了。如果需要自定义按钮文字、图标,或者要做列表选择 → 考虑 promptAction.showActionSheet。如果标准方案都兜不住 → 走 CustomDialog。
第三步:需要自定义 UI 吗?
如果你的弹窗里要放一个评分星级、放一个图片轮播、放一个富文本编辑 → 别挣扎了,CustomDialog 是你唯一的出路。
这个选型流程其实反映了一个重要的工程原则:先用最简单的方案,满足当前需求。不要一上来就写 CustomDialog,AlertDialog 能搞定的事就先用 AlertDialog,等需求逼得你不得不升级的时候再升级。
四、完整代码实现
4.1 弹窗服务封装
我们把弹窗逻辑封装成一个 DialogService,这样在业务代码里调用就会非常干净。这个服务类负责根据配置类型分发到不同的弹窗 API:
// DialogService.ets
import { promptAction } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
export class DialogService {
// 获取 UIAbilityContext,用于 promptAction
private static getContext(): Context {
return AppStorage.get<object>('abilityContext') as Context;
}
// 统一入口
static show(config: DialogConfig): void {
switch (config.type) {
case 'toast':
this.showToast(config.message, config.duration ?? 2000);
break;
case 'confirm':
this.showConfirm(config);
break;
case 'alert':
default:
this.showAlert(config);
break;
}
}
// Toast 轻提示
private static showToast(message: string, duration: number): void {
promptAction.showToast({
message: message,
duration: duration
});
}
// AlertDialog - 单按钮确认
private static showAlert(config: DialogConfig): void {
AlertDialog.show({
title: config.title,
message: config.message,
confirm: {
value: config.confirmText ?? '知道了',
action: () => {}
}
});
}
// ConfirmDialog - 双按钮确认/取消
private static showConfirm(config: DialogConfig): void {
AlertDialog.show({
title: config.title,
message: config.message,
primaryButton: {
value: config.cancelText ?? '取消',
action: () => {}
},
secondaryButton: {
value: config.confirmText ?? '确认',
action: () => {}
}
});
}
}
这段代码最核心的思路就是分发:根据 type 字段决定走哪个分支。Toast 调 promptAction.showToast,单按钮用 AlertDialog.show,双按钮同样是 AlertDialog.show 但传了 primaryButton 和 secondaryButton 两个参数。
4.2 页面中的实际使用
写好了服务层,接下来在页面里用就非常轻松了。假设我们有一个仓库详情页,用户可以 star(收藏)这个仓库:
// RepositoryDetailPage.ets
import { DialogService } from '../service/DialogService';
@Entry
@Component
struct RepositoryDetailPage {
@State isStarred: boolean = false;
build() {
Column() {
// 仓库信息展示
Text('HarmonyOS 核心设计')
.fontSize(20)
.fontWeight(FontWeight.Bold)
Row() {
Button(this.isStarred ? '已收藏' : '收藏')
.onClick(() => this.handleStar())
}
}
.width('100%')
.padding(20)
}
private handleStar(): void {
if (this.isStarred) {
// 取消收藏 - 需要二次确认
DialogService.show({
title: '确认取消收藏',
message: '取消后你将不再收到该仓库的更新通知',
confirmText: '取消收藏',
cancelText: '保留',
type: 'confirm'
});
} else {
// 收藏成功 - 直接 Toast 提示
DialogService.show({
message: '收藏成功!',
type: 'toast',
duration: 2000
});
this.isStarred = true;
}
}
}
你看,页面代码里完全没有大段的弹窗调用,只有 DialogService.show(config) 这一行。业务逻辑和弹窗逻辑完全解耦,后续无论弹窗样式怎么改(比如把 Toast 改成自定义动画),都只需要改 DialogService.ets 这一处文件。
4.3 ActionSheet 操作菜单
除了上面两种基础弹窗,promptAction 还支持从底部滑出的操作菜单 ActionSheet,适合展示多个并列的操作选项:
// 更多操作 - ActionSheet 示例
promptAction.showActionMenu({
title: '仓库操作',
buttons: [
{ text: '分享', color: '#666666' },
{ text: '举报', color: '#666666' },
{ text: '取消收藏', color: '#FF3B30' }
]
}).then((index) => {
if (index === 0) {
// 执行分享逻辑
} else if (index === 2) {
// 执行取消收藏
}
}).catch((error: Error) => {
console.error('ActionSheet error:', error);
});
ActionSheet 特别适合移动端场景——想象一下微信里点开一篇文章,右上角三个点"…"弹出来的就是这种效果。用户可以快速选择操作,不需要跳转到新页面。
4.4 自定义弹窗 CustomDialog
当内置弹窗实在满足不了需求时,CustomDialog 就是终极武器了。下面的例子展示了一个带输入框的弹窗,用户可以输入自定义的 star 理由:
// StarReasonDialog.ets
@CustomDialog
export struct StarReasonDialog {
controller: CustomDialogController;
@State inputText: string = '';
onConfirm: (reason: string) => void = () => {};
build() {
Column() {
Text('收藏理由(选填)')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.margin({ bottom: 12 })
TextInput({ placeholder: '记录一下为什么收藏这个仓库...' })
.onChange((value: string) => {
this.inputText = value;
})
.margin({ bottom: 20 })
Row() {
Button('取消')
.onClick(() => this.controller.close())
Button('确认收藏')
.onClick(() => {
this.onConfirm(this.inputText);
this.controller.close();
})
}
.justifyContent(FlexAlign.SpaceEvenly)
.width('100%')
}
.padding(24)
.backgroundColor('#FFFFFF')
.borderRadius(16)
}
}
然后在页面中创建并使用:
@State dialogController: CustomDialogController = new CustomDialogController({
builder: StarReasonDialog({
onConfirm: (reason: string) => {
// 将 reason 保存到用户偏好设置
console.info('Star reason:', reason);
}
}),
alignment: DialogAlignment.Center
});
// 触发弹窗
this.dialogController.open();
五、深度技术原理
5.1 AlertDialog 背后的渲染机制
你可能已经发现了,AlertDialog 用起来非常简单——AlertDialog.show({ ... }) 一行代码就弹出来了。但这背后到底发生了什么?
HarmonyOS 的 ArkUI 框架采用了声明式 UI范式,所有 UI 都是通过 @Component 装饰器定义的组件树。当 AlertDialog.show() 被调用时,框架经历了以下几步:
第一步:配置解析。框架把你的配置对象(title、message、confirm 等)解析成一个内部的数据结构。这个数据结构包含了弹窗的布局信息和行为配置。
第二步:动态组件创建。ArkUI 在内存中动态构建了一个 AlertDialog 组件,注入到当前页面的组件树里。注意,这个组件是临时创建的,弹窗关闭后就会被销毁,不会留在组件树上占内存。
第三步:渲染与遮罩层。框架为弹窗创建一个遮罩层(overlay),就是弹窗后面那层半透明的深色背景。这个遮罩层一方面用来聚焦用户注意力,另一方面拦截用户对底层页面的操作,防止误触。
第四步:事件绑定。按钮的点击事件被绑定到回调函数上。用户在弹窗上点击"确认"或"取消"后,对应的回调函数执行,弹窗关闭,遮罩层消失。
整个过程是同步阻塞的——当你调用 AlertDialog.show() 时,页面代码的执行会暂停在那里,等待用户做出选择。这是符合直觉的:你不应该让用户在不选择的情况下继续操作。
5.2 promptAction 的异步机制
和 AlertDialog 不同,promptAction 的 API 是非阻塞、异步返回的。以 showToast 为例:
promptAction.showToast({
message: '收藏成功',
duration: 2000
});
调用这行代码后,程序不会暂停,会立即继续往下执行。Toast 的显示和隐藏完全由 promptAction 在后台自动管理,你不需要手动关闭它。
这背后的原理是:promptAction 是 HarmonyOS 提供的一种全局 UI 服务,它运行在一个独立的上下文中,不绑定任何具体的页面。当你的应用调用 showToast 时,实际上是向这个全局服务发送了一个"显示 Toast"的指令,服务接收到指令后在系统层面展示 Toast,不影响你的应用代码继续执行。
这个设计的好处是:Toast 的生命周期和应用解耦了。即使你的页面被销毁了(用户按了返回键),Toast 仍然能正常显示完那两秒钟,不会因为页面没了就突然消失。
5.3 遮罩层的 Z 轴层级管理
弹窗还有一个值得了解的细节:遮罩层的叠加顺序。
假设你的页面上同时有多个弹窗在"排队"——不太常见,但确实可能发生,比如一个 Toast 正在显示,用户又点了按钮触发了 AlertDialog。这种情况下后出现的弹窗会叠在先出现的上面。
HarmonyOS 的渲染引擎通过 **Z 轴层级(Stacking Context)**来管理这个叠加关系。AlertDialog 和 ActionSheet 的遮罩层默认在 Toast 之上,这是因为 Toast 是"轻提示",设计上就希望它不打扰用户的操作流,而 AlertDialog 需要用户必须做出回应,优先级更高。
如果你自己写了 CustomDialog,默认情况下它也会叠在系统弹窗之上。但你可以通过 alignment: DialogAlignment 来控制弹窗在屏幕上的对齐位置(居中、底部、顶部等),不过这个参数控制的是弹窗内容在遮罩层内的位置,不是遮罩层本身的层级。
六、常见问题解答(FAQ)
Q1:Toast 可以显示图片或者自定义样式吗?
不行。promptAction.showToast 的 message 参数只接受字符串,如果你想显示图片或者富文本,只能用 CustomDialog 自己实现一个"长得像 Toast"的弹窗组件。这个做法的缺点是你需要手动处理自动消失的逻辑(比如用 setTimeout 或者 setInterval)。
Q2:AlertDialog 的按钮最多能放几个?
标准 AlertDialog 支持最多两个按钮——一个主按钮(secondaryButton)和一个次按钮(primaryButton)。如果你需要三个及以上的选项,用 promptAction.showActionSheet 更合适,它支持任意数量的操作按钮。
Q3:弹窗可以出现在状态栏(顶部)吗?
默认情况下,AlertDialog 和 CustomDialog 出现在屏幕中央,ActionSheet 从底部滑出。如果你想做顶部通知栏那种样式,需要自己用 CustomDialog 实现,弹窗的样式完全由你决定,理论上可以放在屏幕任何位置。
Q4:弹窗显示期间如何防止用户操作背后的页面?
AlertDialog 和 ActionSheet 自带遮罩层,会自动拦截背景的触摸事件。但 Toast 没有遮罩层。如果你的 Toast 需要防止用户操作,可以用 promptAction.showLoading() 代替,它会显示一个加载遮罩,既能阻止用户操作,又能告诉用户"系统在处理中"。
Q5:CustomDialog 的控制器需要手动销毁吗?
不需要。CustomDialogController 在 close() 被调用后会自动释放资源。但有一个坑要注意:如果你的 CustomDialog 里有定时器(比如 setInterval 或者 animateTo),在 close() 之前一定要手动清除这些定时器,否则定时器不会停止,会导致内存泄漏或者奇怪的行为。
Q6:弹窗出现的时候会自动获得焦点吗?
会。当 AlertDialog 或者 CustomDialog 弹出时,弹窗及其内部的组件会自动获得焦点,底层的页面内容会被遮罩层遮挡,用户无法点击。这是系统自动处理的,不需要你手动写任何代码。但如果你在 CustomDialog 里放了输入框(TextInput),默认情况下输入框可能会自动弹出键盘——这个行为在部分设备上可能不是你想要的,可以通过 TextInput 的属性或者控制焦点来调整。
Q7:HarmonyOS 和 Android 的弹窗体系有什么主要区别?
从开发体验上来说,HarmonyOS 的弹窗 API 比 Android 原生的 AlertDialog 简洁很多。Android 原生开发中,想显示一个对话框需要写 Builder 模式的一长串链式调用,而 HarmonyOS 的 AlertDialog.show({ ... }) 直接传一个对象就搞定了,代码更紧凑,也更符合声明式 UI 的风格。
七、运行效果
7.1 收藏成功 Toast 效果

7.2 取消收藏确认弹窗效果

7.3 ActionSheet 效果

八、扩展方向
8.1 统一弹窗管理平台
如果你在一个大型团队里工作,不同的开发者可能写出来的弹窗样式五花八门——有的按钮是蓝色,有的按钮是红色,有的标题 20 字号,有的 24 字号,用户体验非常割裂。
一个好的扩展方向是搭建统一的弹窗管理平台。你可以在项目中创建一个 DialogTheme 配置类,统一定义弹窗的颜色、圆角、字体大小、按钮间距等样式属性。然后让所有弹窗组件都继承这个主题配置。这样无论是谁写的弹窗,最终呈现出来的风格都是统一的。
// DialogTheme.ets - 主题配置示例
export const DialogTheme = {
// 遮罩颜色
maskColor: 'rgba(0, 0, 0, 0.5)',
// 弹窗背景色
backgroundColor: '#FFFFFF',
// 圆角
borderRadius: 16,
// 主色调(按钮、文字高亮等)
primaryColor: '#007DFF',
// 危险操作色
dangerColor: '#FF3B30',
// 文字颜色
textColor: '#182431',
// 次要文字颜色
secondaryTextColor: '#99182431',
};
8.2 动画增强
HarmonyOS 原生的 AlertDialog 和 promptAction 弹窗动画比较基础。如果你想让弹窗看起来更丝滑,可以用 ArkUI 的 @AnimatableExtend 或者 animateTo 为自定义弹窗添加入场动画——比如从底部滑入、带有弹性回弹效果,或者 Toast 从半透明淡入。
8.3 弹窗埋点与数据统计
在企业级应用里,弹窗的展示次数、点击率、转化率都是重要的产品数据。你可以在 DialogService 的 show 方法里统一加上埋点逻辑:
// 扩展 DialogService - 添加埋点
static show(config: DialogConfig): void {
// 埋点上报
HiLog.info('Dialog shown: ' + config.type + ' - ' + config.title);
// 调用实际弹窗
// ...原有逻辑
}
这样所有弹窗的展示都会自动被记录下来,不需要每个业务场景单独埋点。
8.4 链式调用与 Promise 化
目前我们的 DialogService.show() 是同步调用、回调处理的。如果你喜欢更现代的 Promise/async-await 风格,也可以把确认类弹窗封装成 Promise 版本:
// 返回 Promise 的确认弹窗
static confirm(config: DialogConfig): Promise<boolean> {
return new Promise((resolve) => {
AlertDialog.show({
title: config.title,
message: config.message,
primaryButton: {
value: config.cancelText ?? '取消',
action: () => resolve(false)
},
secondaryButton: {
value: config.confirmText ?? '确认',
action: () => resolve(true)
}
});
});
}
// 使用方式 - async/await
async handleDelete() {
const confirmed = await DialogService.confirm({
title: '确认删除',
message: '删除后数据无法恢复',
confirmText: '删除',
cancelText: '保留'
});
if (confirmed) {
// 执行删除逻辑
}
}
Promise 化的好处是代码的线性度更好——你不需要在回调里套回调,看起来更像同步代码,但背后仍然是异步执行的。
8.5 弹窗的可访问性(Accessibility)
一个经常被忽略但非常重要的扩展方向是可访问性支持。对于视力障碍用户,弹窗里的内容需要能被屏幕阅读器正确朗读。这意味着:
- 弹窗需要有明确的语义标签。AlertDialog 的标题和内容默认会被屏幕阅读器读取,但如果用 CustomDialog 自定义了布局,就需要手动添加语义属性。
- 按钮需要有可识别的文本。不要只放一个图标就完事,至少要在图标旁边加文字,或者直接用文字按钮。图标按钮应该有一个
aria-label属性描述其功能。 - 焦点管理。弹窗弹出时,焦点应该自动移到弹窗内部第一个可交互的元素上,用户按 Tab 可以在弹窗内的元素之间切换,不要让焦点跑到底层的页面上去。
HarmonyOS 提供了基础的 Accessibility 支持,但在弹窗场景下,很多行为需要开发者手动保证。这些可访问性细节在个人开发的小项目里可能显得多余,但如果你在做面向公众的应用,特别是教育、医疗、政务类的应用,可访问性是必须考虑的因素。一个真正好的弹窗,不只是功能正确,还要让所有人都能用。
好了,弹窗这个话题我们就聊到这里。从生活场景出发,到数据模型设计,到三种弹窗方案的选型,再到代码实现、原理拆解、常见问题和扩展方向,你应该对 HarmonyOS 的弹窗体系有了完整的认识了。
回头看这篇文章的核心线索,其实就两个字:合适。不是 Toast 能做的就都用 Toast,不是 CustomDialog 酷炫就都上 CustomDialog。工具是为场景服务的,场景对了,工具自然就对了。在实际项目中,我见过太多弹窗滥用的情况:用户点一个无关紧要的提示也要弹出确认框,用户删除东西反而只有一个轻飘飘的 Toast 没有二次确认。这些反直觉的设计背后,往往是开发者没有想清楚"这个弹窗到底要达成什么目的"。所以下次写弹窗之前,不妨先停下来问自己两个问题:第一,我想让用户知道什么?第二,我需要用户做什么? 答案清楚了,弹窗的形式自然就清楚了。
记住那个三步选型法:不需要操作用 Toast,标准二选一用 AlertDialog,搞不定就用 CustomDialog。简单优先,按需升级,这不仅是弹窗的选型原则,也是做很多事情的好思路。
下次遇到弹窗相关的需求,别急着复制粘贴,先想想这个弹窗想告诉用户什么、用户需要做什么,再决定用什么工具来呈现。好的交互设计,往往就藏在这些细节里。
更多推荐


所有评论(0)