有没有办法在 MaterialDatePicker 上启用沉浸式模式?

Is there a way to enable immersive mode on MaterialDatePicker?

我正在开发一个使用了 MaterialDatePicker 的应用程序。

Material DatePicker Fragment from app

整个应用程序是全屏的(启用沉浸式模式 - 隐藏状态和导航栏),我也希望在 DatePicker 对话框中使用它。我尝试了多种建议,但没有任何效果。有办法实现吗?

更新:

到目前为止我尝试过的:

    val datePickerBuilder = MaterialDatePicker.Builder.dateRangePicker()

    datePickerBuilder.apply {
      setTitleText("SELECT A DATE")
      setTheme(R.style.MaterialCalendarTheme)
      setSelection(
        Pair(
          startDate.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli(),
          endDate.atStartOfDay(ZoneOffset.UTC).toInstant().toEpochMilli(),
        )
      )
    }


    val dp = datePickerBuilder.build()

    dp.dialog?.apply {
      window?.setFlags(
        WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
        WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
      )
      window?.decorView?.setSystemUiVisibility(dp.requireActivity().window.decorView.getSystemUiVisibility())
      setOnShowListener {
        dp.dialog?.window?.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)

        val wm = dp.requireActivity().getSystemService(Context.WINDOW_SERVICE) as WindowManager
        wm.updateViewLayout(dp.dialog?.window?.decorView, dp.dialog?.window?.attributes)
      }
    }

第二个 apply 运算符中的代码 snnipet 适用于我构建的其他自定义 DialogFragmets。

尝试上述建议后,我发现 MaterialDatePicker 的 onCreateDialog 方法是最终方法,因此无法覆盖。

你的方法的问题是 dp.dialog 在那个时候总是 null 因为它只在初始化 DialogFragment 时由 FragmentManager 创建MaterialDatePicker,因此 UI 可见性更改代码永远不会执行。如果您同步显示对话框,它可能非空:

dp.showNow(supportFragmentManager, null)
dp.dialog?.apply {
   // anything here gets executed as the dp.dialog is not null
}

但是,此方法的问题是此代码再也不会被调用。如果重建对话框(例如旋转设备),则不再执行此块内的任何内容,这将导致非全屏对话框。

现在,MaterialDatePicker 有一个基本问题:它是 final,所以 none 对话框创建者/处理程序方法可以被覆盖,这在其他情况下会起作用。

幸好有个叫FragmentLifecycleCallbacks that you can use to listen to the (surprise-surprise) fragment lifecycle events. You can use this to catch the moment where the dialog is built which is after the view is created (callback: onFragmentViewCreated的class。如果您在 ActivityFragmentonCreate(...) 中注册它,您的日期选择器片段(以及对话框本身)将是最新的。

所以,事不宜迟,经过大量实验和不同设置的调整后,以下解决方案可能会满足您的需求(这使用 Activity)。

The sample project is available on GitHub.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    // ... setContentView(...), etc.

    registerDatePickerFragmentCallbacks()
}

// need to call this every time the Activity / Fragment is (re-)created
private fun registerDatePickerFragmentCallbacks() {
    val setFocusFlags = fun(dialog: Dialog, setUiFlags: Boolean) {
        dialog.window?.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)

        if (setUiFlags) {
            dialog.window?.decorView?.setOnSystemUiVisibilityChangeListener { visibility ->
                // after config change (e.g. rotate) the system UI might not be fullscreen
                // this ensures that the UI is updated in case of this
                if (visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) {
                    hideSystemUI(dialog.window?.decorView)
                    hideSystemUI() // this might not be needed
                }
            }

            hideSystemUI(dialog.window?.decorView)
            hideSystemUI() // this might not be needed
        }
    }

    // inline fun that clears the FLAG_NOT_FOCUSABLE flag from the dialog's window
    val clearFocusFlags = fun(dialog: Dialog) {
        dialog.window?.apply {
            clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)

            decorView.also {
                if (it.isAttachedToWindow) {
                    windowManager.updateViewLayout(it, attributes)
                }
            }
        }
    }

    supportFragmentManager.registerFragmentLifecycleCallbacks(object :
        FragmentManager.FragmentLifecycleCallbacks() {

        override fun onFragmentViewCreated(
            fm: FragmentManager,
            f: Fragment,
            v: View,
            savedInstanceState: Bundle?
        ) {
            // apply this to MaterialDatePickers only
            if (f is MaterialDatePicker<*>) {
                f.requireDialog().apply {
                    setFocusFlags(this, true)

                    setOnShowListener {
                        clearFocusFlags(this)
                    }
                }
            }
        }
    }, false)
}


override fun onResume() {
    super.onResume()
    // helps with small quirks that could happen when the Activity is returning to a resumed state
    hideSystemUI()
}

// this is probably already in your class
override fun onWindowFocusChanged(hasFocus: Boolean) {
    super.onWindowFocusChanged(hasFocus)

    if (hasFocus) hideSystemUI()
}

private fun hideSystemUI() {
    hideSystemUI(window.decorView)
}

// this is where you apply the full screen and other system UI flags
private fun hideSystemUI(view: View?) {
    view?.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
            or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
            or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
            or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
            or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
            or View.SYSTEM_UI_FLAG_FULLSCREEN)
}

要让您的 MaterialDatePicker 使用沉浸式模式,您需要以下任一样式。第一个显示普通对话框,而第二个使用全屏对话框,这会禁用打开对话框时通常发生的背景变暗,并确保对话框以全屏显示window.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- Normal dialog -->
    <style name="MyCalendar" parent="ThemeOverlay.MaterialComponents.MaterialCalendar">
        <!-- you can use ?attr/colorSurface to remove any blinking happening during re-creation of the dialog -->
        <item name="android:navigationBarColor">?attr/colorPrimary</item>

        <!-- or use translucent navigation bars -->
        <!--<item name="android:windowTranslucentNavigation">true</item>-->

        <item name="android:immersive">true</item>
        <item name="android:windowFullscreen">true</item>
        <item name="android:windowIsTranslucent">false</item>
    </style>

    <-- Fullscreen dialog -->
    <style name="MyCalendar.Fullscreen" parent="ThemeOverlay.MaterialComponents.MaterialCalendar.Fullscreen">
        <item name="android:windowIsFloating">false</item>
        <item name="android:backgroundDimEnabled">false</item>

        <!-- you can use ?attr/colorSurface to remove any blinking happening during re-creation of the dialog -->
        <item name="android:navigationBarColor">?attr/colorPrimary</item>

        <!-- or use translucent navigation bars -->
        <!--<item name="android:windowTranslucentNavigation">true</item>-->

        <item name="android:immersive">true</item>
        <item name="android:windowFullscreen">true</item>
        <item name="android:windowIsTranslucent">false</item>
    </style>
</resources>

构建对话框时,只需将此样式作为对话框的主题传递即可:

val datePickerBuilder = MaterialDatePicker.Builder.dateRangePicker()

// for a normal dialog
datePickerBuilder.setTheme(R.style.MyCalendar)

// for a fullscreen dialog
datePickerBuilder.setTheme(R.style.MyCalendar_Fullscreen)

这是全屏沉浸式对话框的样子:

还有一个普通的沉浸式对话:

These screenshots were taken on an emulator that has navigation bar normally.