Supercharge Your Flutter Apps With Apple Watch Integration
Leandro Pontes Berleze | Sep 25, 2024
I believe that almost every Android dev has at least once tried to get a View’s dimension, but it returned 0, nada. This is a very common situation, it is one of those problems that we just Google for a solution and find a workaround that seems to work. We add it to the code without having much idea of what’s going on and that’s it. In this article I’ll explain why this happens and why many of the most used workarounds should be avoided. In the end, I’ll give you a very sweet solution about Android views.
One of the very basic Android concepts that we first learn is the Activity lifecycle. We understand that the onCreate() callback is where the UI should be declared and configured. Having this in mind, let’s say we have a View that for some reason needs a manual setup like this:
myView.layoutParams.width = myView.width / 2
This sets myView width based on its own width, this means that this code should run only once. You add it inside onCreate() right after setting content view, run the application but it doesn’t work as expected. myView had a width of zero and then your code just divided 0 by 2. So you think, maybe the view will be laid out in onStart(). You test it and again it doesn’t work. Maybe onResume() will work? Wait… what?! onResume doesn’t work either?! The next lifecycle step should be when the Activity is already running and you are still unable to finish the UI setup. What else can we do?
Ok, let’s be radical and try onWindowsFocusChanged(). Voilà, it works! You are happy and can go back to work. Suddenly your app goes to background and a phone call pops up, it’s your mom asking you to buy some bread on your way home. After the call ends, your app comes back to the foreground, as well as your frustration. myView has now half the width that it should, and you realize that onWindowsFocusChanged() was called again after the app came back to the foreground.
We now know that for some reason there isn’t a lifecycle callback for when the activity UI is fully laid out, which is something that some stacks provide for us, such as on iOS. So the question is, when is a view fully laid out and drawn?
An Activity layout is drawn upon the view hierarchy. From the image below, you can see that this hierarchy is a tree with two different nodes – ViewGroup and View.
ViewGroup is also a View, but it is a layout or container that can contain many children. Examples of ViewGroups are LinearLayout and ConstraintLayout. View is any other UI component.
Each View has two different types of sizes: the measured width and height, which is basically how big it wants to be, and the width and height that are the actual dimensions they have after being laid out. The desired size cannot always be provided, e.g. a view could request a width larger than its parent’s width.
Both measured and actual sizes are set in two steps:
After the layout pass finishes, the layout can finally be drawn by the OS.
This is a very basic explanation, but it is enough to have an idea of how the draw process works. But when does it happen? The documentation states that it starts when the Activity receives focus, then let’s see it happening with a small code that I wrote.
The code has a custom LinearLayout and TextView which I created just to override onMeasure(), onLayout()/layout() and draw() functions and add some logs to them. You can check it out in this Gist.
Running it results in these logs:
1 onCreate() executed 2 onStart() executed 3 onResume() executed 4 LinearLayout: entering onMeasure(). Measured width: 0 5 TextView: entering onMeasure(). Measured width: 0 6 TextView: leaving onMeasure(). Measured width: 171 7 LinearLayout: leaving onMeasure(). Measured width: 171 8 LinearLayout: entering onLayout(). Actual width: 171 9 TextView: entering layout(). Actual width: 0 10 TextView: leaving layout(). Actual width: 171 11 LinearLayout: leaving onLayout(). Actual width: 171 12 onWindowFocusChanged() executed 13 LinearLayout: entering onMeasure(). Measured width: 171 14 TextView: entering onMeasure(). Measured width: 171 15 TextView: leaving onMeasure(). Measured width: 171 16 LinearLayout: leaving onMeasure(). Measured width: 171 17 LinearLayout: entering onLayout(). Actual width: 171 18 TextView: entering layout(). Actual width: 171 19 TextView: leaving layout(). Actual width: 171 20 LinearLayout: leaving onLayout(). Actual width: 171 21 TextView: draw() executed
We can perfectly see measure and layout passes happening from lines 1 to 11, with measured and actual sizes of both views going from 0 to 171. We can also notice that the two passes happen twice, once before onWindowFocusChanged()and again from lines 13 to 21, but TextView calls draw() only when the Activity gets focus. This means that the layout is indeed drawn only after the Activity receives focus, but the views are laid out before it.
Understanding this whole process is very helpful, but it’s still hard to know how to have a callback for when the entire screen is laid out.
After doing a little research, you will probably find some solutions that people claim they work, which usually are:
Android KTX is a library that is part of Android Jetpack and has a lot of useful Kotlin extensions. For our case, it has the View.doOnLayout() extension with a callback for when a view is laid out, and it also ensures that the callback runs even if the View has already been laid out and hasn’t requested layout. If you are wondering what’s the magic behind it, there isn’t. It uses View.OnLayoutChangeListener without any crazy logic, as you can see in the source code.
With doOnLayout() we have a callback for when a specific view is laid out, but what if you want a callback that is called when the entire layout is laid out? Well, we know that the root View is the last one to be laid out, so this means that by using doOnLayout() with the root View, it will be called when all other Views have already been laid out. There’s just a small problem: how to get the root View?
To achieve this goal in a generic way, we could get the content view of top-level window decor using a base Activity class:
abstract class BaseActivity: AppCompatActivity() {
protected open fun doOnRootViewLayout(action: () -> Unit) {
val rootView = window.decorView.findViewById<View>(android.R.id.content)
rootView.doOnLayout {
action()
}
}
}
Unfortunately I couldn’t find much information about this approach and how safe it is, except an article explaining that this low-level layer can change in other Android versions or devices. Also on Android documentation there isn’t much information about android.R.id.content.
My suggestion is to give an ID to the root view of your layout and use this view to call doOnLayout(), which also works with Fragments and custom views.
We were able to understand when Views have dimensions set and are drawn. For some reason there is not a lifecycle callback for that, but at least we came up with a working solution using doOnLayout.
Working with Android is a mix of love and hate, we all know that. Sometimes very basic things are difficult to achieve or understand (and it is even harder to understand why they are not explained in the documentation). I hope this article helps you to diminish the hate moments caused by some Android flaws (although it might have added more questions than answers) and write a better code and UI without fear of all that obscurity that Android sometimes leaves to us.
https://developer.android.com/guide/components/activities/activity-lifecycle
https://developer.android.com/reference/android/app/Activity#onWindowFocusChanged(boolean)
https://developer.android.com/guide/topics/ui/how-android-draws
https://developer.android.com/reference/android/view/View.html#post(java.lang.Runnable)
https://developer.android.com/reference/android/view/ViewTreeObserver.OnGlobalLayoutListener
https://antonioleiva.com/kotlin-ongloballayoutlistener/
https://developer.android.com/reference/android/view/View.OnLayoutChangeListener
https://developer.android.com/jetpack/
https://developer.android.com/kotlin/ktx
https://developer.android.com/reference/kotlin/androidx/core/view/package-summary#doonlayout
https://android-developers.googleblog.com/2009/03/window-backgrounds-ui-speed.html
https://developer.android.com/reference/android/R.id#content
A computer scientist who always dig deep to find the best solutions, and is in love with mobile development. Also likes playing video games and basketball.