Android — DialogFragment显示后隐藏的导航栏显示问题

沉浸式显示广泛的应用于大部分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对页面的WindowdecorView进行操作,隐藏了导航栏。但是在DialogFragment中,Dialog对象也有其所属的WindowdecorView,上述示例代码中并没有针对Dialog所属的WindowdecorView进行配置。

基于上面的分析,对示例代码进行调整,调整如下:

  • 加载弹窗
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中添加。

ExampleDemo github

ExampleDemo gitee

相关推荐
无极程序员1 小时前
远程主机可能不符合glibc和libstdc++ VS Code服务器的先决条件
android·java·运维·服务器·php
快乐1012 小时前
Mac下FFmpeg编译和集成
android
_一条咸鱼_2 小时前
Android大厂面试秘籍:不同Android系统版本特性分析
android·面试·android jetpack
casual_clover3 小时前
Android 中如何配置 targetSdk 值
android
zd8451015003 小时前
安卓开发提示Android Gradle plugin错误
android
pengyu5 小时前
【Flutter 状态管理 - 贰】 | 提升对界面与状态的认知
android·flutter·dart
_一条咸鱼_5 小时前
Android大厂面试秘籍: Activity Intent 解析与处理模块(三)
android·面试·android jetpack
Full Stack Developme5 小时前
SQL 中的 NULL 处理
android·数据库·sql
我最厉害。,。6 小时前
XSS 跨站&SVG&PDF&Flash&MXSS&UXSS&配合上传&文件添加脚本
android·pdf·xss
界面开发小八哥8 小时前
支持iOS与Android!SciChart开源金融图表库助力高效开发交易应用
android·ios·数据分析·数据可视化·图表工具·scichart