有没有办法在 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。如果您在 Activity
或 Fragment
的 onCreate(...)
中注册它,您的日期选择器片段(以及对话框本身)将是最新的。
所以,事不宜迟,经过大量实验和不同设置的调整后,以下解决方案可能会满足您的需求(这使用 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.
我正在开发一个使用了 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。如果您在 Activity
或 Fragment
的 onCreate(...)
中注册它,您的日期选择器片段(以及对话框本身)将是最新的。
所以,事不宜迟,经过大量实验和不同设置的调整后,以下解决方案可能会满足您的需求(这使用 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.