DialogFragment api <= 23 requestFeature() must be called before adding content

1,990 阅读1分钟

前情提要

  1. 需求: 显示弹窗的同时隐藏状态栏.

  2. 具体实现: 项目里有一个基类 BaseDialogFragment, 继承自 DialogFragment, 通过重写 BaseDialogFragment.onCreateDialog(), 设置隐藏状态栏

  3. 示例代码:

class NoNavDialogFragment : BaseDialogFragment() {

    /**
     * 全屏
     */
    fun Window?.fullScreen() {
        this ?: return
        setFlags(
            WindowManager.LayoutParams.FLAG_FULLSCREEN,
            WindowManager.LayoutParams.FLAG_FULLSCREEN
        )
        val uiOptions = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                // 隐藏导航栏
                or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
                // 全屏(隐藏状态栏)
                or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                or View.SYSTEM_UI_FLAG_FULLSCREEN
                // 沉浸式
                or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
        decorView.systemUiVisibility = uiOptions
    }

    override fun getLayoutResId(): Int = R.layout.activity_dialog

    override fun getDialog(): Dialog? {
        return AlertDialog.Builder(requireContext()).create()
            .apply {
                val window = window ?: return this
                //这里获取了 decorView 导致了最终的崩溃
                window.decorView.setOnSystemUiVisibilityChangeListener {
                    if (it and View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN == 0) {
                        window.fullScreen()
                    }
                }
                window.setFlags(
                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                )
                setOnShowListener {
                    window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
                    window.fullScreen()
                }
            }
    }

}
  1. 崩溃条件: api <= 23时(只看了23和23以后的, 猜测23以前的也是这样), 重写 DialogFragment.onCreateDialog(), 设置了 style = STYLE_NO_TITLE, 然后获取 decorView 会导致崩溃, 报错: requestFeature() must be called before adding content

api <= 23 崩溃的原因

因为 DialogFragment.onCreateDialog() 创建Dialog后会调用 setupDialog 如果这时候设置了 STYLE_NO_INPUT / STYLE_NO_FRAME / STYLE_NO_TITLE 会去调用 Window.requestWindowFeature(), 然后 PhoneWindow 会判断 PhoneWindow.mContentParent 是否为空, 如果非空, 就会报错崩溃.

而我们如果重写 DialogFragment.onCreateDialog() 且在里面调用了 Window.getDecorView() 会导致 Window.installDecor() 的调用, 从而使 PhoneWindow.mContentParent 非空

避免办法: 不在 DialogFragment.onCreateDialog() 里获取 decorView, 可以在之后的生命周期, 例如 DialogFragment.onViewCreatead, DialogFragment.onActivityCreated

上代码:

    /**
     * DialogFragment
     */
    @Override
    @NonNull
    public LayoutInflater onGetLayoutInflater(@Nullable Bundle savedInstanceState) {
        //... 这个函数会在 onCreateView 调用
        mDialog = onCreateDialog(savedInstanceState);
        setupDialog(mDialog, mStyle);
        //...
    }

    public void setupDialog(@NonNull Dialog dialog, int style) {
        switch (style) {
            case STYLE_NO_INPUT:
                Window window = dialog.getWindow();
                if (window != null) {
                    window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                            | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
                }
                // fall through...
            case STYLE_NO_FRAME:
            case STYLE_NO_TITLE:
                //导致崩溃的语句
                dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
        }
    }

    /**
     * PhoneWindow (api = 23)
     */
    @Override
    public boolean requestFeature(int featureId) {
        if (mContentParent != null) {
            throw new AndroidRuntimeException("requestFeature() must be called before adding content");
        }
        //...
    }

    @Override
    public final View getDecorView() {
        if (mDecor == null) {
            installDecor();
        }
        return mDecor;
    }

    private void installDecor() {
        //...
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
        }
        //...
    }

api > 23 不会崩溃的原因:

新增了一个变量 PhoneWindow.mContentParentExplicitlySet, 即是否显性设置了 ContentView. 当 PhoneWindow.mContentParentExplicitlySet == true 才会报错. 所以这一块的逻辑就和 Activity 类似了, 即调用 Activity.requestWindowFeature() 必须要在 Activity.setContentView() 之前调用

上代码:

    /**
     * PhoneWindow (api > 23)
     */
    @Override
    public boolean requestFeature(int featureId) {
        if (mContentParentExplicitlySet) {
            throw new AndroidRuntimeException("requestFeature() must be called before adding content");
        }
        //...
    }

    @Override
    public void setContentView(int layoutResID) {
        //... 只有手动调用 setContentView 才会置为 true
        mContentParentExplicitlySet = true;
    }