androiddialogscreen-density

Custom dialog moved when opened


I have been building an Android application that should have a fixed display size regardless of the user's display preferences. I know that I shouldn't do this, but it's one of my application's specifications. I achieve this by overriding attachBaseContext(newBase: Context?) as follows:

override fun attachBaseContext(newBase: Context?) {
    val newOverride = Configuration(newBase?.resources?.configuration)

    var screenWidth = newBase?.resources?.displayMetrics?.widthPixels
    var screenHeight = newBase?.resources?.displayMetrics?.heightPixels

    if (screenHeight!! < screenWidth!!) {
        val newScreenHeight = screenWidth
        screenWidth = screenHeight
        screenHeight = newScreenHeight
    }

    val targetDensityDpi = if ((screenHeight.toDouble()/screenWidth.toDouble()) < MINIMUM_ASPECT_RATIO) {
        (screenHeight / 4f).roundToInt()
    } else {
        (screenWidth / 2.25f).roundToInt()
    }

    newOverride.fontScale = 1.0f
    newOverride.densityDpi = targetDensityDpi
    applyOverrideConfiguration(newOverride)

    super.attachBaseContext(newBase)
}

In simple words, this method determines the density to be applied to the window based on the window's aspect ratio. MINIMUM_ASPECT_RATIO is the minimum aspect ratio to be applied to the entire application:

const val MINIMUM_ASPECT_RATIO = 1.66

If the aspect ratio is less than MINIMUM_ASPECT_RATIO, the longer edge of the available display will be set to 640 dp (4 × 160 = 640). Otherwise, the shorter edge of the display will be set to 360 dp (2.25 × 160 = 360).

Another specification of my application is that it has to be run in full screen mode. So, I defined a CustomDialog class and overrode the onStart() and show() methods like this:

private class CustomDialog(context: Context): Dialog(context) {
    override fun onStart() {
        super.onStart()

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            val controller = WindowCompat.getInsetsController(window!!, window!!.decorView)
            controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
            ViewCompat.setOnApplyWindowInsetsListener(window!!.decorView) { v, insets ->
                if (insets.isVisible(WindowInsetsCompat.Type.navigationBars()) ||
                    insets.isVisible(WindowInsetsCompat.Type.statusBars())) {
                    controller.hide(WindowInsetsCompat.Type.systemBars())
                }
                ViewCompat.onApplyWindowInsets(v, insets)
            }
        } else {
            @Suppress("DEPRECATION")
            window?.decorView?.setOnSystemUiVisibilityChangeListener {
                @Suppress("DEPRECATION")
                window?.decorView?.systemUiVisibility = (
                        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
                                or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
                        )
            }
        }
    }

    override fun show() {
        window?.setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            window?.setDecorFitsSystemWindows(false)
            val controller = WindowCompat.getInsetsController(window!!, window!!.decorView)
            controller.hide(WindowInsetsCompat.Type.systemBars())
            controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
        } else {
            @Suppress("DEPRECATION")
            window?.decorView?.systemUiVisibility = (
                    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
                            or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
                    )
        }

        super.show()
        window?.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
    }
}

By doing so, the dialog will always be shown in full screen mode. Here's an example of how I show the dialog:

val dialog = CustomDialog(this)
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
dialog.setContentView(R.layout.privacy_options_dialog)
dialog.window?.setDimAmount(0.3f)
dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
dialog.show()

Here comes the problem. If the user's preferred screen density is equal to the density applied to the application (determined in attachBaseContext(newBase: Context?)), the dialog opens as expected. However, if the user's preferred screen density is not the same as the density applied to the application, the dialog shakes rapidly when opened, and some views of the dialog's layout, especially those that are near the dialog's bounds, are not shown when the layout is large. This output is undesirable. I use CardView for the custom dialog's layout as follows:

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:cardBackgroundColor="@color/darkgray"
    app:cardCornerRadius="24dp"
    android:layout_marginHorizontal="4dp"
    android:layout_marginVertical="0dp">

    <!-- Views -->

</androidx.cardview.widget.CardView>

The custom dialog should not move when displayed, and it should open with all of its contents fully shown to the user, regardless of the user's preferred display size or density. I suspect this could be because I did not fix the density in CustomDialog, which causes the parent's height (set to wrap_content) to be calculated incorrectly, but I couldn't seem to figure out how this can be achieved. I tried wrapping the layout with RelativeLayout but to no avail. How can this problem be fixed?


Solution

  • After days of researching this problem, I have finally found a solution. If you use match_parent or wrap_content to define the bounds of the layout's parent (which is CardView in this example), the bug becomes noticeable if the density applied to the window is different from the user's preferred density setting. To circumvent this bug, specify the dimensions of the custom dialog's layout so that the width and height can be precisely known. However, you are responsible for ensuring that all the content is displayed on the custom dialog and not obscured due to setting the layout's dimensions (layout_width and layout_height) with absolute values.

    If you are keen to use wrap_content or match_parent, you will need to first measure the intended dimensions of the layout. To do so, make sure that the device's display size is the same as the density applied to the activity (specified in attachBaseContext(newBase: Context?)). This way, the dialog will be displayed correctly. Then, include this line in onStart() within CustomDialog(context: Context) to get the parent's dimensions:

    window?.decorView?.post {
        println("Width: ${window?.decorView?.width}, Height: ${window?.decorView?.height}")
    }
    

    However, keep in mind that these values are in pixels, not density-independent pixels (dp). To convert these values from px to dp, simply divide window?.decorView?.width and window?.decorView?.height by resources.displayMetrics.density. Then, you must set the dimensions of the window with absolute values before showing the custom dialog. For example, if the desired width and height of the layout are 300dp and 200dp, respectively, add these lines before calling dialog.show():

    val density = resources.displayMetrics.density
    dialog.window?.setLayout((300 * density).roundToInt(), (200 * density).roundToInt())
    

    By including these lines, the dialog will not shake when opened, and all contents will be fully displayed to the user. I have tested this method on a mobile device and a tablet, both at API 31, without issues, and this approach should work on recent Android versions.