測量 View 就是測量一個矩形

透過另一個視角來觀察,所有的 Widget,我們使用的小控件都是Widget。如果TextView和Buttton等
因此,自定義 View 的第一步,我們要在心里默念 – 我們現(xiàn)在要確定一個矩形了!
既然是矩形,那么它肯定有明確的寬高和位置坐標,寬高是在測量階段得出。然后在布局階段,確定好位置信息對矩形進行布局,之后的視覺效果就交給繪制流程了,我們是最好的畫家。
布局繪畫涉及兩個過程:測量過程和布局過程。測量過程通過measure方法實現(xiàn),是View樹自頂向下的遍歷,每個View在循環(huán)過程中將尺寸細節(jié)往下傳遞,當(dāng)測量過程完成之后,所有的View都存儲了自己的尺寸。第二個過程則是通過方法layout來實現(xiàn)的,也是自頂向下的。在這個過程中,每個父View負責(zé)通過計算好的尺寸放置它的子View。
好了,我們知道了測量的就是長和寬,我們的目的也就是長和寬。
View 設(shè)置尺寸的基本方法
接下來的過程,我將會用一系列比較細致的實驗來說明問題,我們先看看在 Android 中使用 Widget 的時候,怎么定義大小。比如我們要在屏幕上使用一個 Button。
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test"/>
這樣屏幕上就出現(xiàn)了一個按鈕。

我們再把寬高固定。
<Button
android:layout_width="200dp"
android:layout_height="50dp"
android:text="test"/>

再換一種情況,將按鈕的寬度由父容器決定
<Button
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="test"/>

上面就是我們?nèi)粘i_發(fā)中使用的步驟,通過 layout_width 和 layout_height 屬性來設(shè)置一個 View 的大小。而在 xml 中,這兩個屬性有 3 種取值可能。
android:layout_height="wrap_content" //View 本身的內(nèi)容決定高度
android:layout_height="match_parent" //與父視圖等高
android:layout_height="fill_parent" //與父視圖等高
android:layout_height="100dip" //精確設(shè)置高度值為 100dip
我們再進一步,現(xiàn)在給 Button 找一個父容器進行觀察。父容器背景由特定顏色標識。
一
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ff0000">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test" />
</RelativeLayout>

二
可以看到 RelativeLayout 包裹著 Button。我們再換一種情況。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:background="#ff0000">
<Button
android:layout_width="120dp"
android:layout_height="wrap_content"
android:text="test" />
</RelativeLayout>

三
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:background="#ff0000">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="test" />
</RelativeLayout>

四
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:background="#ff0000">
<Button
android:layout_width="1000dp"
android:layout_height="wrap_content"
android:text="test" />
</RelativeLayout>

似乎發(fā)生了不怎么愉快的事情,Button 想要的長度是 1000 dp,而 RelativeLayout 最終給予的卻仍舊是在自己的有限范圍參數(shù)內(nèi)。就好比山水莊園向光明開發(fā)區(qū)政府要地 1 萬畝,政府說沒有這么多,最多 2000 畝。
Button 是一個 View,RelativeLayout 是一個 ViewGroup。那么對于一個 View 而言,它相當(dāng)于山水莊園,而 ViewGroup 類似于政府的角色。View 蕓蕓眾生,它們的多姿多彩構(gòu)成了美麗的 Android 世界,ViewGroup 卻有自己的規(guī)劃,所謂規(guī)劃也就是以大局為重嘛,盡可能協(xié)調(diào)管轄區(qū)域內(nèi)各個成員的位置關(guān)系。
山水莊園拿地蓋樓需要同政府協(xié)商溝通,自定義一個 View 也需要同它所處的 ViewGroup 進行協(xié)商。
那么,它們的協(xié)議是什么?
View 和 ViewGroup 之間的測量協(xié)議 MeasureSpec
我們自定義一個 View,onMeasure()是一個關(guān)鍵方法。也是本文重點研究內(nèi)容。測量自己的大小,為正式布局提供建議。(注意,只是建議,至于用不用,要看onLayout);
public class TestView extends View {
public TestView(Context context) {
super(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
onMeasure() 中有兩個參數(shù) widthMeasureSpec、heightMeasureSpec。它們是什么?看起來和寬高有關(guān)。
它們確實和寬高有關(guān),了解它們需要從一個類說起。MeasureSpec。
MeasureSpec
MeasureSpec 是 View.java 中一個靜態(tài)類
/**
* MeasureSpec類的源碼分析
**/
public class MeasureSpec {
// 進位大小 = 2的30次方
// int的大小為32位,所以進位30位 = 使用int的32和31位做標志位
private static final int MODE_SHIFT = 30;
// 運算遮罩:0x3為16進制,10進制為3,二進制為11
// 3向左進位30 = 11 00000000000(11后跟30個0)
// 作用:用1標注需要的值,0標注不要的值。因1與任何數(shù)做與運算都得任何數(shù)、0與任何數(shù)做與運算都得0
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
// UNSPECIFIED的模式設(shè)置:0向左進位30 = 00后跟30個0,即00 00000000000
// 通過高2位
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
// EXACTLY的模式設(shè)置:1向左進位30 = 01后跟30個0 ,即01 00000000000
public static final int EXACTLY = 1 << MODE_SHIFT;
// AT_MOST的模式設(shè)置:2向左進位30 = 10后跟30個0,即10 00000000000
public static final int AT_MOST = 2 << MODE_SHIFT;
/**
* makeMeasureSpec()方法
* 作用:根據(jù)提供的size和mode得到一個詳細的測量結(jié)果嗎,即measureSpec
**/
public static int makeMeasureSpec(int size, int mode) {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
// 設(shè)計目的:使用一個32位的二進制數(shù),其中:第32和第31位代表測量模式(mode)、后30位代表測量大?。╯ize)
}
/**
* getMode()方法
* 作用:通過measureSpec獲得測量模式(mode)
**/
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
// 即:測量模式(mode) = measureSpec & MODE_MASK;
// MODE_MASK = 運算遮罩 = 11 00000000000(11后跟30個0)
//原理:保留measureSpec的高2位(即測量模式)、使用0替換后30位
// 例如10 00..00100 & 11 00..00(11后跟30個0) = 10 00..00(AT_MOST),這樣就得到了mode的值
}
/**
* getSize方法
* 作用:通過measureSpec獲得測量大小size
**/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
// size = measureSpec & ~MODE_MASK;
// 原理類似上面,即 將MODE_MASK取反,也就是變成了00 111111(00后跟30個1),將32,31替換成0也就是去掉mode,保留后30位的size
}
}
MeasureSpec 代表測量規(guī)則,而它的手段則是用一個 int 數(shù)值來實現(xiàn)。我們知道一個 int 數(shù)值有 32 bit。MeasureSpec 將它的高 2 位用來代表測量模式 Mode,低 30 位用來代表數(shù)值大小 Size。
- wrap_content-> MeasureSpec.AT_MOST
- match_parent -> MeasureSpec.EXACTLY
- 具體值 -> MeasureSpec.EXACTLY
實際使用
/**
* MeasureSpec類的具體使用
**/
// 1. 獲取測量模式(Mode)
int specMode = MeasureSpec.getMode(measureSpec)
// 2. 獲取測量大?。⊿ize)
int specSize = MeasureSpec.getSize(measureSpec)
// 3. 通過Mode 和 Size 生成新的SpecMode
int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);
上面講了那么久MeasureSpec,那么MeasureSpec值到底是如何計算得來?
結(jié)論:子View的MeasureSpec值根據(jù)子View的布局參數(shù)(LayoutParams)和父容器的MeasureSpec值計算得來的,具體計算邏輯封裝在getChildMeasureSpec()里。如下圖:

