Android 刘海屏 DialogFragment 全屏显示适配指南

2,085 阅读2分钟

DialogFragment 全屏显示适配指南

在开发中,我们可能需要用到Dialog全屏展示的场景,这并没有想象中的那么容易,需要考虑:

  1. 顶部状态栏
  2. 底部导航栏
  3. 刘海屏、挖孔屏

期间也在Google搜过一些方案,但是不够系统,且均有瑕疵,本文将依据Android官方文档和代码,逐步探索出可信赖的全屏展示方案。

1. Full-screen dialog guideline

m3.material.io/components/… 中举了一下日历例子,效果如下:

Dialog_Full-Screen-Calendar.gif

下一步就是找到相关的代码,底部有一个入口:

image.png 最终对应的github链接是:github.com/material-co…

github里面并没有上述例子,但是有类似的case:

44_1698824752 (1).gif

我们可以根据material-components的源码实现,来探索全屏展示的方案。

2. 全屏展示Theme配置

1. MaterialDatePicker theme配置

// https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/datepicker/res/values/themes.xml
<style name="ThemeOverlay.Material3.MaterialCalendar.Fullscreen">
  <item name="android:windowIsFloating">false</item>
  <item name="android:windowElevation" tools:ignore="NewApi">@dimen/m3_datepicker_elevation</item>
  <item name="materialCalendarStyle">@style/Widget.Material3.MaterialCalendar.Fullscreen</item>
  <item name="materialCalendarHeaderLayout">@style/Widget.Material3.MaterialCalendar.HeaderLayout.Fullscreen</item>
  <item name="materialCalendarHeaderSelection">@style/Widget.Material3.MaterialCalendar.HeaderSelection.Fullscreen</item>
</style>

不难发现,关键设置项为<item name="android:windowIsFloating">false</item>

2. Demo全屏效果

1. theme设置

    <style name="MyFullScreenDialog" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowIsFloating">false</item>
    </style>

2. dialog 代码

 class MyFullScreenDialog : DialogFragment() {
    ...
    
    override fun getTheme(): Int {
        return R.style.MyFullScreenDialog
    }
 }

image.png

3. 内容延伸到状态栏和导航栏

3.1 MaterialDatePicker代码设置

// https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/datepicker/MaterialDatePicker.java
public final class MaterialDatePicker<S> extends DialogFragment {
  ...
  @Override
  public void onStart() {
    super.onStart();
    Window window = requireDialog().getWindow();
    // Dialogs use a background with an InsetDrawable by default, so we have to replace it.
    if (fullscreen) {
      window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
      window.setBackgroundDrawable(background);
      enableEdgeToEdgeIfNeeded(window);
    } 
    ...
  }
  
  private void enableEdgeToEdgeIfNeeded(Window window) {
      if (edgeToEdgeEnabled) {
        // Avoid enabling edge-to-edge multiple times.
        return;
      }
      final View headerLayout = requireView().findViewById(R.id.fullscreen_header);
      
      // 重点
      EdgeToEdgeUtils.applyEdgeToEdge(window, true, ViewUtils.getBackgroundColor(headerLayout), null);
      ...
    }
}

这里面的edge-to-edge是指将内容显示区域延伸至状态栏导航栏,官方介绍:developer.android.com/develop/ui/…

You can make your app display edge-to-edge—using the entire width and height of the display—by drawing behind the system bars. The system bars are the status bar and the navigation bar.

e2e-intro.gif

关于设置状态栏&导航栏颜色,查看edge-to-edge或者EdgeToEdgeUtils.applyEdgeToEdge源码即可。

3.2 Demo全屏效果(二)

WindowCompat.setDecorFitsSystemWindows(window, false) // 设置全屏
val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()) // 隐藏system bars

image.png

有没有注意到顶部的白条没,这个是因为左上角有个摄像头

image.png

4. 适配挖孔屏、刘海屏

参考官方文档:developer.android.com/develop/ui/…

Before attempting to render your app into the cutout area, ensure your app is configured to display content edge to edge.

这个edge to edge.我们前面已经设置了,先在只需要配置:android:windowLayoutInDisplayCutoutMode即可。

image-20231101153613948.png

5. 完整代码

5.1 完整theme配置

<style name="MyFullScreenDialog" parent="Theme.AppCompat.Light.NoActionBar">
    <item name="android:windowIsFloating">false</item>
    <item name="android:windowLayoutInDisplayCutoutMode" tools:ignore="NewApi">shortEdges</item>
<!--<item name="android:windowFullscreen">true</item>-->
    <item name="android:statusBarColor">@android:color/transparent</item>
</style>

5.2 完整dialog 代码

 class MyFullScreenDialog : DialogFragment() {
    ...
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return super.onCreateDialog(savedInstanceState).also {
            it.window?.let { window ->
                WindowCompat.setDecorFitsSystemWindows(window, false) // 设置全屏
                val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
                windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()) // 隐藏system bars
            }
        }
    }
    
    override fun getTheme(): Int {
        return R.style.MyFullScreenDialog
    }
 }

6. 总结

本文以官方MaterialDatePicker组件作为起点,逐步探索出Dialog全屏展示的方案,能够适配状态栏、导航栏、刘海屏,上述内容均有官方文档作为依据,理论上值得信赖。