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.
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:
And a normal immersive dialog:
These screenshots were taken on an emulator that has navigation bar normally.