三个对象
业务里写成这样:
open class CardDialog : DialogFragment()dialog.show(supportFragmentManager, null)日常把这两行叫「弹窗」。打开类看到的是 Fragment 子类,show() 交给的是 FragmentManager,用户看到的那块 UI 却不在 Activity 的布局里。
入口那一行的 dialog 是 CardDialog 实例。进到 CardDialog 的方法里,它就是 this。this 的类型是 DialogFragment,也就是 Fragment;它不是窗口。窗口是框架稍后写进 字段 dialog 的那个 android.app.Dialog。
用户看到的那块 UI 来自 onCreateView 里 inflate 的 R.layout.dialog_content。CardDialog 只是例子类名,没有单独的卡片控件。这份 XML 就是 Dialog 的内容布局,不是窗口。inflate(...) 的返回值就是这份布局的根,之后 requireView() 拿到的也是它。没有「返回值」和「根节点」两层。
这里的 XML 就是 Dialog 的内容布局,例如 R.layout.dialog_content,不是另有一份「业务布局」。它最外层那个 LinearLayout / ConstraintLayout,和普通 Fragment inflate 出来的是同一种 View。不会因为是 Dialog 就多一层「Dialog 根」,也不会变成窗口本身。
变的是这棵树挂进哪扇窗口的哪个坑。每扇 Window 都有自己的壳和内容坑:壳是 DecorView,坑是壳里的 android.R.id.content。R.layout.dialog_content 永远是后来塞进坑里的 child,不是壳本身。窗口宽高、dim 属于 Window / DecorView,不在这份 XML 上。
普通 Fragment
Activity 窗口
└─ DecorView
└─ android.R.id.content
└─ Activity 的 setContentView
└─ 某个 FrameLayout container
└─ inflate(页面 XML) 的返回值
DialogFragment
Dialog 窗口(另一扇独立 Window)
└─ DecorView
└─ android.R.id.content
└─ inflate(R.layout.dialog_content) 的返回值所以差别只在挂到哪:普通 Fragment 挂进 Activity 窗口里某个 container;DialogFragment 挂进 Dialog 窗口的内容区。onCreateView 的 container 因此是 null,要等框架 setContentView 才真正 add 上去。
requireView() 读的是 这个 Fragment 自己的视图树,定义在 Fragment 上,和普通 Fragment 同一个方法。onCreateView 返回什么,Fragment 就存什么;getView() / requireView() 之后读的就是这份。DialogFragment 没有另一套「Dialog 专用 requireView」。
它不去 inflate。inflate 发生在 onCreateView;业务代码在 onViewCreated 里自己调 requireView()(或直接用参数 view),读已经存下来的布局根。方法是 final,不要重写。框架挂载时用的是 getView(),不会去调 requireView()。
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.dialog_content, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// view 和 requireView() 是同一棵 Fragment 视图树
view.findViewById<Button>(R.id.ok).setOnClickListener { dismiss() }
}Fragment 的视图树 ← requireView() / onViewCreated 的 view
└─ inflate 的布局根
└─ 按钮、文案
Dialog 窗口的视图树
└─ DecorView
└─ android.R.id.content ← 坑,不是 requireView()
└─ 上面那棵 Fragment 视图(setContentView 之后才挂进来)三步是同一棵 Fragment 视图树:onCreateView inflate 出 R.layout.dialog_content;之后 requireView() 就能读到它,此时还没有 parent;onActivityCreated 里 setContentView(getView()) 才把它加成 android.R.id.content 的 child。挂到 Dialog 上不改变它是谁的 View。
这里叠了这三个对象:
| 对象 | 代码里是谁 | 它是什么 | 谁管它 |
|---|---|---|---|
| Fragment 自身 | 入口变量 dialog,子类方法里的 this | CardDialog 实例 | FragmentManager |
| Dialog | 字段 dialog | 一个独立 Window,根是 DecorView | WindowManager |
| Fragment 的视图树 | inflate(R.layout.dialog_content) 的返回值 = requireView() | Dialog 内容布局的根,不是 DecorView | 先由 Fragment 持有,后挂进 Dialog 内容区 |
DialogFragment 不是第四个对象,它就是 this:一个用 Fragment 生命周期去管 Dialog 的 Fragment。布局根创建时还不在窗口里;Fragment 先拿着它,等 setContentView 才把它加成 Dialog 内容区的 child。
官方把显示、隐藏、关闭都交给 Fragment API,不要直接操作 Dialog,见 Display dialogs with DialogFragment。
图把上面三个对象拆到两扇窗口里。左边是宿主关系:this 作为 Fragment 已经挂到 Activity 上(onAttach)。右边是窗口关系:inflate 返回的布局根要等到 setContentView 才成为 Dialog 内容区的 child。缺的是这次 add,不是左边的 onAttach。这份布局根不是 Dialog 的 DecorView。
依据是 androidx.fragment:fragment:1.2.5 与平台 Dialog / LayoutInflater。较新 AndroidX 可能把 setContentView 从 onActivityCreated 挪走,但这条先后关系不变。
内容挂在哪一个窗口
布局根不进 Activity 的 container,所以 onCreateView 的 container 参数是 null。
这也是为什么不用纯 new Dialog(context):Dialog 不管配置变化,没有 FragmentManager 恢复,生命周期要自己对齐。DialogFragment 用 Fragment 把窗口绑到宿主 Activity 上。代价是同时出现两套回调:管窗口壳的走 Dialog,管 R.layout.dialog_content 的走 Fragment 视图回调。Dialog 自身的显示、关闭和监听回调见 Dialog。
生命周期
show(fm, tag) 默认 commit() 入队。返回时窗口还不存在。生命周期由 FragmentManager 稍后 execPendingActions() 驱动。
show() 之后窗口才对用户可见。三个对象都还在,另外加上提交事务的 Activity 和稍后执行事务的 FragmentManager:
| 角色 | 对应前面 | 职责 |
|---|---|---|
| Activity | 调用方 | 调用 show(),提交事务 |
| FragmentManager | 管 this | 稍后执行事务,驱动回调 |
| DialogFragment | this | 建 theme、窗口;onCreateView 交出布局根 |
| 布局根 | inflate(R.layout.dialog_content) | onCreateView 创建,onActivityCreated 才挂进 Dialog 内容区 |
| Dialog | 字段 dialog | 先 setContentView 接内容,再 onStart 里 show() / addView |
DialogFragment 1.2.5 的调用点:
onGetLayoutInflater()里调用onCreateDialog(),再setupDialog()。官方注释:该方法在onCreate之后、onCreateView之前。onCreateView()正常由 Fragment 驱动,container为空;返回值就是这个 Fragment 的视图根。此后requireView()可读到它,尚未挂载。onViewCreated(view)在onCreateView返回之后;参数view和requireView()是同一棵 Fragment 视图树。onActivityCreated()里mDialog.setContentView(view)(mDialog就是字段dialog),把同一份 XML 挂进 Dialog 内容区。onStart()里mDialog.show(),内部走到WindowManager.addView()。
onCreateDialog 必须早于 onCreateView:框架要先有带 theme 的 Dialog Context,再用它的 LayoutInflater 造内容。show() 只提交事务,真正让窗口对用户可见的是最后一步 Dialog.show()。
每个回调改什么
回调按时间排成一条链。改动按对象落点:窗口属性走 Dialog 回调,R.layout.dialog_content 走 Fragment 视图回调。
| 回调 | 改什么 | 此时有没有 View |
|---|---|---|
onCreate | setStyle(),定 Dialog theme | 没有 |
onCreateDialog | new Dialog,gravity、进场动画、是否无标题 | 没有 |
onCreateView | inflate 布局 | 正在创建布局根 |
onViewCreated | findView、绑文案和按钮 | 有布局根,未必有 parent |
onActivityCreated | 框架 setContentView,业务尽量不覆写 | 布局根挂到 Dialog 内容区 |
onStart | 窗口宽高、dim、系统蒙层 | 已 setContentView,即将 Dialog.show() |
setStyle() 必须放在 onCreate。官方要求:Dialog 创建后再调没有效果。
onActivityCreated 是 这个 Fragment 的回调,不是「宿主 Activity 刚创建完」。Activity 可能早已 onResume;弹窗是后来 show() 的,这个 Fragment 的 onActivityCreated 也是后跑的。AndroidX 已把它标成 deprecated,业务改用 onViewCreated / onCreate。1.2.5 仍在这里 setContentView,读框架源码时还得认这个名字。
关闭与销毁
关闭同样分两层。dismiss() 当场摘掉 Dialog 窗口,并在主线程上同步调用 onDismiss();Fragment 的移除是 commit() 出去的事务,onDestroyView → onDestroy → onDetach 要等这个事务执行。所以 onDismiss 早于 onDetach,中间默认隔着一条主线程消息。窗口怎么 show / hide / dismiss / cancel,以及 listener 为什么是消息,见 Dialog。
依据是 androidx.fragment:fragment:1.2.5。较新 AndroidX 另有 dismissNow(),用 commitNow() 同步完成移除。
dismiss() 的两段
// DialogFragment.dismissInternal,1.2.5,节选
mDialog.setOnDismissListener(null);
mDialog.dismiss();
if (!fromOnDismiss) {
if (Looper.myLooper() == mHandler.getLooper()) {
onDismiss(mDialog);
} else {
mHandler.post(mDismissRunnable);
}
}
...
FragmentTransaction ft = getParentFragmentManager().beginTransaction();
ft.remove(this);
ft.commit();第一段是窗口。DialogFragment 自己给 Dialog 设了 OnDismissListener,但这里先摘掉 listener,再手动调用 onDismiss()。平台 dismiss() 默认还会再投一条 dismiss 消息;源码注释写明不等那条消息,保证回调早于 onDestroy。非主线程调用时,onDismiss() 被 post 到主线程。
第二段是 Fragment。commit() 只把 remove 放进队列,事务在之后的主线程消息里执行,才走到 onDestroyView、onDestroy、onDetach。
各回调对应的 Dialog 动作
| Fragment 回调 | Dialog 一侧 |
|---|---|
onPause | 无 |
onStop | hide():窗口还在,只是看不见。随后 isShowing() 为 false,见 Dialog |
onDestroyView | 窗口还在则 dismiss();已经关过则无操作。尚未 dismiss 过时补一次 onDismiss(),随后 mDialog = null |
onDestroy | 无 |
onDetach | 无,只更新内部的 mDismissed 标记 |
到 onDetach 时窗口早已移除(dismiss() 路径上是调用当场,销毁路径上是 onDestroyView)。这一步只剩 Fragment 脱离宿主。Activity 销毁同样会走到 onDetach,不要把它当成「用户关掉了弹窗」。
三种关闭来源
| 触发 | 回调顺序 |
|---|---|
代码调用 dismiss() | Dialog.dismiss() → onDismiss()(同步)→ 提交 remove 事务 → 稍后 onDestroyView → onDestroy → onDetach |
| 返回键、点击窗口外部 | Dialog 走 cancel(),窗口当场移除;onCancel()、onDismiss() 作为两条消息依次到达,见 Dialog。onDismiss() 里才提交 remove 事务 → 稍后 onDestroyView → onDestroy → onDetach |
| Activity 销毁、配置变化 | 没有业务侧 dismiss():FragmentManager.dispatchDestroy() → onDestroyView(补 Dialog.dismiss() / onDismiss())→ onDestroy → onDetach |
改动落点
先判断改的是窗口还是内容。窗口对应字段 dialog(DecorView 那一层),内容对应这个 Fragment 的视图树(requireView()):
| 要改的东西 | 放哪 |
|---|---|
| theme、点击外部是否关闭 | onCreate + setStyle |
| gravity、进场动画 | onCreateDialog |
| 窗口宽高、dim、系统蒙层 | onStart |
| inflate 哪份 XML | onCreateView |
| 文案、按钮、ViewStub | onViewCreated |
同一页面上的浮层不一定都是 DialogFragment:
| 形态 | 载体 |
|---|---|
| 居中模态弹窗 | DialogFragment |
| 贴在当前窗口的半屏面板 | PopupWindow |
| 轻提示 | Toast |
| 独立一页的 Dialog 外观 | Dialog 主题 Activity |
打开类时按继承判断:: DialogFragment() 走上面这套回调;PopupWindow 没有 onCreateView;Dialog 主题 Activity 按 Activity 生命周期看。配置变化和异步展示的竞态见 BadTokenException:Dialog异步显示与Activity生命周期竞态。
相关笔记
- Dialog
- BadTokenException:Dialog异步显示与Activity生命周期竞态
- Fragment生命周期
- Fragment
- Activity生命周期
- 梳理Activity、Fragment、View、自定义View的区别与关系
- onSaveInstanceState
- Activityfinish()
评论