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

相关推荐
Dingdangr4 小时前
Android中的Intent的作用
android
技术无疆4 小时前
快速开发与维护:探索 AndroidAnnotations
android·java·android studio·android-studio·androidx·代码注入
GEEKVIP4 小时前
Android 恢复挑战和解决方案:如何从 Android 设备恢复删除的文件
android·笔记·安全·macos·智能手机·电脑·笔记本电脑
Jouzzy10 小时前
【Android安全】Ubuntu 16.04安装GDB和GEF
android·ubuntu·gdb
极客先躯11 小时前
java和kotlin 可以同时运行吗
android·java·开发语言·kotlin·同时运行
Good_tea_h13 小时前
Android中的单例模式
android·单例模式
计算机源码社18 小时前
分享一个基于微信小程序的居家养老服务小程序 养老服务预约安卓app uniapp(源码、调试、LW、开题、PPT)
android·微信小程序·uni-app·毕业设计项目·毕业设计源码·计算机课程设计·计算机毕业设计开题
丶白泽19 小时前
重修设计模式-结构型-门面模式
android
晨春计20 小时前
【git】
android·linux·git
标标大人21 小时前
c语言中的局部跳转以及全局跳转
android·c语言·开发语言