這是Android視圖繪製系列文章的第一篇,系列文章目錄以下:算法
若是想直接看結論能夠移步到第三篇末尾。bash
View繪製就比如畫畫,先拋開Android概念,若是要畫一張圖,首先會想到哪幾個基本問題:app
Android繪製系統也是按照這個思路對View進行繪製,上面這些問題的答案分別藏在less
這一篇將以源碼中的幾個關鍵函數爲線索分析「測量(measure)」。ide
「測量」要解決的問題是肯定待繪製View的尺寸,以View.measure()
爲入口,一探究竟:函數
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
/**
* <p>
* This is called to find out how big a view should be. The parent
* supplies constraint information in the width and height parameters.
* 這個方法用於決定當前view到底有多大,父親提供寬高參數起到限制大小的做用
*
* The actual measurement work of a view is performed in
* {@link #onMeasure(int, int)}, called by this method. Therefore, only
* {@link #onMeasure(int, int)} can and must be overridden by subclasses.
* 真正的測量工做在onMeasure()中進行
*
* @param widthMeasureSpec Horizontal space requirements as imposed by the
* parent(父親施加的寬度要求)
* @param heightMeasureSpec Vertical space requirements as imposed by the
* parent(父親施加的高度要求)
*
* @see #onMeasure(int, int)
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}
/**
* 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.
* View子類應該重載這個方法以定義本身尺寸
*
* <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.
* 重載方法必須調用 setMeasuredDimension()
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
}
複製代碼
從註釋中得知這麼幾個信息:佈局
onMeasure()
中進行,View
的子類應該重載這個方法以定義本身尺寸。onMeasure()
中必須調用setMeasuredDimension()
。View
會經過傳入的寬高參數對子View
的尺寸施加限制。順帶便看了一下常見控件如何重載onMeasure()
,其實套路都同樣,無論是TextView
仍是ImageView
,在一系列計算得出寬高值後將傳入setMeasuredDimension()
。因此,整個測量過程的終點是View.setMeasuredDimension()
的調用,它表示着視圖大小已經有肯定值。post
View
必然依附於一棵「View樹」,那父View
是如何對子View
的尺寸施加影響的?全局搜索View.measure()
被調用的地方,在不少ViewGroup
類型的控件中發現相似child.measure()
的調用,以最簡單的FrameLayout
爲例:ui
public class FrameLayout extends ViewGroup {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//得到孩子數量
int count = getChildCount();
...
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
//遍歷可見孩子或者強制遍歷全部孩子
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//測量孩子
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//記憶孩子中最大寬度
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
//記憶孩子中最大高度
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
...
//以最孩子中最大的尺寸做爲本身的尺寸
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
...
}
}
複製代碼
FrameLayout
會遍歷全部可見的孩子記憶其中最大寬度和最大高度,並以此做爲本身的寬和高(這是FrameLayout
的測量算法,其餘的ViewGroup
應該也有本身獨特的測量算法。)this
父控件在遍歷每一個孩子時會調用measureChildWithMargins()
來測量孩子:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding
* and margins. The child must have MarginLayoutParams The heavy lifting is
* done in getChildMeasureSpec.
* 要求孩子本身測量本身(考慮父親的要求和本身的邊距)
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view(來自父親的寬度要求)
* @param widthUsed Extra space that has been used up by the parent
* horizontally (possibly by other children of the parent)
* @param parentHeightMeasureSpec The height requirements for this view(來自父親的高度要求)
* @param heightUsed Extra space that has been used up by the parent
* vertically (possibly by other children of the parent)
*/
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//得到孩子佈局參數
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//結合父親要求和孩子訴求分別得到寬和高
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
//孩子本身測量本身
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
複製代碼
讀到這裏應該能夠總結出ViewGroup
的測量過程: 遍歷全部的孩子,經過調用View.measure()
觸發孩子們測量本身。測量完全部孩子以後,按照自有的測量算法將孩子們的尺寸轉換成本身的尺寸並傳入View.setMeasuredDimension()
。
觸發孩子測量本身的時候傳入了寬高兩個參數,它們是經過ViewGroup.getChildMeasureSpec()
產生的:
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
...
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
...
switch (specMode) {
case MeasureSpec.EXACTLY:
...
break;
case MeasureSpec.AT_MOST:
...
break;
case MeasureSpec.UNSPECIFIED:
...
break;
}
...
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
}
複製代碼
這個函數中有一個陌生的類MeasureSpec
,點進去看看:
/**
* A MeasureSpec encapsulates the layout requirements passed from parent to child.
* Each MeasureSpec represents a requirement for either the width or the height.
* A MeasureSpec is comprised of a size and a mode. There are three possible
* modes:
* MeasureSpec包裝了父親對孩子的佈局要求,它是尺寸和模式的混合,它包含三種模式
*
* MeasureSpecs are implemented as ints to reduce object allocation.
* MeasureSpec被實現成一個int值爲了節約空間
*/
public static class MeasureSpec {
//前2位是模式,後30位是尺寸
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/**
* 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;
...
}
複製代碼
MeasureSpec
用於在View
測量過程當中描述尺寸,它是一個包含了佈局模式和佈局尺寸的int
值(32位),其中最高的2位表明佈局模式,後30位表明佈局尺寸。它包含三種佈局模式分別是UNSPECIFIED
、EXACTLY
、AT_MOST
。
結合剛纔的ViewGroup.getChildMeasureSpec()
來探究下這些模式究竟是什麼意思:
/**
* Does the hard part of measureChildren: figuring out the MeasureSpec to
* pass to a particular child. This method figures out the right MeasureSpec
* for one dimension (height or width) of one child view.
* 得到孩子佈局參數(寬或高):混合父親要求和孩子訴求
*
* The goal is to combine information from our MeasureSpec with the
* LayoutParams of the child to get the best possible results. For example,
* if the this view knows its size (because its MeasureSpec has a mode of
* EXACTLY), and the child has indicated in its LayoutParams that it wants
* to be the same size as the parent, the parent should ask the child to
* layout given an exact size.
*
* @param spec The requirements for this view 父親要求:要求孩子多寬多高
* @param padding The padding of this view for the current dimension and
* margins, if applicable
* @param childDimension How big the child wants to be in the current
* dimension孩子訴求:想要多寬多高
* @return a MeasureSpec integer for the child
*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//得到父親測量模式
int specMode = MeasureSpec.getMode(spec);
//得到父親尺寸
int specSize = MeasureSpec.getSize(spec);
//從父親尺寸中去除padding
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
//結合父親要求和孩子訴求計算出孩子尺寸,父親有三種類型的要求,孩子有三種類型的訴求,孩子尺寸一共有9種結果。
switch (specMode) {
// Parent has imposed an exact size on us(父親有明確尺寸)
case MeasureSpec.EXACTLY:
//若是孩子對本身尺寸有明確要求,只能知足它,不考慮padding
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
//若是孩子要求和父親同樣大且父親有明確尺寸,則孩子尺寸有肯定,考慮padding
else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
}
//若是孩子要求徹底顯示本身內容,但它不能超過父親,考慮padding
else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can’t be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us(父親有明確最大尺寸)
case MeasureSpec.AT_MOST:
//若是孩子對本身尺寸有明確要求,只能知足它,不考慮padding
if (childDimension >= 0) {
// Child wants a specific size... so be it(父親其實很無奈)
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
}
//若是孩子要求和父親同樣大,但父親只有明確最大尺寸,則孩子也能有明確最大尺寸,考慮padding
else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
//若是孩子要求徹底顯示本身內容,但它不能超過父親,考慮padding
else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can‘t be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be(父親尺寸沒有約束)
case MeasureSpec.UNSPECIFIED:
//若是孩子對本身尺寸有明確要求,只能知足它,不考慮padding
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
複製代碼
這個函數揭示了一個「人間真相」:父親老是對孩子有要求,但孩子也老是有本身的訴求。最圓滿的結局莫過於充分考量兩方面的需求並調和之。ViewGroup.getChildMeasureSpec()
將3種父親的要求和3種孩子的訴求進行了調和(詳見上述代碼及註釋)
View
繪製中的測量是一個從View樹
開始自頂向下的遞歸過程,遞表示父控件觸發子控件測量本身,歸表示子控件完成測量後,父控件測量本身。MeasureSpec
對象傳遞給子控件以指導子控件測量本身。MeasureSpec
用於在View
測量過程當中描述尺寸,它是一個包含了佈局模式和佈局尺寸的int
值(32位),其中最高的2位表明佈局模式,後30位表明佈局尺寸。View.setMeasuredDimension()
的調用,它表示着視圖大小已經有肯定值。