- 子view的大小由父view的MeasureSpec值 和 子view的LayoutParams屬性 共同決定
下面,我們來看getChildMeasureSpec()的源碼分析:
/**
* 源碼分析:getChildMeasureSpec()
* 作用:根據(jù)父視圖的MeasureSpec & 布局參數(shù)LayoutParams,計算單個子View的MeasureSpec
* 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams屬性 共同決定
**/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//參數(shù)說明
* @param spec 父view的詳細測量值(MeasureSpec)
* @param padding view當(dāng)前尺寸的的內(nèi)邊距和外邊距(padding,margin)
* @param childDimension 子視圖的布局參數(shù)(寬/高)
//父view的測量模式
int specMode = MeasureSpec.getMode(spec);
//父view的大小
int specSize = MeasureSpec.getSize(spec);
//通過父view計算出的子view = 父大小-邊距(父要求的大小,但子view不一定用這個值)
int size = Math.max(0, specSize - padding);
//子view想要的實際大小和模式(需要計算)
int resultSize = 0;
int resultMode = 0;
//通過父view的MeasureSpec和子view的LayoutParams確定子view的大小
// 當(dāng)父View的模式為EXACITY時,父view強加給子View確切的值
//一般是父View設(shè)置為match_parent或者固定值的ViewGroup
switch (specMode) {
case MeasureSpec.EXACTLY:
// 當(dāng)子View的LayoutParams>0,即有確切的值
if (childDimension >= 0) {
//子View大小為子自身所賦的值,模式大小為EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
// 當(dāng)子View的LayoutParams為MATCH_PARENT時(-1)
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子view大小為父view大小,模式為EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
// 當(dāng)子view的LayoutParams為WRAP_CONTENT時(-2)
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子view決定自己的大小,但最大不能超過父view,模式為AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 當(dāng)父View的模式為AT_MOST時,父view強加給子View一個最大的值。(一般是父view設(shè)置為wrap_content)
case MeasureSpec.AT_MOST:
// 道理同上
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 當(dāng)父View的模式為UNSPECIFIED時,父容器不對View有任何限制,要多大給多大
// 多見于ListView、GridView
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// 子view大小為子自身所賦的值
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 因為父View為UNSPECIFIED,所以MATCH_PARENT的話子類大小為0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 因為父view為UNSPECIFIED,所以WRAP_CONTENT的話子類大小為0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

MeasureSpec.UNSPECIFIED
子元素告訴父容器它的寬高想要多大就要多大,你不要限制我(自己的事情自己做主,沒有任何限制)。一般開發(fā)者幾乎不需要處理這種情況,在 ScrollView 或者是 AdapterView 中都會處理這樣的情況。所以我們可以忽視它。本文中的示例,基本上會跳過它。
MeasureSpec.EXACTLY
此模式說明可以給子元素一個精確的數(shù)值
MeasureSpec.AT_MOST
當(dāng)一個 View 的 layout_width 或者 layout_height 的取值為 wrap_content 時,它的測量模式就是 MeasureSpec.AT_MOST。
此模式下,子 View 希望它的寬或者高由自己決定。ViewGroup 當(dāng)然要尊重它的要求,但是也有個前提,那就是子視圖不能超過ViewGroup 提供的最大值,也就是它期望寬高不能超過父類提供的建議寬高。(自己的事情只能在一個范圍內(nèi)做主)

了解上面的測量模式后,我們就要動手編寫實例來驗證一些想法了。
自定義 View
我的目標是定義一個文本框,中間顯示黑色文字,背景色為綠色。
我們可以輕松地進行編碼。首先,我們定義好它需要的屬性,然后編寫它的 java 代碼。
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TestView">
<attr name="android:text" format="string" />
<attr name="android:textSize" format="dimension"/>
</declare-styleable>
</resources>
TestView.java
public class TestView extends View {
private int mTextSize;
private TextPaint mPaint;
private String mText;
public TestView(Context context) {
this(context,null);
}
public TestView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public TestView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.TestView);
mText = ta.getString(R.styleable.TestView_android_text);
mTextSize = ta.getDimensionPixelSize(R.styleable.TestView_android_textSize,24);
ta.recycle();
mPaint = new TextPaint();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.BLACK);
mPaint.setTextSize(mTextSize);
mPaint.setTextAlign(Paint.Align.CENTER);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int cx = (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
int cy = (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;
canvas.drawColor(Color.RED);
if (TextUtils.isEmpty(mText)) {
return;
}
canvas.drawText(mText,cx,cy,mPaint);
}
}
布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_margin="20dp"
android:layout_height="match_parent">
<com.example.improve.TestView
android:layout_width="100dp"
android:layout_height="100dp"
android:text="test" />
</RelativeLayout>

我們可以看到在自定義 View 的 TestView 代碼中,我們并沒有做測量有關(guān)的工作,因為我們根本就沒有復(fù)寫它的 onMeasure() 方法。但它卻完成了任務(wù),給定 layout_width 和 layout_height 兩個屬性明確的值之后,它就能夠正常顯示了。我們再改變一下數(shù)值。
<com.example.improve.TestView
android:layout_width="match_parent"
android:layout_height="100dp"
android:text="test" />
將 layout_width 的值改為 match_parent,所以它的寬是由父類決定,但同樣它也正常。

我們已經(jīng)知道,上面的兩種情況其實就是對應(yīng) MeasureSpec.EXACTLY 這種測量模式,在這種模式下 TestView 本身不需要進行處理。
那么有人會問,如果 layout_width 或者 layout_height 的值為 wrap_content 的話,那么會怎么樣呢?
我們繼續(xù)測試觀察。
<com.example.improve.TestView
android:layout_width="wrap_content"
android:layout_height="100dp"
android:text="test" />

效果和前面的一樣,寬度和它的 ViewGroup 同樣了。我們再看。
<com.example.improve.TestView
android:layout_width="100dp"
android:layout_height="wrap_content"
android:text="test"/>

寬度正常,高度卻和 ViewGroup 一樣了。
再看一種情況
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="20dp">
<com.example.improve.TestView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test" />
</RelativeLayout>

這次可以看到,寬高都和 ViewGroup 一致了。
但是,這不是我想要的?。?/p>
wrap_content 對應(yīng)的測量模式是 MeasureSpec.AT_MOST,所以它的第一要求就是 size 是由 View 本身決定,最大不超過 ViewGroup 能給予的建議數(shù)值。
TestView 如果在寬高上設(shè)置 wrap_content 屬性,也就代表著,它的大小由它的內(nèi)容決定,在這里它的內(nèi)容其實就是它中間位置的字符串。顯然上面的不符合要求,那么就顯然需要我們自己對測量進行處理。
我們的思路可以如下:
- 對于 MeasureSpec.EXACTLY 模式,我們不做處理,將 ViewGroup 的建議數(shù)值作為最終的寬高。
- 對于 MeasureSpec.AT_MOST 模式,我們要根據(jù)自己的內(nèi)容計算寬高,但是數(shù)值不得超過 ViewGroup 給出的建議值。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
/**resultW 代表最終設(shè)置的寬,resultH 代表最終設(shè)置的高*/
int resultW = widthSize;
int resultH = heightSize;
int contentW = 0;
int contentH = 0;
/**重點處理 AT_MOST 模式,TestView 自主決定數(shù)值大小,但不能超過 ViewGroup 給出的
* 建議數(shù)值
* */
if (widthMode == MeasureSpec.AT_MOST) {
if (!TextUtils.isEmpty(mText)) {
contentW = (int) mPaint.measureText(mText);
contentW += getPaddingLeft() + getPaddingRight();
resultW = Math.min(contentW, widthSize);
}
}
if (heightMode == MeasureSpec.AT_MOST) {
if (!TextUtils.isEmpty(mText)) {
contentH = mTextSize;
contentH += getPaddingTop() + getPaddingBottom();
resultH = Math.min(contentH, heightSize);
}
}
//一定要設(shè)置這個函數(shù),不然會報錯
setMeasuredDimension(resultW, resultH);
}
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int cx = getPaddingLeft() + (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
int cy = getPaddingTop() + (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;
Paint.FontMetrics metrics = mPaint.getFontMetrics();
cy += metrics.descent;
canvas.drawColor(Color.GREEN);
if (TextUtils.isEmpty(mText)) {
return;
}
canvas.drawText(mText, cx, cy, mPaint);
}
代碼并不難,我們可以做驗證。
<com.example.improve.TestView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:paddingTop="10dp"
android:paddingRight="10dp"
android:text="test"
android:textSize="24sp" />

在 MeasureSpec.EXACTLY 模式下同樣沒有問題。
現(xiàn)在,我們已經(jīng)掌握了自定義 View 的測量方法,其實也很簡單的嘛。
但是,還沒有完。我們驗證的剛剛是自定義 View,對于 ViewGroup 的情況是有些許不同的。
View 和 ViewGroup,雞生蛋,蛋生雞的關(guān)系
ViewGroup 是 View 的子類,但是 ViewGroup 的使命卻是裝載和組織 View。這好比是母雞是雞,母雞下蛋是為了孵化小雞,小雞長大后如果是母雞又下蛋,那么到底是蛋生雞還是雞生蛋?

自定義 View 的測量,我們已經(jīng)掌握了,那現(xiàn)在我們編碼來測試自定義 ViewGroup 時的測量變現(xiàn)。
假設(shè)我們要制定一個 ViewGroup,我們就給它起一個名字叫 TestViewGroup 好了,它里面的子元素按照對角線鋪設(shè),前面說過 ViewGroup 本質(zhì)上也是一個 View,只不過它多了布局子元素的義務(wù)。既然是 View 的話,那么自定義一個 ViewGroup 也需要從測量開始,問題的關(guān)鍵是如何準確地得到這個 ViewGroup 尺寸信息?
我們還是需要仔細討論。
- 當(dāng) TestViewGroup 測量模式為 MeasureSpec.EXACTLY 時,這時候的尺寸就可以按照父容器傳遞過來的建議尺寸。要知道 ViewGroup 也有自己的 parent,在它的父容器中,它也只是一個 View。
- 當(dāng) TestViewGroup 測量模式為 MeasureSpec.AT_MOST 時,這就需要 TestViewGroup 自己計算尺寸數(shù)值。就上面給出的信息而言,TestViewGroup 的尺寸非常簡單,那就是用自身 padding + 各個子元素的尺寸(包含子元素的寬高+子元素設(shè)置的 marging )得到一個可能的尺寸數(shù)值。然后用這個尺寸數(shù)值與 TestViewGroup 的父容器給出的建議 Size 進行比較,最終結(jié)果取最較小值。
- 當(dāng) TestViewGroup 測量成功后,就需要布局了。自定義 View 基本上不要處理這一塊,但是自定義 ViewGroup,這一部分卻不可缺少。onLayout()是實現(xiàn)所有子控件布局的函數(shù)。注意,是所有子控件!那它自己的布局怎么辦?后面我們再講,先講講在onLayout()中我們應(yīng)該做什么。
我們先看看ViewGroup onLayout()函數(shù)的默認行為是什么
在ViewGroup.java中
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
是一個抽象方法,說明凡是派生自ViewGroup的類都必須自己去實現(xiàn)這個方法。像LinearLayout、RelativeLayout等布局,都是重寫了這個方法,然后在內(nèi)部按照各自的規(guī)則對子視圖進行布局的。
接下來,我們就可以具體編碼了。
public class TestViewGroup extends ViewGroup {
public TestViewGroup(Context context) {
this(context,null);
}
public TestViewGroup(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public TestViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
//只關(guān)心子元素的 margin 信息,所以這里用 MarginLayoutParams
return new MarginLayoutParams(getContext(),attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
/**resultW 代表最終設(shè)置的寬,resultH 代表最終設(shè)置的高*/
int resultW = widthSize;
int resultH = heightSize;
/**計算尺寸的時候要將自身的 padding 考慮進去*/
int contentW = getPaddingLeft() + getPaddingRight();
int contentH = getPaddingTop() + getPaddingBottom();
/**對子元素進行尺寸的測量,這一步必不可少*/
measureChildren(widthMeasureSpec,heightMeasureSpec);
MarginLayoutParams layoutParams = null;
for ( int i = 0;i < getChildCount();i++ ) {
View child = getChildAt(i);
layoutParams = (MarginLayoutParams) child.getLayoutParams();
//子元素不可見時,不參與布局,因此不需要將其尺寸計算在內(nèi)
if ( child.getVisibility() == View.GONE ) {
continue;
}
contentW += child.getMeasuredWidth()
+ layoutParams.leftMargin + layoutParams.rightMargin;
contentH += child.getMeasuredHeight()
+ layoutParams.topMargin + layoutParams.bottomMargin;
}
/**重點處理 AT_MOST 模式,TestViewGroup 通過子元素的尺寸自主決定數(shù)值大小,但不能超過
* ViewGroup 給出的建議數(shù)值
* */
if ( widthMode == MeasureSpec.AT_MOST ) {
resultW = contentW < widthSize ? contentW : widthSize;
}
if ( heightMode == MeasureSpec.AT_MOST ) {
resultH = contentH < heightSize ? contentH : heightSize;
}
//一定要設(shè)置這個函數(shù),不然會報錯
setMeasuredDimension(resultW,resultH);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int topStart = getPaddingTop();
int leftStart = getPaddingLeft();
int childW = 0;
int childH = 0;
MarginLayoutParams layoutParams = null;
for ( int i = 0;i < getChildCount();i++ ) {
View child = getChildAt(i);
layoutParams = (MarginLayoutParams) child.getLayoutParams();
//子元素不可見時,不參與布局,因此不需要將其尺寸計算在內(nèi)
if ( child.getVisibility() == View.GONE ) {
continue;
}
childW = child.getMeasuredWidth();
childH = child.getMeasuredHeight();
leftStart += layoutParams.leftMargin;
topStart += layoutParams.topMargin;
child.layout(leftStart,topStart, leftStart + childW, topStart + childH);
leftStart += childW + layoutParams.rightMargin;
topStart += childH + layoutParams.bottomMargin;
}
}
}
然后我們將之添加進 xml 布局文件中進行測試。
<?xml version="1.0" encoding="utf-8"?>
<com.example.improve.TestViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.example.improve.TestView
android:layout_width="120dp"
android:layout_height="wrap_content"
android:paddingLeft="2dp"
android:paddingTop="2dp"
android:paddingRight="2dp"
android:text="test"
android:textSize="24sp" />
<TextView
android:layout_width="120dp"
android:layout_height="50dp"
android:background="#00ff40"
android:paddingLeft="2dp"
android:paddingTop="2dp"
android:paddingRight="2dp"
android:text="test"
android:textSize="24sp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"
android:text="test" />
</com.example.improve.TestViewGroup>

再試驗一下給 TestViewGroup 加上固定寬高。
<?xml version="1.0" encoding="utf-8"?>
<com.example.improve.TestViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="350dp"
android:layout_height="400dp"
android:background="#c3c3c3">
<com.example.improve.TestView
android:layout_width="120dp"
android:layout_height="wrap_content"
android:paddingLeft="2dp"
android:paddingTop="2dp"
android:paddingRight="2dp"
android:text="test"
android:textSize="24sp" />
<TextView
android:layout_width="120dp"
android:layout_height="50dp"
android:background="#00ff40"
android:paddingLeft="2dp"
android:paddingTop="2dp"
android:paddingRight="2dp"
android:text="test"
android:textSize="24sp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"
android:text="test" />
</com.example.improve.TestViewGroup>
結(jié)果如下:

自此,我們也知道了自定義 ViewGroup 的基本步驟,并且能夠處理 ViewGroup 的各種測量模式。
但是,在現(xiàn)實工作開發(fā)過程中,需求是不定的,我上面講的內(nèi)容只是基本的規(guī)則,大家熟練于心的時候才能從容應(yīng)對各種狀況。
getMeasuredWidth()與getWidth()
趁熱打鐵,就這個例子,我們講一個很容易出錯的問題:getMeasuredWidth()與getWidth()的區(qū)別。他們的值大部分時間都是相同的,但意義確是根本不一樣的,我們就來簡單分析一下。
- 首先getMeasureWidth()方法在measure()過程結(jié)束后就可以獲取到了,而getWidth()方法要在layout()過程結(jié)束后才能獲取到。
- getMeasureWidth()方法中的值是通過setMeasuredDimension()方法來進行設(shè)置的,而getWidth()方法中的值則是通過layout(left,top,right,bottom)方法設(shè)置的。
setMeasuredDimension()提供的測量結(jié)果只是為布局提供建議,最終的取用與否要看layout()函數(shù)。大家再看看我們上面寫的TestViewGroup,是不是我們自己使用child.layout(leftStart,topStart, leftStart + childW, topStart + childH)來定義了各個子控件所應(yīng)在的位置:
childW = child.getMeasuredWidth();
childH = child.getMeasuredHeight();
leftStart += layoutParams.leftMargin;
topStart += layoutParams.topMargin;
child.layout(leftStart, topStart, leftStart + childW, topStart + childH);
從代碼中可以看到,我們使用child.layout(leftStart, topStart, leftStart + childW, topStart + childH);來布局控件的位置,其中g(shù)etWidth()的取值就是這里的右坐標減去左坐標的寬度;因為我們這里的寬度是,leftStart + childW,而getMeasuredWidth()與getWidth()的值是一樣的。如果我們在調(diào)用layout()的時候傳進去的寬度值不與getMeasuredWidth()相同,那必然getMeasuredWidth()與getWidth()的值就不再一樣了。
一定要注意的一點是:getMeasureWidth()方法在measure()過程結(jié)束后就可以獲取到了,而getWidth()方法要在layout()過程結(jié)束后才能獲取到。再重申一遍?。。?/strong>
TestViewGroup自己什么時候被布局
在onLayout()中布局它所有的子控件。那它自己什么時候被布局呢?它當(dāng)然也有父控件,它的布局也是在父控件中由它的父控件完成的,就這樣一層一層地向上由各自的父控件完成對自己的布局。真到所有控件的最頂層結(jié)點,在所有的控件的最頂部有一個ViewRoot,它才是所有控件的最終祖先結(jié)點。那讓我們來看看它是怎么來做的吧。
/* final 標識符 , 不能被重載 , 參數(shù)為每個視圖位于父視圖的坐標軸
* @param l Left position, relative to parent
* @param t Top position, relative to parent
* @param r Right position, relative to parent
* @param b Bottom position, relative to parent
*/
public final void layout(int l, int t, int r, int b) {
boolean changed = setFrame(l, t, r, b); //設(shè)置每個視圖位于父視圖的坐標軸
if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
}
onLayout(changed, l, t, r, b);//回調(diào)onLayout函數(shù) ,設(shè)置每個子視圖的布局
mPrivateFlags &= ~LAYOUT_REQUIRED;
}
mPrivateFlags &= ~FORCE_LAYOUT;
在setFrame(l,t,r,b)就是設(shè)置自己的位置,設(shè)置結(jié)束以后才會調(diào)用onLayout(changed, l, t, r, b)來設(shè)置內(nèi)部所有子控件的位置。
OK啦,到這里有關(guān)onMeasure()和onLayout()的內(nèi)容就講完啦,想必大家應(yīng)該也對整個布局流程有了一個清楚的認識了,下面我們再看一個緊要的問題:如何得到自定義控件的左右間距margin值。
獲取子控件Margin的方法
我會先簡單粗暴的教大家怎么先獲取到margin值,然后再細講為什么這樣寫,他們的原理是怎樣的。
如果要自定義ViewGroup支持子控件的layout_margin參數(shù),則自定義的ViewGroup類必須重載generateLayoutParams()函數(shù),并且在該函數(shù)中返回一個ViewGroup.MarginLayoutParams派生類對象,這樣才能使用margin參數(shù)。
我們在上面TestViewGroup例子的基礎(chǔ)上,添加上layout_margin參數(shù);
<?xml version="1.0" encoding="utf-8"?>
<com.as.customview.TestViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:background="#ff00ff"
android:layout_height="match_parent">
<com.as.customview.TestView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_margin="10dp"
android:text="test"
android:background="#FF5722"
android:textSize="60sp" />
<com.as.customview.TestView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_margin="10dp"
android:text="test"
android:background="#4CAF50"
android:textSize="60sp" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:src="@mipmap/ic_launcher"
android:text="test" />
</com.as.customview.TestViewGroup>
我們在每個TestView中都添加了一android:layout_margin參數(shù),而且值是10dp;背景也都分別改為了橙色,綠色和一張圖片;
現(xiàn)在我們運行一上,看看效果:

我們在onLayout()中沒有根據(jù)Margin來布局,當(dāng)然不會出現(xiàn)有關(guān)Margin的效果啦。需要特別注意的是,如果我們在onLayout()中根據(jù)margin來布局的話,那么我們在onMeasure()中計算TestViewGroup的大小時,也要加上margin,不然會導(dǎo)致TestViewGroup太小,而控件顯示不全的問題。費話不多說,我們直接看代碼實現(xiàn)。
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
}
首先,在TestViewGroup在初始化子控件時,會調(diào)用LayoutParams generateLayoutParams(LayoutParams p)來為子控件生成對應(yīng)的布局屬性,但默認只是生成layout_width和layout_height所以對應(yīng)的布局參數(shù),即在正常情況下的generateLayoutParams()函數(shù)生成的LayoutParams實例是不能夠取到margin值的。即:
/**
*從指定的XML中獲取對應(yīng)的layout_width和layout_height值
*/
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
/*
*如果要使用默認的構(gòu)造方法,就生成layout_width="wrap_content"、layout_height="wrap_content"對應(yīng)的參數(shù)
*/
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
所以,如果我們還需要margin相關(guān)的參數(shù)就只能重寫generateLayoutParams()函數(shù)了:
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
由于generateLayoutParams()的返回值是LayoutParams實例,而MarginLayoutParams是派生自LayoutParam的;所以根據(jù)類的多態(tài)的特性,可以直接將此時的LayoutParams實例直接強轉(zhuǎn)成MarginLayoutParams實例;
所以下面這句在這里是不會報錯的:
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
大家也可以為了安全起見利用instanceOf來做下判斷,如下:
MarginLayoutParams lp = null
if (child.getLayoutParams() instanceof MarginLayoutParams) {
lp = (MarginLayoutParams) child.getLayoutParams();
}
所以整體來講,就是利用了類的多態(tài)特性!下面來看看MarginLayoutParams和generateLayoutParams()都做了什么。
generateLayoutParams()實現(xiàn)
//位于ViewGrop.java中
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
public LayoutParams(Context c, AttributeSet attrs) {
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
setBaseAttributes(a,
R.styleable.ViewGroup_Layout_layout_width,
R.styleable.ViewGroup_Layout_layout_height);
a.recycle();
}
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
width = a.getLayoutDimension(widthAttr, "layout_width");
height = a.getLayoutDimension(heightAttr, "layout_height");
}
從上面的代碼中明顯可以看出,generateLayoutParams()調(diào)用LayoutParams()產(chǎn)生布局信息,而LayoutParams()最終調(diào)用setBaseAttributes()來獲得對應(yīng)的寬,高屬性。
這里是通過TypedArray對自定義的XML進行值提取的過程,難度不大,不再細講。從這里也可以看到,generateLayoutParams生成的LayoutParams屬性只有l(wèi)ayout_width和layout_height的屬性值。
下面再來看看MarginLayoutParams的具體實現(xiàn),其實通過上面的過程,大家也應(yīng)該想到,它也是通過TypeArray來解析自定義屬性來獲得用戶的定義值的(大家看到長代碼不要害怕,先列出完整代碼,下面會分段講):
public MarginLayoutParams(Context c, AttributeSet attrs) {
super();
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
int margin = a.getDimensionPixelSize(
com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
if (margin >= 0) {
leftMargin = margin;
topMargin = margin;
rightMargin= margin;
bottomMargin = margin;
} else {
leftMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
UNDEFINED_MARGIN);
rightMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginRight,
UNDEFINED_MARGIN);
topMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginTop,
DEFAULT_MARGIN_RESOLVED);
startMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginStart,
DEFAULT_MARGIN_RELATIVE);
endMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginEnd,
DEFAULT_MARGIN_RELATIVE);
}
a.recycle();
}
這段代碼分為兩部分:
第一部分:提取layout_margin的值并設(shè)置
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
int margin = a.getDimensionPixelSize(
com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
if (margin >= 0) {
leftMargin = margin;
topMargin = margin;
rightMargin= margin;
bottomMargin = margin;
} else {
…………
}
在這段代碼中就是通過提取layout_margin的值來設(shè)置上,下,左,右邊距的。
第二部分:如果用戶沒有設(shè)置layout_margin,而是單個設(shè)置的,那么就一個個提取,代碼如下:
leftMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
UNDEFINED_MARGIN);
rightMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginRight,
UNDEFINED_MARGIN);
topMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginTop,
DEFAULT_MARGIN_RESOLVED);
startMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginStart,
DEFAULT_MARGIN_RELATIVE);
endMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginEnd,
DEFAULT_MARGIN_RELATIVE);
這里就是對layout_marginLeft、layout_marginRight、layout_marginTop、layout_marginBottom的值一個個提取的過程。
從這里大家也可以看到為什么非要重寫generateLayoutParams()函數(shù)了,就是因為默認的generateLayoutParams()函數(shù)只會提取layout_width、layout_height的值,只有MarginLayoutParams()才具有提取margin間距的功能!?。?!
TestViewGroup 作為一個演示用的例子,只為了說明測量規(guī)則和基本的自定義方法。對于 Android 開發(fā)初學(xué)者而言,還是要多閱讀代碼,關(guān)鍵是要多臨摹別人的優(yōu)秀的自定義 View 或者 ViewGroup。
我個人覺得,嘗試自己動手去實現(xiàn)一個流式標簽控件,對于提高自定義 ViewGroup 的能力是有很大的提高,因為只有在自己實踐中思考,在思考和實驗的過程你才會深刻的理解測量機制的用途。
不過自定義一個流式標簽控件是另外一個話題了,也許我會另外開一篇來講解,不過我希望大家親自動手去實現(xiàn)它。

