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

相关推荐
烬奇小云3 小时前
认识一下Unicorn
android·python·安全·系统安全
顾北川_野15 小时前
Android 进入浏览器下载应用,下载的是bin文件无法安装,应为apk文件
android
CYRUS STUDIO15 小时前
Android 下内联汇编,Android Studio 汇编开发
android·汇编·arm开发·android studio·arm
右手吉他15 小时前
Android ANR分析总结
android
PenguinLetsGo17 小时前
关于 Android15 GKI2407R40 导致梆梆加固软件崩溃
android·linux
杨武博19 小时前
音频格式转换
android·音视频
音视频牛哥21 小时前
Android音视频直播低延迟探究之:WLAN低延迟模式
android·音视频·实时音视频·大牛直播sdk·rtsp播放器·rtmp播放器·android rtmp
ChangYan.1 天前
CondaError: Run ‘conda init‘ before ‘conda activate‘解决办法
android·conda
二流小码农1 天前
鸿蒙开发:ForEach中为什么键值生成函数很重要
android·ios·harmonyos
夏非夏1 天前
Android 生成并加载PDF文件
android