- Building Android UIs with Custom Views
- Raimon Ràfols Montané
- 879字
- 2021-07-02 15:33:38
Measuring our custom view
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:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); }
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:
<com.packt.rrafols.customview.OwnCustomView android:layout_width="150dp" android:layout_height="150dp"/>
Code on our custom view, using MeasureSpec.toString(int) to get a string description of the measure specification and the size looks like this:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { Log.d(TAG, "width spec: " +
MeasureSpec.toString(widthMeasureSpec)); Log.d(TAG, "height spec: " +
MeasureSpec.toString(heightMeasureSpec)); super.onMeasure(widthMeasureSpec, heightMeasureSpec); }
The result on the Android log is as follows:
D/com.packt.rrafols.customview.OwnCustomView: width : MeasureSpec: EXACTLY 394
D/com.packt.rrafols.customview.OwnCustomView: height: MeasureSpec: EXACTLY 394
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.
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:
<com.packt.rrafols.customview.OwnCustomView android:layout_width="match_parent" android:layout_height="match_parent"/>
Running the same code as before, we get the following on the Android log:
D/com.packt.rrafols.customview.OwnCustomView: width : MeasureSpec: EXACTLY 996
D/com.packt.rrafols.customview.OwnCustomView: height: MeasureSpec: EXACTLY 1500
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:
<com.packt.rrafols.customview.OwnCustomView android:layout_width="match_parent" android:layout_height="wrap_content"/>
The result is as follows:
D/com.packt.rrafols.customview.OwnCustomView: width : MeasureSpec: EXACTLY 996
D/com.packt.rrafols.customview.OwnCustomView: height: MeasureSpec: AT_MOST 1500
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:
<?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="@dimen/activity_vertical_margin"> <com.packt.rrafols.customview.OwnCustomView
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout> </ScrollView>
Then we'll get the following on the Android log:
D/com.packt.rrafols.customview.OwnCustomView: width : MeasureSpec: EXACTLY 996
D/com.packt.rrafols.customview.OwnCustomView: height: MeasureSpec: UNSPECIFIED 1500
That seems alright, but what happens if we now run this code? We'll get an empty screen; our red background we've previously implemented is gone:

That is because we're not managing the size of our custom view. Let's fix that, as shown in the following code:
private static int getMeasurementSize(int measureSpec, int defaultSize) { int mode = MeasureSpec.getMode(measureSpec); int size = MeasureSpec.getSize(measureSpec); switch(mode) { case MeasureSpec.EXACTLY: return size; case MeasureSpec.AT_MOST: return Math.min(defaultSize, size); case MeasureSpec.UNSPECIFIED: default: return defaultSize; } } @Override protected void onMeasure(int widthMeasureSpec, int
heightMeasureSpec) { int width = getMeasurementSize(widthMeasureSpec, DEFAULT_SIZE); int height = getMeasurementSize(heightMeasureSpec,
DEFAULT_SIZE); setMeasuredDimension(width, height); }
Now, depending on the measurement specs, we'll set the size of our view by calling the setMeasuredDimension(int, int) method.
For the full example, check the source code in the Example03-Measurement folder in the GitHub repository.
- Advanced Quantitative Finance with C++
- Web前端開發技術:HTML、CSS、JavaScript(第3版)
- JIRA 7 Administration Cookbook(Second Edition)
- C#程序設計教程
- Oracle 18c 必須掌握的新特性:管理與實戰
- Lighttpd源碼分析
- Visual Foxpro 9.0數據庫程序設計教程
- Android應用開發深入學習實錄
- Python+Office:輕松實現Python辦公自動化
- Hack與HHVM權威指南
- 安卓工程師教你玩轉Android
- Mastering ArcGIS Server Development with JavaScript
- Python人工智能項目實戰
- Web程序設計與架構
- Python程序員面試算法寶典