洋洋灑灑寫了這么多的內(nèi)容,其實基本上已經(jīng)完結(jié)了,已經(jīng)不耐煩的同學(xué)可以直接跳轉(zhuǎn)到后面的總結(jié)。但是,對于有鉆研精神的同學(xué)來講,其實還不夠。還沒有完。
問題1:到底是誰在測量 View ?
問題2:到底是什么時候需要測量 View ?
針對問題 1:
我們在自定義 TestViewGroup 的時候,在 onMeasure() 方法中,通過了一個 API 對子元素進行了測量,這個 API 就是 measureChildren()。這個方法進行了什么樣的處理呢?我們可以去看看。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
/**
* 分析2:measureChild()
* 作用:a. 計算單個子View的MeasureSpec
* b. 測量每個子View最后的寬 / 高:調(diào)用子View的measure()
**/
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
// 1. 獲取子視圖的布局參數(shù)
final LayoutParams lp = child.getLayoutParams();
// 2. 根據(jù)父視圖的MeasureSpec & 布局參數(shù)LayoutParams,計算單個子View的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,// 獲取 ChildView 的 widthMeasureSpec
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,// 獲取 ChildView 的 heightMeasureSpec
mPaddingTop + mPaddingBottom, lp.height);
// 3. 將計算好的子View的MeasureSpec值傳入measure(),進行最后的測量
// 下面的流程即類似單一View的過程,此處不作過多描述
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
代碼簡短易懂,分別調(diào)用 child 的 measure() 方法。值得注意的是,傳遞給 child 的測量規(guī)格已經(jīng)發(fā)生了變化,比如 widthMeasureSpec 變成了 childWidthMeasureSpec。原因是這兩行代碼:
一開始我們就了解子視圖是如何測量的
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
我們繼續(xù)向前,ViewGroup 的 measureChild() 方法最終會調(diào)用 View.measure() 方法。我們進一步跟蹤。
/**
* 源碼分析:measure()
* 定義:Measure過程的入口;屬于View.java類 & final類型,即子類不能重寫此方法
* 作用:基本測量邏輯的判斷
**/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 參數(shù)說明:View的寬 / 高測量規(guī)格
...
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
// 計算視圖大小 ->>分析1
} else {
...
}
/**
* 分析1:onMeasure()
* 作用:a. 根據(jù)View寬/高的測量規(guī)格計算View的寬/高值:getDefaultSize()
* b. 存儲測量后的View寬 / 高:setMeasuredDimension()
**/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 參數(shù)說明:View的寬 / 高測量規(guī)格
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected int getSuggestedMinimumWidth() {
//mMinWidth = android:minWidth屬性所指定的值;
return (mBackground == null) ? mMinWidth : max(mMinWidth,mBackground.getMinimumWidth());
}
//getSuggestedMinimumHeight()同理
/**
* 分析2:setMeasuredDimension()
* 作用:存儲測量后的View寬 / 高
* 注:該方法即為我們重寫onMeasure()所要實現(xiàn)的最終目的
**/
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
// 將測量后子View的寬 / 高值進行傳遞
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
// 由于setMeasuredDimension()的參數(shù)是從getDefaultSize()獲得的
// 下面我們繼續(xù)看getDefaultSize()的介紹
/**
* 分析3:getDefaultSize()
* 作用:根據(jù)View寬/高的測量規(guī)格計算View的寬/高值
**/
public static int getDefaultSize(int size, int measureSpec) {
// 參數(shù)說明:
// size:提供的默認大小
// measureSpec:寬/高的測量規(guī)格(含模式 & 測量大?。?
// 設(shè)置默認大小
int result = size;
// 獲取寬/高測量規(guī)格的模式 & 測量大小
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
// 模式為UNSPECIFIED時,使用提供的默認大小 = 參數(shù)Size
case MeasureSpec.UNSPECIFIED:
result = size;
break;
// 模式為AT_MOST,EXACTLY時,使用View測量后的寬/高值 = measureSpec中的Size
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
// 返回View的寬/高值
return result;
}
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
//返回背景圖Drawable的原始寬度
return intrinsicWidth > 0 ? intrinsicWidth :0 ;
}
// 由源碼可知:mBackground.getMinimumWidth()的大小 = 背景圖Drawable的原始寬度
// 若無原始寬度,則為0;
// 注:BitmapDrawable有原始寬度,而ShapeDrawable沒有
最后,我們在看看測量的流程圖

