In the quick example we built in the previous chapter, we delegated all sizes and measurement to the parent view itself. To be honest, we haven't even delegated it; we just didn't do anything specifically to take care of that. Being able to control the size and dimensions of our custom view is something we definitely need to pay some attention to. To start, we're going to override the onMeasure() method from view as follows:
Reading the Android documentation about the onMeasure() method, we should see we must call either setMeasuredDimension(int, int) or the superclass' onMeasure(int, int). If we forget to do so, we'll get an IllegalStateException:
com.packt.rrafols.customview E/AndroidRuntime: FATAL EXCEPTION: main Process: com.packt.rrafols.customview, PID: 13601 java.lang.IllegalStateException: View with id -1: com.packt.rrafols.customview.OwnCustomView#onMeasure() did not set the measured dimension by calling setMeasuredDimension() at android.view.View.measure(View.java:18871)
There are three different modes in which our view's parent can indicate to our view how it should calculate its size. We can get the mode by using the MeasureSpec.getMode(int) method with each size spec widthMeasureSpec and heightMeasureSpec.
These modes are as follows:
MeasureSpec.EXACTLY
MeasureSpec.AT_MOST
MeasureSpec.UNSPECIFIED
We'll get MeasureSpec.EXACTLY when the size has been calculated or decided by the parent. Our view will have that size even if it requires or returns a different size. If we get MeasureSpec.AT_MOST we have more flexibility: we can be as big as we need but up to the size we also have. Finally, if we received MeasureSpec.UNSPECIFIED, we can size our view to whatever size we want or the view needs.
Using MeasureSpec.getSize(int), we can also get a size value from the size spec.
Now that we have all this, how do we know which values map to the width and height parameters on our XML layout file? Easy to see, let's check. For example, if we specify precise values as shown in the activity_main.xml file in the GitHub repository, we will get the following code:
Our view will be 394 by 394 pixels exactly. The 394 pixels comes from transforming the 150dp to pixels on the mobile device I was using for testing.
As there are many Android devices with different resolutions and screen densities, we should always use density-independent pixel ( dp) or ( dip) instead of pixels.
For more details about dp, refer to a video by Google available on YouTube: DesignBytes: Density-independent Pixels.
If you would like to convert from dp to real pixels on a specific device, you can use the following method:
public final int dpToPixels(int dp) {
return (int) (dp * getResources().getDisplayMetrics().density + 0.5);
}
We can see how the conversion is done using the density of the screen, so on different devices the conversion can be different. The + 0.5 in the previous code is just to round up the value when converting from a floating point number to an int.
To convert from pixels to density-independent points, we have to do the inverse operation, as shown in the following code:
public final int pixelsToDp(int dp) {
return (int) (dp / getResources().getDisplayMetrics().density + 0.5);
}
Let's now see what we receive if we use different measure parameters, such as match_parent or wrap_content, as shown in the activity_main.xml file in the GitHub repository:
So we are still getting a MeasureSpec.EXACTLY, but this time with the size of the parent RelativeLayout; let's try changing one of the match_parents for a wrap_content in activity_main.xml:
We can spot an easy-to-follow pattern with MeasureSpec.EXACTLY and MeasureSpec.AT_MOST, but what about MeasureSpec.UNSPECIFIED?
We'll get a MeasureSpec.UNSPECIFIED if our parent is not bounded; for example, if we have a vertical LinearLayout inside a ScrollView, as shown in the scrollview_layout.xml file in the GitHub repository: