Android自定義控件知識探索——View的測量模式
一個Android開發者總會遇到自定義控件的問題。要學會自定義控件的開發,最好的方法是將要用到的知識點一個個掌握。當掌握這些分散的知識點就意味著寫一個自定義控件會變得容易。本篇文章是對View的測量的探究。
概念
View的測量主要掌握三種測量模式:
貼上源碼:
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
這里的測量是對View的width和height進行測量。
UNSPECIFIED:未指定測量模式。View大小不確定,想要多大有多大。
EXACTLY: 精確值模式。當控件的width和height設置為具體值或者match_parent時就是這個模式。
AT_MOST:最大值模式。父布局決定子布局大小(例如:父布局width或者height設置一個默認的精確值,子布局設置為wrap_content。此時子布局的最大width或者height就是父布局的width或者height)。使用這種測量模式的View,設置的一定是wrap_content。
測試
接下來通過具體的代碼來測試三種測量模式使用的場景:
準備工作:新建一個View。在onMesure()中寫測量的代碼。
public class TestMesureView extends View {
public TestMesureView(Context context) {
this(context, null);
}
public TestMesureView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TestMesureView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = 0;
int height = 0;
int widthMode = getMode(widthMeasureSpec);
int heightMode = getMode(heightMeasureSpec);
/** 測量width **/
width = getReallySize(widthMode,widthMeasureSpec);
/** 測量height **/
height = getReallySize(heightMode,heightMeasureSpec);
Log.i("really width mode",logMode(widthMode));
Log.i("really width",String.valueOf(width));
Log.i("really split","---------------------------");
Log.i("really height mode",logMode(heightMode));
Log.i("really height",String.valueOf(height));
setMeasuredDimension(width, height);
}
/**
* 獲取測量模式
* @param sizeMeasureSpec
* @return
*/
private int getMode(int sizeMeasureSpec){
return MeasureSpec.getMode(sizeMeasureSpec);
}
/**
* 通過測量模式獲取真正的Size
* @param mode
* @param sizeMeasureSpec
* @return
*/
private int getReallySize(int mode,int sizeMeasureSpec){
int specSize = 0;
switch (mode){
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
specSize = MeasureSpec.getSize(sizeMeasureSpec);
break;
case MeasureSpec.UNSPECIFIED:
specSize = sizeMeasureSpec;
break;
}
return specSize;
}
private String logMode(int mode){
switch (mode){
case MeasureSpec.AT_MOST:
return "AT_MOST";
case MeasureSpec.EXACTLY:
return "EXACTLY";
case MeasureSpec.UNSPECIFIED:
return "UNSPECIFIED";
}
return "";
}
}
如上代碼:
getMode() : 獲取測量模式的方法,核心方法為 MeasureSpec.getMode(sizeMeasureSpec); 將onMeasure(int widthMeasureSpec, int heightMeasureSpec)。中兩個參數分別傳入就可分別得到width的測量模式和height的測量模式。
getReallySize(): 獲取測量到的值的方法。核心方法為 MeasureSpec.getSize(sizeMeasureSpec);將onMeasure(int widthMeasureSpec, int heightMeasureSpec)。中兩個參數分別傳入就可分別得到width的真實大小和height的真實大小。
1、EXACTLY
a、將layout_width,layout_height 都設為 match_parent。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.mg.axe.androiddevelop.view.TestMesureView
android:background="#33ee33"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
10-10 22:25:21.271 8207-8207/com.mg.axe.androiddevelop I/really width mode: EXACTLY
10-10 22:25:21.271 8207-8207/com.mg.axe.androiddevelop I/really width: 1080
10-10 22:25:21.271 8207-8207/com.mg.axe.androiddevelop I/really split: ---------------------------
10-10 22:25:21.271 8207-8207/com.mg.axe.androiddevelop I/really height mode: EXACTLY
10-10 22:25:21.271 8207-8207/com.mg.axe.androiddevelop I/really height: 1860
運行結果
分析Log和截圖:
通過運行結果可以看到view充滿整個屏幕。
分析Log可以知道,兩者的測量模式都是 EXACTLY
手機的分辨率為1920*1080 , width為1080 , height為1860(因為有狀態欄所以不是1920)
b、指定精確大小,將layout_width,layout_height 都設為 100dp。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.mg.axe.androiddevelop.view.TestMesureView
android:background="#33ee33"
android:layout_width="100dp"
android:layout_height="100dp" />
</LinearLayout>
10-11 00:13:23.511 763-763/com.mg.axe.androiddevelop I/really width mode: EXACTLY
10-11 00:13:23.511 763-763/com.mg.axe.androiddevelop I/really width: 300
10-11 00:13:23.511 763-763/com.mg.axe.androiddevelop I/really split: ---------------------------
10-11 00:13:23.511 763-763/com.mg.axe.androiddevelop I/really height mode: EXACTLY
10-11 00:13:23.511 763-763/com.mg.axe.androiddevelop I/really height: 300
運行結果
分析:
分析Log可以知道,兩者的測量模式都是 EXACTLY
獲取到的width和height都為 300. (系統測量會將單位轉為px)
2、AT_MOST
a、父布局將layout_width,layout_height 都設為 match_parent
將子布局的layout_width,layout_height 都設為 wrap_content
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.mg.axe.androiddevelop.view.TestMesureView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#33ee33"/>
</LinearLayout>
10-11 01:27:32.656 29513-29513/com.mg.axe.androiddevelop I/really width mode: AT_MOST
10-11 01:27:32.656 29513-29513/com.mg.axe.androiddevelop I/really width: 1080
10-11 01:27:32.656 29513-29513/com.mg.axe.androiddevelop I/really split: ---------------------------
10-11 01:27:32.656 29513-29513/com.mg.axe.androiddevelop I/really height mode: AT_MOST
10-11 01:27:32.656 29513-29513/com.mg.axe.androiddevelop I/really height: 1860
分析:
子布局的寬高測量模式都為: AT_MOST
父布局的layout_width和layout_height都為match_parent,父布局的寬高約為屏幕的寬高。
子布局的layout_width和layout_height都為wrap_content,子布局大小不固定,但是最大值受父布局大小影響。這種情況的測量模式就是 AT_MOST 。
b、將父布局設置為指定大小,需要測量的布局將layout_width,layout_height 都設為 wrap_content
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="100dp"
android:layout_height="100dp"
android:orientation="vertical">
<com.mg.axe.androiddevelop.view.TestMesureView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#33ee33" />
</LinearLayout>
10-11 01:30:41.126 2423-2423/com.mg.axe.androiddevelop I/really width mode: AT_MOST
10-11 01:30:41.126 2423-2423/com.mg.axe.androiddevelop I/really width: 300
10-11 01:30:41.126 2423-2423/com.mg.axe.androiddevelop I/really split: ---------------------------
10-11 01:30:41.126 2423-2423/com.mg.axe.androiddevelop I/really height mode: AT_MOST
10-11 01:30:41.126 2423-2423/com.mg.axe.androiddevelop I/really height: 300
分析:
這種情況和上面a測試的結論一樣。子布局大小不固定,但是最大值受父布局大小影響。這種情況的測量模式就是 EXACTLY 。
c、測試出一種特殊的情況
當父布局是RelativeLayout,子布局的layout_width,layout_height 都設為 wrap_content時,子布局的width測量模式為EXACTLY
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="100dp"
android:layout_height="100dp"
android:orientation="vertical">
<com.mg.axe.androiddevelop.view.TestMesureView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#33ee33" />
</RelativeLayout>
10-11 09:05:26.970 14275-14275/com.mg.axe.androiddevelop I/really width mode: EXACTLY
10-11 09:05:26.970 14275-14275/com.mg.axe.androiddevelop I/really width: 300
10-11 09:05:26.970 14275-14275/com.mg.axe.androiddevelop I/really split: ---------------------------
10-11 09:05:26.970 14275-14275/com.mg.axe.androiddevelop I/really height mode: AT_MOST
10-11 09:05:26.970 14275-14275/com.mg.axe.androiddevelop I/really height: 300
分析:
我暫時也不知道子View的寬的測量模式是EXACTLY。這應該是一種特殊情況。
這里再次做提醒:如果這個View的測量模式為AT_MOST,這個View一定設置了wrap_content
3、UNSPECIFIED
a、添加父布局scrollview,將測試的view放在里面
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.mg.axe.androiddevelop.view.TestMesureView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#33ee33"/>
</ScrollView>
</LinearLayout>
10-11 01:18:23.566 15113-15113/com.mg.axe.androiddevelop I/really width mode: AT_MOST
10-11 01:18:23.566 15113-15113/com.mg.axe.androiddevelop I/really width: 1080
10-11 01:18:23.566 15113-15113/com.mg.axe.androiddevelop I/really split: ---------------------------
10-11 01:18:23.566 15113-15113/com.mg.axe.androiddevelop I/really height mode: UNSPECIFIED
10-11 01:18:23.566 15113-15113/com.mg.axe.androiddevelop I/really height: 0
分析:
這里我們只要分析height就行了,這種情況下 父布局ScrollView的子view的高度是不固定的,想要多大就可多大。所以這里height的測量模式為 UNSPECIFIED
實際應用
1、先測量再繪制
在寫自定義控件時,涉及到測量繪制的。一般是先測量再繪制。
2、測量方法
這個是上面寫的方法。是參照源碼寫的。
private int getReallySize(int mode,int sizeMeasureSpec){
int specSize = 0;
switch (mode){
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
specSize = MeasureSpec.getSize(sizeMeasureSpec);
break;
case MeasureSpec.UNSPECIFIED:
specSize = sizeMeasureSpec;
break;
}
return specSize;
}
在View的源碼中有一個getDefaultSize的方法。
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
}
3、測量完畢之后一定要調用setMeasuredDimension(width, height);
要調用setMeasuredDimension或者super.onMeasure來設置自身的mMeasuredWidth和mMeasuredHeight,否則,就會拋出異常.
來自:http://www.jianshu.com/p/85548a440cd2