0

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.

Gergely Kőrössy
  • 5,620
  • 3
  • 28
  • 44
  • Could you share what you tried so far? – Gergely Kőrössy Jan 14 '21 at 14:02
  • @GergelyKőrössy The question is updated – Răzvan Dănilă Jan 14 '21 at 14:35
  • The concept is absurd, because `Dialog` is not supposed cover the whole screen. – Martin Zeitler Jan 14 '21 at 14:47
  • I want this screen to be consistent with the other ones. Enabling immersive mode on all screens, it just looks weird seeing the navigation and status bars on this one – Răzvan Dănilă Jan 14 '21 at 14:57
  • @RăzvanDănilă I think I have found a solution but there are two quirks: 1. If the user uses text input instead of picking the date, the bottom bar gets shown (without the buttons for now) and that cannot be disabled as the keyboard is in focus. 2. If the user leaves the app with the recents button and returns to it while the dialog is displayed, the bottom bar is shown for a brief period when the dialog is closed. Do you want me to try fixing this or it's good enough for you? – Gergely Kőrössy Jan 15 '21 at 10:26
  • @GergelyKőrössy Thank you for your time. I'll apreciate if you could share what you've found so far, I'll continue from that. But if you're willing to help me further, I'll be very grateful – Răzvan Dănilă Jan 15 '21 at 12:53
  • @RăzvanDănilă I think I have finally figured out everything. I'll post a solution soon. – Gergely Kőrössy Jan 16 '21 at 00:43
  • @GergelyKőrössy Great, thanks a lot! – Răzvan Dănilă Jan 16 '21 at 20:34

1 Answers1

0

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.

Gergely Kőrössy
  • 5,620
  • 3
  • 28
  • 44