沉浸式显示广泛的应用于大部分App中,基本上可以说是App的必备功能,在之前的文章Android 全屏显示和沉浸式显示中介绍过如何通过WindowInsetsControllerCompat
实现沉浸式显示。当然,我也将其应用到了我司的App中。但是在后续开发测试的过程中发现了一个奇怪的现象,加载弹窗显示时,已经隐藏的底部导航栏又显示出来了。
问题复现
下面通过一段示例代码演示一下:
- 加载弹窗
kotlin
class LoadingDialogFragment : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.LoadingDialog)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
dialog?.let { containerDialog ->
containerDialog.window?.run {
setBackgroundDrawable(ContextCompat.getDrawable(requireContext(), android.R.color.transparent))
decorView.setBackgroundResource(android.R.color.transparent)
val layoutParams = attributes
layoutParams.width = DensityUtil.dp2Px(200)
layoutParams.height = DensityUtil.dp2Px(120)
layoutParams.gravity = Gravity.CENTER
attributes = layoutParams
}
containerDialog.setCancelable(true)
containerDialog.setCanceledOnTouchOutside(false)
}
return LayoutLoadingDialogBinding.inflate(layoutInflater, container, false).root
}
}
- 示例页面
kotlin
class DialogFragmentExampleActivity : AppCompatActivity() {
val DIALOG_TYPE_LOADING = "loadingDialog"
private lateinit var insetsController: WindowInsetsControllerCompat
private var alreadyChanged = false
private var callDismissDialogTime = 0L
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = LayoutDialogFragmentExampleActivityBinding.inflate(layoutInflater).also {
setContentView(it.root)
}
// 调整系统栏
WindowCompat.setDecorFitsSystemWindows(window, false)
insetsController = WindowCompat.getInsetsController(window, window.decorView).also {
it.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
it.hide(WindowInsetsCompat.Type.navigationBars())
}
window.statusBarColor = ContextCompat.getColor(this, android.R.color.transparent)
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets ->
if (!alreadyChanged) {
alreadyChanged = true
windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()).run {
binding.topView.updateLayoutParams<ConstraintLayout.LayoutParams> { height += top }
binding.tvTitle.updateLayoutParams<ConstraintLayout.LayoutParams> { topMargin += top }
}
}
WindowInsetsCompat.CONSUMED
}
binding.btnShowLoadingDialog.setOnClickListener {
showLoadingDialog()
}
}
private fun showLoadingDialog() {
LoadingDialogFragment().run {
show(supportFragmentManager, DIALOG_TYPE_LOADING)
}
// 模拟耗时操作,两秒后关闭弹窗
lifecycleScope.launch(Dispatchers.IO) {
delay(2000)
dismissLoadingDialog()
}
}
private fun dismissLoadingDialog() {
callDismissDialogTime = System.currentTimeMillis()
lifecycleScope.launch(Dispatchers.IO) {
if (async { checkLoadingDialogStatue() }.await()) {
withContext(Dispatchers.Main) {
// 从supportFragmentManager中获取加载弹窗,并调用隐藏方法
(supportFragmentManager.findFragmentByTag(DIALOG_TYPE_LOADING) as? DialogFragment)?.run {
if (dialog?.isShowing == true) {
dismissAllowingStateLoss()
}
}
}
}
}
}
/**
* 检查加载弹窗的状态直到获取到加载弹窗或者超过时间
*/
private suspend fun checkLoadingDialogStatue(): Boolean {
return if (supportFragmentManager.findFragmentByTag(DIALOG_TYPE_LOADING) == null && System.currentTimeMillis() - callDismissDialogTime < 1500L) {
delay(100)
checkLoadingDialogStatue()
} else {
true
}
}
}
效果如图:
解决显示异常问题
上述示例代码中,在示例页面的初始化方法中通过WindowInsetsControllerCompat
对页面的Window
和decorView
进行操作,隐藏了导航栏。但是在DialogFragment
中,Dialog
对象也有其所属的Window
和decorView
,上述示例代码中并没有针对Dialog
所属的Window
和decorView
进行配置。
基于上面的分析,对示例代码进行调整,调整如下:
- 加载弹窗
kotlin
class LoadingDialogFragment : DialogFragment() {
......
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
dialog?.let { containerDialog ->
containerDialog.window?.run {
WindowCompat.setDecorFitsSystemWindows(this, false)
WindowCompat.getInsetsController(this, decorView).also {
it.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
it.hide(WindowInsetsCompat.Type.navigationBars())
}
......
}
......
}
return LayoutLoadingDialogBinding.inflate(layoutInflater, container, false).root
}
}
修改后效果如图:
适配不同机型
通常来说,上述示例代码用的是官方的API,应该不会出现什么意外,然而还是出现了意外。公司的另一台三星的测试机跟我自己的测试机Pixel 3a XL效果差别很大。
三星测试机(SM-A515F)效果如下:
未调整 | 调整后 |
---|---|
虽然这可能是安卓的通病,但对于这种情况我还是感到有些遗憾,通用API在不同厂商的手机上效果居然差这么多。虽然遗憾,但还是得解决问题。
根据效果图来看,对页面的配置生效了,对Dialog
的配置也生效了,但是DialogFragment
隐藏后重置了对页面的配置。最简单的处理就是在DialogFragment
消失之后判断下导航栏是否显示,显示则隐藏。
调整代码如下:
- 加载弹窗
kotlin
class LoadingDialogFragment : DialogFragment() {
......
override fun onDestroyView() {
super.onDestroyView()
// 这里通过setFragmentResult API 来传递弹窗已经关闭的消息。
parentFragmentManager.setFragmentResult(DialogFragmentExampleActivity::class.java.simpleName, Bundle())
}
}
- 示例页面
kotlin
class DialogFragmentExampleActivity : AppCompatActivity() {
......
private var navigationBarShow = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = LayoutDialogFragmentExampleActivityBinding.inflate(layoutInflater).also {
setContentView(it.root)
}
supportFragmentManager.setFragmentResultListener(this::class.java.simpleName, this) { requestKey, result ->
// 接收加载弹窗关闭的消息
if (requestKey == this::class.java.simpleName) {
if (navigationBarShow) {
// 根据实践,不延迟500毫秒有概率出现无法隐藏的情况。
lifecycleScope.launch(Dispatchers.IO) {
delay(500L)
withContext(Dispatchers.Main) {
hideNavigationBar()
}
}
}
}
}
......
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets ->
windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()).run {
if (!alreadyChanged) {
alreadyChanged = true
binding.topView.updateLayoutParams<ConstraintLayout.LayoutParams> { height += top }
binding.tvTitle.updateLayoutParams<ConstraintLayout.LayoutParams> { topMargin += top }
}
// 当底部空间不为0时可以判断导航栏显示
navigationBarShow = bottom != 0
}
WindowInsetsCompat.CONSUMED
}
......
}
......
private fun hideNavigationBar() {
insetsController.hide(WindowInsetsCompat.Type.navigationBars())
}
override fun onDestroy() {
super.onDestroy()
// 页面销毁时清除监听
supportFragmentManager.clearFragmentResultListener(this::class.java.simpleName)
}
}
修改后效果如图:
示例
演示代码已在示例Demo中添加。