測量 View 就是測量一個矩形
透過另一個視角來觀察,所有的 Widget奈梳,我們使用的小控件都是Widget杈湾。如果TextView和Buttton等
因此,自定義 View 的第一步攘须,我們要在心里默念 – 我們現(xiàn)在要確定一個矩形了漆撞!
既然是矩形,那么它肯定有明確的寬高和位置坐標(biāo)于宙,寬高是在測量階段得出浮驳。然后在布局階段,確定好位置信息對矩形進行布局捞魁,之后的視覺效果就交給繪制流程了至会,我們是最好的畫家。
布局繪畫涉及兩個過程:測量過程和布局過程谱俭。測量過程通過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 找一個父容器進行觀察例驹。父容器背景由特定顏色標(biāo)識。
一
<?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位做標(biāo)志位
private static final int MODE_SHIFT = 30;
// 運算遮罩:0x3為16進制蜘拉,10進制為3,二進制為11
// 3向左進位30 = 11 00000000000(11后跟30個0)
// 作用:用1標(biāo)注需要的值有鹿,0標(biāo)注不要的值旭旭。因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位代表測量大熊怼(size)
}
/**
* 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. 獲取測量大信(Size)
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
我的目標(biāo)是定義一個文本框崔挖,中間顯示黑色文字,背景色為綠色庵寞。
我們可以輕松地進行編碼狸相。首先,我們定義好它需要的屬性捐川,然后編寫它的 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 一致了滤港。
但是,這不是我想要的芭颗 溅漾!
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)鍵是如何準(zhǔ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()的取值就是這里的右坐標(biāo)減去左坐標(biāo)的寬度;因為我們這里的寬度是,leftStart + childW机蔗,而getMeasuredWidth()與getWidth()的值是一樣的蒲祈。如果我們在調(diào)用layout()的時候傳進去的寬度值不與getMeasuredWidth()相同,那必然getMeasuredWidth()與getWidth()的值就不再一樣了萝嘁。
一定要注意的一點是:getMeasureWidth()方法在measure()過程結(jié)束后就可以獲取到了梆掸,而getWidth()方法要在layout()過程結(jié)束后才能獲取到。再重申一遍Q姥浴K崆铡!
TestViewGroup自己什么時候被布局
在onLayout()中布局它所有的子控件咱枉。那它自己什么時候被布局呢卑硫?它當(dāng)然也有父控件,它的布局也是在父控件中由它的父控件完成的蚕断,就這樣一層一層地向上由各自的父控件完成對自己的布局欢伏。真到所有控件的最頂層結(jié)點,在所有的控件的最頂部有一個ViewRoot亿乳,它才是所有控件的最終祖先結(jié)點颜懊。那讓我們來看看它是怎么來做的吧。
/* final 標(biāo)識符 , 不能被重載 河爹, 參數(shù)為每個視圖位于父視圖的坐標(biāo)軸
* @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è)置每個視圖位于父視圖的坐標(biāo)軸
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間距的功能M榷獭F凉俊;婷巍!
TestViewGroup 作為一個演示用的例子赴魁,只為了說明測量規(guī)則和基本的自定義方法卸奉。對于 Android 開發(fā)初學(xué)者而言,還是要多閱讀代碼颖御,關(guān)鍵是要多臨摹別人的優(yōu)秀的自定義 View 或者 ViewGroup择卦。
我個人覺得,嘗試自己動手去實現(xiàn)一個流式標(biāo)簽控件郎嫁,對于提高自定義 ViewGroup 的能力是有很大的提高秉继,因為只有在自己實踐中思考,在思考和實驗的過程你才會深刻的理解測量機制的用途泽铛。
不過自定義一個流式標(biāo)簽控件是另外一個話題了尚辑,也許我會另外開一篇來講解,不過我希望大家親自動手去實現(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);
}
我們這篇文章就到這里