前情提要
-
需求: 显示弹窗的同时隐藏状态栏.
-
具体实现: 项目里有一个基类
BaseDialogFragment, 继承自DialogFragment, 通过重写BaseDialogFragment.onCreateDialog(), 设置隐藏状态栏 -
示例代码:
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()
}
}
}
}
- 崩溃条件: 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;
}