Activity 中的道,最頂層的那個 View?
道生一,一生二,二生三,三生萬物,萬物負陰而抱陽,沖氣以為和。– 《道德經(jīng)》
我們已經(jīng)知道,不管是對于 View 還是 ViewGroup 而言,測量的起始是 measure() 方法,沿著控件樹一路遍歷下去。那么,對于 Android 一個 Activity 而言,它的頂級 View 或者頂級 ViewGroup 是哪一個呢?
從 setContentView 說起
我們知道給 Activity 布局的時候,在 onCreate() 中設(shè)置 setContentView() 的資源文件就是我們普通開發(fā)者所能想到的比較頂層的 View 了。比如在 activity_main.xml 中設(shè)置一個 RelativeLayout,那么這個 RelativeLayout 就是 Activity 最頂層的 View 嗎?誰調(diào)用它的 measure() 方法觸發(fā)整個控件樹的測量?
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
initActionBar();
}
public Window getWindow() {
return mWindow;
}
/**
* Abstract base class for a top-level window look and behavior policy. An
* instance of this class should be used as the top-level view added to the
* window manager. It provides standard UI policies such as a background, title
* area, default key processing, etc.
*
* <p>The only existing implementation of this abstract class is
* android.policy.PhoneWindow, which you should instantiate when needing a
* Window. Eventually that class will be refactored and a factory method
* added for creating Window instances without knowing about a particular
* implementation.
*/
public abstract class Window {
}
public class PhoneWindow extends Window implements MenuBuilder.Callback {
}
可以看到,調(diào)用 Activity.setContentView() 其實就是調(diào)用 PhoneWindow.setContentView()。
PhoneWindow.java
@Override
public void setContentView(View view) {
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
if (mContentParent == null) {
installDecor();
} else {
mContentParent.removeAllViews();
}
mContentParent.addView(view, params);
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
注意,在上面代碼中顯示,通過 setContentView 傳遞進來的 view 被添加到了一個 mContentParent 變量上了,所以可以回答上面的問題,通過 setContentView() 中傳遞的 View 并不是 Activity 最頂層的 View。我們再來看看 mContentParent。
它只是一個 ViewGroup。我們再把焦點聚集到 installDecor() 這個函數(shù)上面。
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
// Set up decor part of UI to ignore fitsSystemWindows if appropriate.
mDecor.makeOptionalFitsSystemWindows();
mTitleView = (TextView)findViewById(com.android.internal.R.id.title);
if (mTitleView != null) {
mTitleView.setLayoutDirection(mDecor.getLayoutDirection());
if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
View titleContainer = findViewById(com.android.internal.R.id.title_container);
if (titleContainer != null) {
titleContainer.setVisibility(View.GONE);
} else {
mTitleView.setVisibility(View.GONE);
}
if (mContentParent instanceof FrameLayout) {
((FrameLayout)mContentParent).setForeground(null);
}
} else {
mTitleView.setText(mTitle);
}
} else {
mActionBar = (ActionBarView) findViewById(com.android.internal.R.id.action_bar);
}
}
}
代碼很長,我刪除了一些與主題無關(guān)的代碼。這個方法體內(nèi)引出了一個 mDecor 變量,它通過 generateDecor() 方法創(chuàng)建。DecorView 是 PhoneWindow 定義的一個內(nèi)部類,實際上是一個 FrameLayout。
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
}
我們回到 generate() 方法
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
DecorView 怎么創(chuàng)建的我們已經(jīng)知曉,現(xiàn)在看看 mContentParent 創(chuàng)建方法 generateLayout()。它傳遞進了一個 DecorView,所以它與 mDecorView 肯定有某種關(guān)系。
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
WindowManager.LayoutParams params = getAttributes();
// Inflate the window decor.
// Embedded, so no decoration is needed.
layoutResource = com.android.internal.R.layout.screen_simple;
// System.out.println("Simple!");
mDecor.startChanging();
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
mDecor.finishChanging();
return contentParent;
}
原代碼很長,我刪除了一些繁瑣的代碼,整個流程變得很清晰,這個方法內(nèi) inflate 了一個 xml 文件,然后被添加到了 mDecorView。而 mContentParent 就是這個被添加進去的 view 中。
這個 xml 文件是 com.android.internal.R.layout.screen_simple,我們可以從 SDK 包中找出它來。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
就是一個 LinearLayout ,方向垂直。2 個元素,一個是 actionbar,一個是 content。并且 ViewStub 導(dǎo)致 actionbar 需要的時候才會進行加載。
總之由以上信息,我們可以得到 Activity 有一個 PhoneWindow 對象,PhoneWindow 中有一個 DecorView,DecorView 內(nèi)部有一個 LinearLayout,LinearLayout 中存在 id 為 android:id/content 的布局 mContentParent。 mContentParent加載Activity 通過 setContentView 傳遞進來的 View,所以整個結(jié)構(gòu)呼之欲出。
注意:因為代碼有刪簡,實際上 LinearLayout 由兩部分組成,下面的是 Content 無疑,上面的部分不一定是 ActionBar,也可能是 title,不過這不影響我們,我們只需要記住 content 就好了。

DecorView 才是 Activity 中整個控件樹的根。
誰測繪了頂級 View ?
既然 DecorView 是整個測繪的發(fā)起點,那么誰對它進行了測繪?誰調(diào)用了它的 measure() 方法,從而導(dǎo)致整個控件樹自上至下的尺寸測量?
我們平常開發(fā)知道調(diào)用一個 View.requestLayout() 方法,可以引起界面的重新布局,那么 requestLayout() 干了什么?
我們再回到 PhoneWindow 的 setContentView() 中來。
public void setContentView(View view, ViewGroup.LayoutParams params) {
if (mContentParent == null) {
installDecor();
} else {
mContentParent.removeAllViews();
}
mContentParent.addView(view, params);
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
我們看看 mContentParent.addView(view, params) 的時候發(fā)生了什么。
public void addView(View child, int index, LayoutParams params) {
if (DBG) {
System.out.println(this + " addView");
}
// addViewInner() will call child.requestLayout() when setting the new LayoutParams
// therefore, we call requestLayout() on ourselves before, so that the child's request
// will be blocked at our level
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
我們這篇文章就到這里