Search code examples
androidmaterial-uimaterial-components-androidmaterialdatepicker

Is there a way to enable immersive mode on MaterialDatePicker?


I'm developing an app in which MaterialDatePicker is used.

Material DatePicker Fragment from app

The entire application is fullscreen (immersive mode enabled - status and navigation bars are hidden) and I also want this in the DatePicker dialog. I've tried multiple suggestions but nothing worked. Is there a way to achieve this?

UPDATE:

What I've tried so far:

    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)
      }
    }

The code snnipet from inside the second apply opperator works on other custom DialogFragmets that I've built.

After trying the above suggestion I've seen that the onCreateDialog method from MaterialDatePicker is final so an override is not possible.


Solution

  • The problem with your approach is that dp.dialog is always null at that point as it only gets created by the FragmentManager when it initializes the DialogFragment of the MaterialDatePicker, thus the UI visibility changing code never gets executed. It could be non-null if you showed the dialog synchronously:

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

    However, the problem with this approach is that this code never gets called anymore. If the dialog is rebuilt (e.g. the device is rotated), nothing inside this block is executed anymore which would result in a non-fullscreen dialog.

    Now, there is a fundamental problem with MaterialDatePicker: it's final so none of the dialog creator / handler methods can be overridden which would work in other cases.

    Fortunately there is a class called 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). If you register this in your Activity's or Fragment's onCreate(...), your date picker fragment (and hence the dialog itself) will be up-to-date.

    So, without further ado, after a lot of experiments and tweaks with the different settings, the following solution might satisfy your needs (this uses an 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)
    }
    

    To make your MaterialDatePicker use the immersive mode, you need either of the following styles. The first displays a normal dialog while the second uses the full screen one, which disables the background dimming that usually happens when you open a dialog and makes sure that the dialog is displayed in a full screen 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>
    

    When you're building the dialog, just pass this style as the theme for the dialog:

    val datePickerBuilder = MaterialDatePicker.Builder.dateRangePicker()
    
    // for a normal dialog
    datePickerBuilder.setTheme(R.style.MyCalendar)
    
    // for a fullscreen dialog
    datePickerBuilder.setTheme(R.style.MyCalendar_Fullscreen)
    

    This is how the fullscreen immersive dialog looks like:

    enter image description here

    And a normal immersive dialog:

    enter image description here

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