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?
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.