HarmonyOS 6.1 弹窗与半模态实战:AlertDialog / CustomDialog / bindSheet / bindContentCover 全解
基于 API 12+,四种弹窗模式深度对比 + 踩坑实录,附 DialogDemo 完整代码走读
写在前面
做鸿蒙项目到中期,弹窗这件事开始变得微妙。起初一个 AlertDialog.show() 就够用了,后来产品说要加个输入框,再后来设计师画了个底部弹出的操作面板,再再后来又来个全屏的编辑页——但不想用 Navigation 跳转,说体验太重。
我一开始的思路很简单:弹窗嘛,不就是一个遮罩层加个内容区?自己写个 @Builder 配合 @State 控制显隐不就完了?结果写了两周发现自己造的轮子在转场动画、手势交互、键盘避让上全是 bug,回头一看 ArkUI 已经提供了四种弹窗方案,该用哪个、怎么用、哪里有坑,这篇文章一口气讲清楚。
一、AlertDialog:最省心的确认弹窗
AlertDialog 是四种方案里最简单的。不需要声明控制器,不需要 Builder,一行 AlertDialog.show() 搞定。适合的场景很明确:只需要用户确认或取消,不需要自定义 UI。
DialogDemo 里的用法是这样的:
AlertDialog.show({
title: '提示',
message: '这是一个AlertDialog示例,确认要继续操作吗?',
primaryButton: {
value: '取消',
action: () => {
this.dialogResult = '取消'
this.addLog('AlertDialog: 取消')
}
},
secondaryButton: {
value: '确认',
action: () => {
this.dialogResult = '确认'
this.addLog('AlertDialog: 确认')
}
}
})
这东西的优点是零配置——不需要提前声明,不需要管理生命周期,调用就弹,点完就消失,系统帮你处理一切。缺点也很明显:你没法改布局,没法加输入框,按钮文案和个数都受限。产品要是说"弹窗里加个勾选框",AlertDialog 就直接歇菜了。
有一点值得注意:AlertDialog 在 UIExtensionComponent 里不生效。这不是 bug,是系统限制——UIExtension 的上下文不具备弹出系统级弹窗的能力。如果你在卡片或 Extension 里需要弹窗提示,得走 CustomDialog 或自己画。
另一个细节是按钮顺序。primaryButton 在左侧,secondaryButton 在右侧,这个位置跟 Android 的 AlertDialog 是反的(Android 的 positive button 在右)。团队里安卓转过来的同事刚开始老点错,后来统一了规范才好点。如果你觉得别扭,可以在 UI 规范里注明"左侧取消、右侧确认",至少让团队认知一致。
AlertDialog 的 alignment 参数可以控制弹窗位置,默认居中,设成 DialogAlignment.BOTTOM 可以从底部弹出。但说实话,底部弹出的交互更适合用 bindSheet 来做,AlertDialog 底部弹出的动画和遮罩逻辑都不如 bindSheet 自然。
二、CustomDialog:能写自定义 UI 的弹窗
AlertDialog 搞不定的事情,CustomDialog 基本都能搞定。它的核心思路是:用 @CustomDialog 装饰器声明一个自定义组件,然后在父组件里通过 CustomDialogController 控制它的打开和关闭。
DialogDemo 里的 NameDialog 就是一个典型用法——弹窗里有个输入框让用户填姓名:
@CustomDialog
struct NameDialog {
controller: CustomDialogController
onConfirm: (name: string) => void = (_name: string) => {}
@State inputName: string = ''
build() {
Column() {
Text('请输入姓名')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
TextInput({ placeholder: '姓名', text: this.inputName })
.onChange((value: string) => {
this.inputName = value
})
.width('80%')
.margin({ bottom: 24 })
Row() {
Button('取消')
.onClick(() => {
this.controller.close()
})
.backgroundColor('#999')
.fontColor(Color.White)
Button('确认')
.onClick(() => {
this.onConfirm(this.inputName)
this.controller.close()
})
}
.justifyContent(FlexAlign.SpaceEvenly)
.width('100%')
}
.padding(24)
}
}
然后在父组件 DialogDemo 里这样声明和使用:
private nameDialogController: CustomDialogController = new CustomDialogController({
builder: NameDialog({
onConfirm: (name: string) => {
this.customDialogResult = name
this.addLog('CustomDialog返回: ' + name)
}
})
})
点击按钮打开:
Button('显示自定义弹窗')
.onClick(() => {
this.nameDialogController.open()
this.addLog('CustomDialog打开')
})

