三个对象

业务里写成这样:

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() }
}
inflate、requireView 与 setContentView 的先后 两棵树不要混:
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,子类方法里的 thisCardDialog 实例FragmentManager
Dialog字段 dialog一个独立 Window,根是 DecorViewWindowManager
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。

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。

普通 Fragment 与 DialogFragment 的窗口归属 这份布局稍后由框架放进 Dialog,不塞进 Activity 布局。inflate 当时布局根还没进 `android.R.id.content` 那个坑,parent 还是空的,XML 上的 `layout_width` / `layout_height` 不会立刻变成 `layoutParams`。在这里读布局根尺寸、或按 `layoutParams` 改窗口,对不上预期:窗口宽高和 dim 改的是 DecorView / Window,不是这份 XML。

这也是为什么不用纯 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稍后执行事务,驱动回调
DialogFragmentthis建 theme、窗口;onCreateView 交出布局根
布局根inflate(R.layout.dialog_content)onCreateView 创建,onActivityCreated 才挂进 Dialog 内容区
Dialog字段 dialog先 setContentView 接内容,再 onStart 里 show() / addView
DialogFragment.show 之后的调用时序

DialogFragment 1.2.5 的调用点:

  1. onGetLayoutInflater() 里调用 onCreateDialog(),再 setupDialog()。官方注释:该方法在 onCreate 之后、onCreateView 之前。
  2. onCreateView() 正常由 Fragment 驱动,container 为空;返回值就是这个 Fragment 的视图根。此后 requireView() 可读到它,尚未挂载。
  3. onViewCreated(view) 在 onCreateView 返回之后;参数 view 和 requireView() 是同一棵 Fragment 视图树。
  4. onActivityCreated() 里 mDialog.setContentView(view)(mDialog 就是字段 dialog),把同一份 XML 挂进 Dialog 内容区。
  5. onStart() 里 mDialog.show(),内部走到 WindowManager.addView()。

onCreateDialog 必须早于 onCreateView:框架要先有带 theme 的 Dialog Context,再用它的 LayoutInflater 造内容。show() 只提交事务,真正让窗口对用户可见的是最后一步 Dialog.show()。

每个回调改什么

回调按时间排成一条链。改动按对象落点:窗口属性走 Dialog 回调,R.layout.dialog_content 走 Fragment 视图回调。

DialogFragment 生命周期
回调改什么此时有没有 View
onCreatesetStyle(),定 Dialog theme没有
onCreateDialognew Dialog,gravity、进场动画、是否无标题没有
onCreateViewinflate 布局正在创建布局根
onViewCreatedfindView、绑文案和按钮有布局根,未必有 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无
onStophide():窗口还在,只是看不见。随后 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 哪份 XMLonCreateView
文案、按钮、ViewStubonViewCreated

同一页面上的浮层不一定都是 DialogFragment:

形态载体
居中模态弹窗DialogFragment
贴在当前窗口的半屏面板PopupWindow
轻提示Toast
独立一页的 Dialog 外观Dialog 主题 Activity

打开类时按继承判断:: DialogFragment() 走上面这套回调;PopupWindow 没有 onCreateView;Dialog 主题 Activity 按 Activity 生命周期看。配置变化和异步展示的竞态见 BadTokenException:Dialog异步显示与Activity生命周期竞态。


相关笔记