Android 自定義view的測量
首先在自定義view的時候,你需要告訴系統該畫一個多大的View。這個過程在onMeasure()方法裏進行的。
Android系統給我們提供了一個設計短小精悍卻功能強大的類——MeasureSpec類。通過它可以幫助我們測量view。
MeasureSpec是一個32位的int值,其中高2位爲測量的模式,低30位爲測量的大小,在計算中使用位運算的原因是爲了提高並優化效率。
如下是MeasureSpec源碼:
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/** @hide */
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
/**
* 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;
/**
* Creates a measure specification based on the supplied size and mode.
*
* The mode must always be one of the following:
* <ul>
* <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>
* <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>
* <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>
* </ul>
*
* <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's
* implementation was such that the order of arguments did not matter
* and overflow in either value could impact the resulting MeasureSpec.
* {@link android.widget.RelativeLayout} was affected by this bug.
* Apps targeting API levels greater than 17 will get the fixed, more strict
* behavior.</p>
*
* @param size the size of the measure specification
* @param mode the mode of the measure specification
* @return the measure specification based on size and mode
*/
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
/**
* Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED
* will automatically get a size of 0. Older apps expect this.
*
* @hide internal use only for compatibility with system widgets and older apps
*/
public static int makeSafeMeasureSpec(int size, int mode) {
if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) {
return 0;
}
return makeMeasureSpec(size, mode);
}
/**
* Extracts the mode from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the mode from
* @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
* {@link android.view.View.MeasureSpec#AT_MOST} or
* {@link android.view.View.MeasureSpec#EXACTLY}
*/
@MeasureSpecMode
public static int getMode(int measureSpec) {
//noinspection ResourceType
return (measureSpec & MODE_MASK);
}
/**
* Extracts the size from the supplied measure specification.
*
* @param measureSpec the measure specification to extract the size from
* @return the size in pixels defined in the supplied measure specification
*/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
static int adjust(int measureSpec, int delta) {
final int mode = getMode(measureSpec);
int size = getSize(measureSpec);
if (mode == UNSPECIFIED) {
// No need to adjust size for UNSPECIFIED mode.
return makeMeasureSpec(size, UNSPECIFIED);
}
size += delta;
if (size < 0) {
Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size +
") spec: " + toString(measureSpec) + " delta: " + delta);
size = 0;
}
return makeMeasureSpec(size, mode);
}
/**
* Returns a String representation of the specified measure
* specification.
*
* @param measureSpec the measure specification to convert to a String
* @return a String with the following format: "MeasureSpec: MODE SIZE"
*/
public static String toString(int measureSpec) {
int mode = getMode(measureSpec);
int size = getSize(measureSpec);
StringBuilder sb = new StringBuilder("MeasureSpec: ");
if (mode == UNSPECIFIED)
sb.append("UNSPECIFIED ");
else if (mode == EXACTLY)
sb.append("EXACTLY ");
else if (mode == AT_MOST)
sb.append("AT_MOST ");
else
sb.append(mode).append(" ");
sb.append(size);
return sb.toString();
}
}
在MeasureSpec類中有三種模式:
EXACTLY
即精準模式,當我們將控件的layout_width屬性或者layout_height屬性指定爲具體數值時,比如layout_width=”200dp”,或者指定爲match_parent屬性時(佔據父類View的大小),系統使用的是EXACTLY。AT_MOST
最大值模式,當控件的layout_width屬性或layout_height屬性指定爲warp_content時,控件大小一般隨着控件的子控件的內容的變化而變化,此時控件的尺寸只要不超過父控件允許的最大值即可。- UNSPECIFIED
這個屬性它不指定其大小測量模式,View想多大就多大,通常情況下繪製自定義view時才使用。
下面我們在看下View中的onMeasure()方法是怎麼寫的:
/**
* <p>
* Measure the view and its content to determine the measured width and the
* measured height. This method is invoked by {@link #measure(int, int)} and
* should be overridden by subclasses to provide accurate and efficient
* measurement of their contents.
* </p>
*
* <p>
* <strong>CONTRACT:</strong> When overriding this method, you
* <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
* measured width and height of this view. Failure to do so will trigger an
* <code>IllegalStateException</code>, thrown by
* {@link #measure(int, int)}. Calling the superclass'
* {@link #onMeasure(int, int)} is a valid use.
* </p>
*
* <p>
* The base class implementation of measure defaults to the background size,
* unless a larger size is allowed by the MeasureSpec. Subclasses should
* override {@link #onMeasure(int, int)} to provide better measurements of
* their content.
* </p>
*
* <p>
* If this method is overridden, it is the subclass's responsibility to make
* sure the measured height and width are at least the view's minimum height
* and width ({@link #getSuggestedMinimumHeight()} and
* {@link #getSuggestedMinimumWidth()}).
* </p>
*
* @param widthMeasureSpec horizontal space requirements as imposed by the parent.
* The requirements are encoded with
* {@link android.view.View.MeasureSpec}.
* @param heightMeasureSpec vertical space requirements as imposed by the parent.
* The requirements are encoded with
* {@link android.view.View.MeasureSpec}.
*
* @see #getMeasuredWidth()
* @see #getMeasuredHeight()
* @see #setMeasuredDimension(int, int)
* @see #getSuggestedMinimumHeight()
* @see #getSuggestedMinimumWidth()
* @see android.view.View.MeasureSpec#getMode(int)
* @see android.view.View.MeasureSpec#getSize(int)
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
有上面的代碼看出來在View中onMeasure中最後執行的是setMeasureDimension()方法,參數是getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
我們在來看下getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
/**
* Utility to return a default size. Uses the supplied size if the
* MeasureSpec imposed no constraints. Will get larger if allowed
* by the MeasureSpec.
*
* @param size Default size for this view
* @param measureSpec Constraints imposed by the parent
* @return The size this view should be.
*/
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;
}
由上面的源代碼可以看出來只處理 MeasureSpec.UNSPECIFIED、MeasureSpec.EXACTLY這倆種測量方式。
所以我們在自定義view時,如果想讓自定義View支持warp_content屬性,那麼就必須要重寫onMeasure()方法來指定warp_content時的大小。
接下來我們可以自己自定義View 如下
- 首先重寫onMeasure()方法。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(measureWidth(widthMeasureSpec),
measureHeight(heightMeasureSpec));
}
- 其中setMeasuredDimension() 是view在onMeasure()方法中最後要調用的。這裏我們用measureWidth(widthMeasureSpec)和measureHeight(heightMeasureSpec)重新定義測量規則。
下面我們對measureWidth(widthMeasureSpec)舉例說明如何自定義測量值的。
第一步從MeasureSpec對象中提取具體的測量模式和大小,代碼如下:
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
- 第二步通過判斷測量的模式,給出不同的測量值如下:
private int measureWidth(int widthMeasureSpec){
int result=0;
int specMode=MeasureSpec.getMode(widthMeasureSpec);
int specSize=MeasureSpec.getSize(widthMeasureSpec);
if(specMode==MeasureSpec.EXACTLY){
result=widthMeasureSpec;
}else{
result=500;
if(specMode==MeasureSpec.AT_MOST){
result=Math.min(result,specSize);
}
}
return result;
}
我在這裏說明一下,當specMode爲EXACTLy時,直接指定specSize即可,當specMode爲其他的模式時,需要給一個默認的大小。如果指定wrap_content屬性,即AT_MOST模式,需要取出我們指定的大小與specSize 其中最小的那個值,作爲測量的最後測量值.
ViewGroup
在最後簡單說下ViewGroup的測量,ViewGroup回去管理其子View,其中一個管理項目就是負責子View的顯示大小。當ViewGroup的大小爲wrap_content,ViewGroup就需要對子view進行遍歷,以便獲的所有子view的大小,從而決定自己的大小。而在其他模式下則會通過具體的指定值來設置自身的大小。
當子view測量完畢後,就需要將子view放到合適的位置上,這個過程就是View的Layout過程。ViewGroup在執行Layout過程時,同樣是遍歷來調用子view的layout的方法,並指定其具體顯示的位置,從而來決定其佈局位置。
自定義ViewGroup時,通常會重寫onLayout方法來控制子View顯示位置的邏輯。同樣,如果需要支持wrap_content屬性,那麼它還必須重寫onMeasure()方法,這點與View是一樣的。