看起来很 straightforward,对吧?但我在这上面栽过一个大跟头。
坑:CustomDialogController 在 aboutToDisappear 后调用会崩溃
这个坑真的坑了我一整天。场景是这样的:有个页面 A,里面声明了 CustomDialogController,页面 A 被 Navigation pop 掉之后,某个异步回调还在持有这个 controller 的引用,然后调用了 open() —— 直接崩溃,报错是类似 “CustomDialogController is not valid” 的错误。
原因说起来不复杂:CustomDialogController 的生命周期跟它所在的组件绑定。组件销毁了,controller 就失效了,再调用就崩溃。但问题在于,异步场景下这个时序很难保证。比如你弹出了 CustomDialog,用户在弹窗里点了确认,确认逻辑里发了个网络请求,网络请求回来之后你想再弹一个提示弹窗——但此时组件可能已经被 pop 了。
我的解决方案比较粗暴但有效:在调用 open() 之前检查组件是否还活着。做法是在组件里加一个标志位:
@State private isAlive: boolean = true
aboutToDisappear() {
this.isAlive = false
}
// 弹窗调用处
someAsyncCallback() {
if (!this.isAlive) return
this.nameDialogController.open()
}
还有一个相关的坑:CustomDialogController 必须作为组件的成员变量声明,不能在方法里临时 new 一个。如果你写成:
// 错误写法!
onClick(() => {
let ctrl = new CustomDialogController({ builder: MyDialog() })
ctrl.open()
})
这种写法在某些版本上会直接不显示,或者显示了但关闭后崩溃。原因还是生命周期——局部变量的 controller 在方法执行完就可能被回收,弹窗还没关就找不到了。必须在组件级别声明 controller,这是硬性规则。
关于数据回传,DialogDemo 里用的是回调函数的方式(onConfirm 参数)。另一种方式是用 AppStorage 或 LocalStorage 做中转,但我个人更偏好回调——逻辑更内聚,不用在全局状态里加临时变量。如果你的弹窗需要回传的数据特别复杂(比如一整个表单对象),回调参数直接传对象就行,ArkTS 里不存在序列化的问题。
CustomDialog 还有个 autoCancel 参数,默认 true,点遮罩层能关闭。如果你的弹窗是那种"必须让用户做出选择"的场景(比如同意用户协议),记得设成 false,否则用户一碰遮罩就关了,你啥也没拿到。
三、bindSheet:半模态,这个我用得最多
半模态(Bottom Sheet)在移动端设计里是个高频交互模式:分享面板、操作选择、筛选器、评论输入……几乎每个 App 都有一堆这种场景。HarmonyOS NEXT 提供的 bindSheet 是目前四种弹窗方案里我用得最多的。
DialogDemo 里的 sheetBuilder 展示了一个典型的操作选择面板:
@Builder
sheetBuilder() {
Column() {
Text('选择操作')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.margin({ top: 20, bottom: 12 })
List() {
ForEach(this.sheetItems, (item: SheetItem) => {
ListItem() {
Row() {
Text(item.icon)
.fontSize(24)
.margin({ right: 12 })
Text(item.name)
.fontSize(16)
}
.width('100%')
.padding({ left: 20, right: 20, top: 14, bottom: 14 })
.onClick(() => {
this.sheetResult = item.name
this.isSheetShow = false
this.addLog('Sheet选择: ' + item.name)
})
}
}, (item: SheetItem) => item.name)
}
.divider({ strokeWidth: 1, color: '#eeeeee' })
.width('100%')
}
.width('100%')
}
绑定到组件上:
.bindSheet(this.isSheetShow, this.sheetBuilder, {
showClose: true,
onWillDisappear: () => {
this.addLog('Sheet即将消失')
}
})
打开方式就是改 isSheetShow 为 true:
Button('显示半模态Sheet')
.onClick(() => {
this.isSheetShow = true
this.addLog('Sheet打开')
})
相比 CustomDialog 的 controller 模式,bindSheet 的状态驱动方式更符合声明式 UI 的思路——改个布尔值就弹出/关闭,不需要手动管理 controller 的生命周期,不用担心组件销毁后 controller 失效的问题。就这一点,我就更倾向于用 bindSheet 而不是 CustomDialog。
坑:SheetSize.FIT_CONTENT 在内容未渲染时高度为 0
这个坑非常隐蔽。bindSheet 的 SheetSize 枚举有 MEDIUM(半屏)、LARGE(大半屏)、FIT_CONTENT(自适应内容高度)三个值。FIT_CONTENT 听起来很美好——内容多高 Sheet 就多高,不多不少。但实际用起来你会发现,有时候 Sheet 弹出来就一条缝,高度几乎为 0。
问题出在时序上:FIT_CONTENT 是在 Sheet 弹出的瞬间测量内容高度的。如果你的内容里有异步加载的图片、条件渲染的区域、或者 @State 驱动的列表数据还没填充完,那测量的时候内容就是空的,高度自然就是 0。Sheet 弹出之后数据到了、内容撑开了,但高度不会重新计算——系统不会给你重新走一遍布局测量。
我遇到的具体场景是:Sheet 里放了一个列表,列表数据是从网络请求回来的。请求没到的时候列表为空,Sheet 高度就是 0;请求到了之后列表有了数据,但 Sheet 还是那条缝。
解决思路有两个:
方案 A:不用 FIT_CONTENT,用 MEDIUM 或 LARGE。既然自适应靠不住,那就给个固定比例。MEDIUM 大概是屏幕一半,LARGE 大概是屏幕 90%,大多数场景够用了。这是最省事的做法。
方案 B:数据准备好了再弹 Sheet。把 isSheetShow = true 放在网络请求的回调里,确保数据到了再弹出。这样 FIT_CONTENT 测量的时候内容已经是完整的,高度就正确了。我现在的项目里基本都用这个方案——反正你数据没到弹个空 Sheet 也没意义。
还有一个跟高度相关的细节:bindSheet 有个 detents 参数,可以设置多个高度档位,用户可以上下拖动在不同高度之间切换。如果你之前用 Android 的 BottomSheetBehavior 做过类似功能,你会觉得这个 API 设计得很舒服。但 detents 和 FIT_CONTENT 不要同时用——两者在测量逻辑上会打架,我试过,表现不可预期。
关于拖拽关闭
bindSheet 默认支持往下拖拽关闭,这是半模态交互的标配。但 SheetOptions 里并没有一个 draggable 属性让你控制是否可拖拽——至少在 API 12 这个版本没有。如果你需要禁止用户拖拽关闭(比如弹出的 Sheet 里有个表单,怕用户误触),目前没有官方 API 能做到这一点。
网上有人建议用 onWillDismiss 回调来拦截,但这个回调只能拦截"即将关闭"的行为,不能从根本上禁止拖拽手势。实际效果是:用户拖了,Sheet 跟着手移动了,松手之后 onWillDismiss 被调用,你可以选择不关——但拖拽的视觉反馈已经发生了,体验很割裂。
我的临时方案是把 showClose 设为 false(隐藏右上角关闭按钮),然后在 Sheet 内容里放一个明确的"关闭"或"取消"按钮,引导用户用按钮关闭而不是拖拽。不算完美,但至少不会出现"填了一半表单被误触关闭"的问题。期待后续版本加上 draggable 属性。
bindSheet 的生命周期回调比较丰富:onWillAppear、onAppear、onWillDisappear、onDisappear 四个阶段都有。DialogDemo 里用了 onWillDisappear 来记录日志。实际项目中我主要用 onAppear 来做焦点设置(比如弹出评论输入 Sheet 时自动聚焦输入框),用 onDisappear 来做状态清理。
四、bindContentCover:全屏模态,轻量级"页面跳转"
bindContentCover 是四种方案里最"重"的——它弹出的是全屏内容,视觉上就是一个新页面,但交互上还是弹窗模式。适合的场景是:需要全屏展示的复杂内容,但又不想走 Navigation 路由。
DialogDemo 里的 coverBuilder 做了一个带导航栏的全屏内容页:
@Builder
coverBuilder() {
Column() {
Row() {
Text('< 返回')
.fontSize(18)
.fontColor(Color.White)
.onClick(() => {
this.isCoverShow = false
this.addLog('ContentCover关闭')
})
Blank()
Text('全屏内容')
.fontSize(18)
.fontColor(Color.White)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor('#1976D2')
.alignItems(VerticalAlign.Center)
Scroll() {
Column() {
Text('这是一个全屏模态页面')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.margin({ top: 40, bottom: 16 })
Text('bindContentCover适用于需要全屏展示的复杂内容,\n如详情页、编辑页等场景。')
.fontSize(15)
.fontColor('#666666')
.textAlign(TextAlign.Center)
.margin({ bottom: 32 })
Button('确认选择')
.width('60%')
.onClick(() => {
this.coverResult = '已确认全屏内容'
this.isCoverShow = false
this.addLog('ContentCover确认: 已确认全屏内容')
})
}
.width('100%')
.alignItems(HorizontalAlign.Center)
}
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
绑定方式跟 bindSheet 类似:
.bindContentCover(this.isCoverShow, this.coverBuilder, {
modalTransition: ModalTransition.DEFAULT
})
用 ModalTransition.DEFAULT 会有一个从底部滑上来的转场动画,跟 Navigation 的 push 动画很像。还有 NONE(无动画,直接出现)和 FULL_SCALE(缩放动画)可选。
坑:bindContentCover 里无法直接操作父组件状态
这个坑跟 ArkUI 的组件作用域设计有关。bindContentCover 弹出的内容虽然在同一个组件文件里写的 @Builder,但它运行在独立的组件作用域中。这意味着你在 coverBuilder 里用 this.xxx 访问父组件的 @State 变量时,只能读,不能写。
等等,你可能会说:DialogDemo 里明明写了 this.coverResult = '已确认全屏内容' 和 this.isCoverShow = false,这不就是在改父组件状态吗?确实能改,但这里面有个微妙的地方——能改是因为 coverBuilder 里引用的 this 仍然是父组件的实例。ArkUI 的 @Builder 函数里的 this 指向取决于调用上下文,当 @Builder 被用作 bindContentCover 的参数时,this 仍然指向宿主组件。
但如果你在 coverBuilder 里又嵌套了一个自定义组件,而这个自定义组件想改父组件的状态,那就不行了——它拿不到父组件的 this。这种情况下需要用 @Link 或回调函数来桥接。
实际项目里我踩的具体场景是:coverBuilder 里放了一个编辑表单组件 EditForm,表单提交后需要把编辑结果回传给父组件并关闭 ContentCover。解决方案是在 EditForm 里声明一个 onSave 回调:
@Component
struct EditForm {
@State formData: FormData = new FormData()
onSave: (data: FormData) => void = (_data: FormData) => {}
build() {
// ... 表单UI ...
Button('保存')
.onClick(() => {
this.onSave(this.formData)
})
}
}
然后在 coverBuilder 里:
EditForm({
onSave: (data: FormData) => {
this.editResult = data
this.isCoverShow = false
}
})
这样做虽然多了一层回调包装,但逻辑清晰,也不违背组件间通信的规范。
另一个值得说的点是:bindContentCover 虽然视觉上像页面跳转,但它不走 Navigation 的路由栈。这意味着用户按物理返回键的时候,行为可能跟预期不同——Navigation 栈里的页面会被 pop,而不是关闭 ContentCover。如果你希望返回键能关闭 ContentCover,需要自己监听返回事件。不过话又说回来,如果你需要完整的返回键支持、路由栈管理、跨页面状态共享,那就不应该用 bindContentCover,直接用 Navigation 才是正道。bindContentCover 的定位是"轻量级全屏弹窗",不是"替代 Navigation"。
五、四种弹窗方案横向对比
表格说话:
| 维度 | AlertDialog | CustomDialog | bindSheet | bindContentCover |
|---|---|---|---|---|
| 适用场景 | 简单确认/警告 | 复杂表单/自定义UI | 半屏选择/操作面板 | 全屏详情/编辑页 |
| 自定义程度 | 低(只能改文案和按钮) | 中(自由布局,但尺寸受限) | 高(完整Builder) | 高(完整Builder) |
| 交互模式 | 模态阻塞 | 模态阻塞 | 半模态,可拖拽 | 全屏模态 |
| 生命周期 | 系统管理,无需手动 | 手动管理controller | 手势驱动+回调 | 转场动画+回调 |
| 数据回传 | 按钮action回调 | 回调函数 | 回调函数 | 回调函数 |
| 打开方式 | AlertDialog.show() |
controller.open() |
isShow = true |
isShow = true |
| 关闭方式 | 自动/按钮 | controller.close() |
拖拽/设false | 设false/按钮 |
| 是否阻塞页面 | 是 | 是 | 否(半屏可见下层) | 是(全屏覆盖) |
| 转场动画 | 淡入淡出 | 淡入淡出 | 底部滑入 | 可选三种 |
| 键盘避让 | 系统处理 | 需手动配置 | 系统处理 | 系统处理 |
选型决策其实很简单,我在项目里总结了一个判断流程:
需要弹窗提示?
├─ 只需确认/取消? → AlertDialog
├─ 需要自定义UI?
│ ├─ 占半屏以内? → CustomDialog
│ ├─ 底部操作面板? → bindSheet
│ └─ 全屏内容? → bindContentCover
└─ 不确定? → 优先 bindSheet
为什么"不确定就选 bindSheet"?因为半模态是移动端最通用的交互模式——它不阻塞整个页面,用户能看到上下文,操作成本低,关起来方便。我自己项目里四种弹窗的使用比例大概是 AlertDialog 10%、CustomDialog 15%、bindSheet 55%、bindContentCover 20%。bindSheet 占了大头。
六、DialogDemo 完整代码走读
把 DialogDemo 的代码串一遍,看看四种弹窗是怎么在一个页面里和平共处的。
6.1 状态声明
页面里维护了四种弹窗各自的结果状态,加上一个日志文本和两个控制 bindSheet / bindContentCover 显隐的布尔值:
@State dialogResult: string = '未操作'
@State customDialogResult: string = '未操作'
@State sheetResult: string = '未操作'
@State coverResult: string = '未操作'
@State logText: string = ''
@State activeTab: number = 0
@State isSheetShow: boolean = false
@State isCoverShow: boolean = false
注意 CustomDialog 的 controller 不是 @State 变量,而是 private 成员——因为 CustomDialogController 不是可观察的对象,加了 @State 反而可能引发不必要的重渲染。
6.2 页面布局
整个页面用了 Tabs 分两个 Tab:“实战演示"和"对比指南”。演示 Tab 里依次展示四种弹窗的触发按钮和结果文本,指南 Tab 里是对比表格和踩坑提醒。
build 方法的核心结构:
build() {
Column() {
// 顶部导航栏
Row() { /* ... */ }
.width('100%')
.height(56)
.backgroundColor('#1976D2')
Tabs({ index: this.activeTab }) {
TabContent() { this.demoTab() }
.tabBar('实战演示')
TabContent() { this.guideTab() }
.tabBar('对比指南')
}
.layoutWeight(1)
}
.width('100%')
.height('100%')
.bindSheet(this.isSheetShow, this.sheetBuilder, { /* ... */ })
.bindContentCover(this.isCoverShow, this.coverBuilder, { /* ... */ })
}
bindSheet 和 bindContentCover 都绑在最外层 Column 上——这是推荐的做法,绑在根容器上可以确保弹窗的遮罩层覆盖整个页面。如果绑在某个子组件上,遮罩可能只覆盖那个子组件的区域。
6.3 对比数据
DialogDemo 里用接口定义了对比表的数据结构:
interface CompareItem {
type: string
scenario: string
customLevel: string
interactMode: string
lifecycle: string
}
然后初始化数据:
private compareData: CompareItem[] = [
{ type: 'AlertDialog', scenario: '简单确认/警告', customLevel: '低', interactMode: '模态阻塞', lifecycle: '系统管理' },
{ type: 'CustomDialog', scenario: '复杂表单/自定义UI', customLevel: '中', interactMode: '模态阻塞', lifecycle: '手动管理' },
{ type: 'bindSheet', scenario: '半屏选择/操作面板', customLevel: '高', interactMode: '半模态', lifecycle: '手势驱动' },
{ type: 'bindContentCover', scenario: '全屏详情/编辑页', customLevel: '高', interactMode: '全屏模态', lifecycle: '转场动画' }
]
指南 Tab 里用 ForEach 渲染这个表格,虽然手动写 Column + Row 的表格不如 DataGrid 组件优雅,但 ArkUI 目前没有 DataGrid,这是唯一的方式。
6.4 日志系统
DialogDemo 里有个 addLog 方法,给每种弹窗的操作都打上时间戳日志,方便观察交互时序:
private addLog(msg: string): void {
let now: Date = new Date()
let h: number = now.getHours()
let m: number = now.getMinutes()
let s: number = now.getSeconds()
let time: string = h.toString() + ':' + (m < 10 ? '0' : '') + m.toString() + ':' + (s < 10 ? '0' : '') + s.toString()
this.logText = '[' + time + '] ' + msg + '\n' + this.logText
}
这个日志在调试弹窗生命周期时非常有用——你可以看到弹窗什么时候打开、什么时候关闭、回调在什么时序触发。特别是调试 bindSheet 的 onWillDisappear 时,日志能帮你确认回调的触发时机是在拖拽手势结束之后、还是 Sheet 动画结束之后(答案是前者)。
七、实战经验总结
什么时候该用哪种弹窗
写了一年多鸿蒙项目,我对四种弹窗的偏好已经比较明确了:
AlertDialog 只用于系统级确认——删除数据、退出登录、授权提示。其他场景不用,因为没法定制 UI。
CustomDialog 用于需要自定义布局但不需要太大面积的场景——带输入框的确认弹窗、选择日期的日历弹窗、评分弹窗。但要注意 controller 的生命周期管理,异步场景下容易出问题。
bindSheet 是我的主力选择——操作面板、筛选器、评论输入、图片选择、分享面板,通通用 bindSheet。它的手势交互是系统级的,流畅度比自己实现要好得多。唯一需要注意的是 FIT_CONTENT 的高度测量问题。
bindContentCover 只在"全屏展示但不想走路由"的场景下使用——比如图片预览、临时编辑页、大表单填写。如果你发现 ContentCover 里开始写复杂的生命周期逻辑和路由守卫,那就该考虑换成 Navigation 了。
几个容易忽略的细节
-
CustomDialog 的 controller 必须在组件成员变量里声明,不能在方法里 new。这不是建议,是强制要求。违反了要么不显示,要么崩溃。
-
bindSheet 的 FIT_CONTENT 有渲染时序问题,数据没准备好就弹出来的话高度会是 0。要么用 MEDIUM/LARGE 固定比例,要么等数据到了再弹。
-
bindContentCover 不走 Navigation 路由栈,返回键行为需要自己处理。别把它当页面跳转用。
-
SheetOptions 没有 draggable 属性,没法禁止拖拽关闭。如果业务上需要强制用户操作完才能关闭,目前只能通过 UI 引导(放明确的关闭按钮、隐藏关闭图标)来缓解。
-
AlertDialog 在 UIExtensionComponent 里不生效,Extension 场景下要用 CustomDialog 或其他方案替代。
-
弹窗里的组件树是独立的,尤其是 bindSheet 和 bindContentCover 的 Builder 内容,它们跟父组件之间的状态同步需要通过回调或 @Link 来桥接,不要想当然地认为
this能穿透一切。
我踩过的最离谱的坑
最离谱的一个是 CustomDialogController 崩溃——不是在开发阶段崩溃,是上线之后用户反馈偶现崩溃。复现条件是:用户快速操作,弹窗弹出 → 页面被 pop → 弹窗关闭回调触发 → controller 已经失效 → 崩溃。开发阶段很难复现因为你的操作速度不够快、网络延迟不够大。后来加了 isAlive 标志位才彻底解决。
第二离谱的是 bindSheet 的 FIT_CONTENT 高度为 0。当时我在 Sheet 里放了一个 LazyForEach 列表,数据是页面 aboutToAppear 时请求的。本地开发时网络很快,请求几乎瞬间完成,所以 Sheet 弹出时数据已经有了,高度正常。上了测试环境网络延迟 200ms,Sheet 弹出时数据还没到,高度变成 0。更恶心的是有些数据到了之后 Sheet 高度能自动修正,有些又不能——跟列表项的复杂度有关。最后统一改成了 MEDIUM,问题消失。
八、写在最后
弹窗这种东西,单个看都不复杂,但四种方案各有各的设计理念和 API 风格,混着用的时候容易踩坑。我建议项目里统一规范:哪种场景用哪种弹窗,提前定好,别让不同开发者在同一个项目里各用各的。
DialogDemo 虽然代码量不大,但把四种弹窗的核心用法和坑点都覆盖了。建议你把 Demo 跑起来,每种弹窗都操作几遍,特别关注一下 bindSheet 的拖拽关闭行为和 bindContentCover 的返回键行为——这两个是线上最容易出交互问题的地方。
如果你正在做鸿蒙项目的弹窗模块,希望这篇文章能帮你少踩几个坑。那些坑我替你踩过了,教训就是:别信文档里"自适应高度"这种美好描述,跑起来看实际表现才是真的。
更多推荐

所有评论(0)