本文原創(chuàng), 轉(zhuǎn)載請注明出處 : http://blog.csdn.net/qinjuning
上篇文章<< Android中measure過程、WRAP_CONTENT詳解以及xml布局文件解析流程淺析(上) >>中,我們
了解了View樹的轉(zhuǎn)換過程以及如何設(shè)置View的LayoutParams的。本文繼續(xù)沿著既定軌跡繼續(xù)未完成的job。
主要知識點(diǎn)如下:
1、MeasureSpc類說明
2、measure過程詳解(揭秘其細(xì)節(jié));
3、root View被添加至窗口時(shí),UI框架是如何設(shè)置其LayoutParams值得。
在講解measure過程前,我們非常有必要理解MeasureSpc類的使用,否則理解起來也只能算是囫圇吞棗。
1、MeasureSpc類說明
1.1 SDK 說明如下
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.
即:
MeasureSpc類封裝了父View傳遞給子View的布局(layout)要求。每個(gè)MeasureSpc實(shí)例代表寬度或者高度
(只能 是其一)要求。 它有三種模式:
①、UNSPECIFIED(未指定),父元素部隊(duì)自元素施加任何束縛,子元素可以得到任意想要的大小;
②、EXACTLY(完全),父元素決定自元素的確切大小,子元素將被限定在給定的邊界里而忽略它本身大小;
③、AT_MOST(至多),子元素至多達(dá)到指定大小的值。
常用的三個(gè)函數(shù):
static int getMode(int measureSpec) : 根據(jù)提供的測量值(格式)提取模式(上述三個(gè)模式之一)
static int getSize(int measureSpec) : 根據(jù)提供的測量值(格式)提取大小值(這個(gè)大小也就是我們通常所說的大小)
static int makeMeasureSpec(int size,int mode) : 根據(jù)提供的大小值和模式創(chuàng)建一個(gè)測量值(格式)
以上摘取自: <<
MeasureSpec介紹及使用詳解
>>
1.2 MeasureSpc類源碼分析 其為View.java類的內(nèi)部類,路徑:\frameworks\base\core\java\android\view\View.java
public class View implements ... { ... public static class MeasureSpec { private static final int MODE_SHIFT = 30; //移位位數(shù)為30 //int類型占32位,向右移位30位,該屬性表示掩碼值,用來與size和mode進(jìn)行"&"運(yùn)算,獲取對應(yīng)值。 private static final int MODE_MASK = 0x3 << MODE_SHIFT; //向右移位30位,其值為00 + (30位0) , 即 0x0000(16進(jìn)制表示) public static final int UNSPECIFIED = 0 << MODE_SHIFT; //向右移位30位,其值為01 + (30位0) , 即0x1000(16進(jìn)制表示) public static final int EXACTLY = 1 << MODE_SHIFT; //向右移位30位,其值為02 + (30位0) , 即0x2000(16進(jìn)制表示) public static final int AT_MOST = 2 << MODE_SHIFT; //創(chuàng)建一個(gè)整形值,其高兩位代表mode類型,其余30位代表長或?qū)挼膶?shí)際值。可以是WRAP_CONTENT、MATCH_PARENT或具體大小exactly size public static int makeMeasureSpec(int size, int mode) { return size + mode; } //獲取模式 ,與運(yùn)算 public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } //獲取長或?qū)挼膶?shí)際值 ,與運(yùn)算 public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } } ... }
MeasureSpec類的處理思路是:
①、右移運(yùn)算,使int 類型的高兩位表示模式的實(shí)際值,其余30位表示其余30位代表長或?qū)挼膶?shí)際值----可以是
WRAP_CONTENT、MATCH_PARENT或具體大小exactly size。
②、通過掩碼MODE_MASK進(jìn)行與運(yùn)算 “&”,取得模式(mode)以及長或?qū)?value)的實(shí)際值。
2、measure過程詳解
2.1 measure過程深入分析
之前的一篇博文 << Android中View繪制流程以及invalidate()等相關(guān)方法分析 >> ,我們從”二B程序員”的角度簡單 解了measure過程的調(diào)用過程。過了這么多,我們也該 升級了,- - 。現(xiàn)在請開始從”普通程序員”角度去理解這個(gè)
過 程。我們重點(diǎn)查看measure過程中地相關(guān)方法。
我們說過,當(dāng)UI框架開始繪制時(shí),皆是從ViewRoot.java類開始繪制的。
ViewRoot類簡要說明
: 任何顯示在設(shè)備中的窗口,例如:Activity、Dialog等,都包含一個(gè)ViewRoot實(shí)例,該
類 主要用來與遠(yuǎn)端 WindowManagerService交互以及控制(開始/銷毀)繪制。
Step 1
、 開始UI繪制 , 具體繪制方法則是:
路徑:\frameworks\base\core\java\android\view\ViewRoot.java public final class ViewRoot extends Handler implements ViewParent,View.AttachInfo.Callbacks { ... //mView對象指添加至窗口的root View ,對Activity窗口而言,則是DecorView對象。 View mView; //開始View繪制流程 private void performTraversals(){ ... //這兩個(gè)值我們在后面討論時(shí),在回過頭來看看是怎么賦值的。現(xiàn)在只需要記住其值MeasureSpec.makeMeasureSpec()構(gòu)建的。 int childWidthMeasureSpec; //其值由MeasureSpec類構(gòu)建 , makeMeasureSpec int childHeightMeasureSpec;//其值由MeasureSpec類構(gòu)建 , makeMeasureSpec // Ask host how big it wants to be host.measure(childWidthMeasureSpec, childHeightMeasureSpec); ... } ... }
Step 2 、
調(diào)用measure()方法去做一些前期準(zhǔn)備
measure()方法原型定義在View.java類中,final修飾符修飾,其不能被重載:
public class View implements ... { ... /** * This is called to find out how big a view should be. The parent * supplies constraint information in the width and height parameters. * * @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) { //判斷是否為強(qiáng)制布局,即帶有“FORCE_LAYOUT”標(biāo)記 以及 widthMeasureSpec或heightMeasureSpec發(fā)生了改變 if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { // first clears the measured dimension flag //清除MEASURED_DIMENSION_SET標(biāo)記 ,該標(biāo)記會在onMeasure()方法后被設(shè)置 mPrivateFlags &= ~MEASURED_DIMENSION_SET; // measure ourselves, this should set the measured dimension flag back // 1、 測量該View本身的大小 ; 2 、 設(shè)置MEASURED_DIMENSION_SET標(biāo)記,否則接寫來會報(bào)異常。 onMeasure(widthMeasureSpec, heightMeasureSpec); // flag not set, setMeasuredDimension() was not invoked, we raise // an exception to warn the developer if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) { throw new IllegalStateException("onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } mPrivateFlags |= LAYOUT_REQUIRED; //下一步是layout了,添加LAYOUT_REQUIRED標(biāo)記 } mOldWidthMeasureSpec = widthMeasureSpec; //保存值 mOldHeightMeasureSpec = heightMeasureSpec; //保存值 } ... }
參數(shù)widthMeasureSpec和heightMeasureSpec 由父View構(gòu)建,表示父View給子View的測量要求。其值地構(gòu)建
會在下面步驟中詳解。
measure()方法顯示判斷是否需要重新調(diào)用設(shè)置改View大小,即調(diào)用onMeasure()方法,然后操作兩個(gè)標(biāo)識符:
①、重置 MEASURED_DIMENSION_SET : onMeasure()方法中,需要添加該標(biāo)識符,否則,會報(bào)異常;
②、添加 LAYOUT_REQUIRED : 表示需要進(jìn)行l(wèi)ayout操作。
最后,保存當(dāng)前的widthMeasureSpec和heightMeasureSpec值。
Step 3 、 調(diào)用onMeasure()方法去真正設(shè)置View的長寬值,其默認(rèn)實(shí)現(xiàn)為:
/** * 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 overriden by subclasses to provide accurate and efficient * measurement of their contents. * * @param widthMeasureSpec horizontal space requirements as imposed by the parent. * The requirements are encoded with * @param heightMeasureSpec vertical space requirements as imposed by the parent. * The requirements are encoded with */ //設(shè)置該View本身地大小 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } /** * Utility to return a default size. Uses the supplied size if the * MeasureSpec imposed no contraints. 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. */ //@param size參數(shù)一般表示設(shè)置了android:minHeight屬性或者該View背景圖片的大小值 public static int getDefaultSize(int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); //根據(jù)不同的mode值,取得寬和高的實(shí)際值。 switch (specMode) { case MeasureSpec.UNSPECIFIED: //表示該View的大小父視圖未定,設(shè)置為默認(rèn)值 result = size; break; case MeasureSpec.AT_MOST: //表示該View的大小由父視圖指定了 case MeasureSpec.EXACTLY: result = specSize; break; } return result; } //獲得設(shè)置了android:minHeight屬性或者該View背景圖片的大小值, 最為該View的參考值 protected int getSuggestedMinimumWidth() { int suggestedMinWidth = mMinWidth; // android:minHeight if (mBGDrawable != null) { // 背景圖片對應(yīng)地Width。 final int bgMinWidth = mBGDrawable.getMinimumWidth(); if (suggestedMinWidth < bgMinWidth) { suggestedMinWidth = bgMinWidth; } } return suggestedMinWidth; } //設(shè)置View在measure過程中寬和高 protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= MEASURED_DIMENSION_SET; //設(shè)置了MEASURED_DIMENSION_SET標(biāo)記 }
主要功能就是根據(jù)該View屬性(android:minWidth和背景圖片大小)和父View對該子View的"測量要求",設(shè)置該 View 的 mMeasuredWidth 和 mMeasuredHeight 值。
這兒只是一般的View類型地實(shí)現(xiàn)方法。一般來說,父View,也就是ViewGroup類型,都需要在重寫onMeasure() 方法,遍歷 所有子View,設(shè)置每個(gè)子View的大小。基本思想如下: 遍歷所有子View,設(shè)置每個(gè)子View的大小。偽
代 碼表示為:
//某個(gè)ViewGroup類型的視圖 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //必須調(diào)用super.ononMeasure()或者直接調(diào)用setMeasuredDimension()方法設(shè)置該View大小,否則會報(bào)異常。 super.onMeasure(widthMeasureSpec , heightMeasureSpec) //setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), // getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); //遍歷每個(gè)子View for(int i = 0 ; i < getChildCount() ; i++){ View child = getChildAt(i); //調(diào)用子View的onMeasure,設(shè)置他們的大小。childWidthMeasureSpec , childHeightMeasureSpec ? child.onMeasure(childWidthMeasureSpec, childHeightMeasureSpec); } }
Step 2、 Step 3 代碼也比較好理解,但問題是我們示例代碼中widthMeasureSpec、 heightMeasureSpec是如何
確定的呢?父View是如何設(shè)定其值的?
要想回答這個(gè)問題,我們看是去源代碼里找找答案吧。在ViewGroup.java類中,為我們提供了三個(gè)方法,去設(shè)置
每 個(gè)子View的大小,基本思想也如同我們 之前描述的思想:遍歷所有子View,設(shè)置每個(gè)子View的大小。
主要有如下方法:
/** * Ask all of the children of this view to measure themselves, taking into * account both the MeasureSpec requirements for this view and its padding. * We skip children that are in the GONE state The heavy lifting is done in * getChildMeasureSpec. */ //widthMeasureSpec 和 heightMeasureSpec 表示該父View的布局要求 //遍歷每個(gè)子View,然后調(diào)用measureChild()方法去實(shí)現(xiàn)每個(gè)子View大小 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) { // 不處于 “GONE” 狀態(tài) measureChild(child, widthMeasureSpec, heightMeasureSpec); } } } /** * Ask one of the children of this view to measure itself, taking into * account both the MeasureSpec requirements for this view and its padding. * The heavy lifting is done in getChildMeasureSpec. * * @param child The child to measure * @param parentWidthMeasureSpec The width requirements for this view * @param parentHeightMeasureSpec The height requirements for this view */ //測量每個(gè)子View高寬時(shí),清楚了該View本身的邊距大小,即android:padding屬性 或android:paddingLeft等屬性標(biāo)記 protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final LayoutParams lp = child.getLayoutParams(); // LayoutParams屬性 //設(shè)置子View的childWidthMeasureSpec屬性,去除了該父View的邊距值 mPaddingLeft + mPaddingRight final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); //設(shè)置子View的childHeightMeasureSpec屬性,去除了該父View的邊距值 mPaddingTop + mPaddingBottom final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
measureChildren()方法:遍歷所有子View,調(diào)用 measureChild()方法去設(shè)置該子View的屬性值。
measureChild() 方法 : 獲取特定子View的 widthMeasureSpec、 heightMeasureSpec,調(diào)用measure()方法
設(shè)置子View的實(shí)際寬高值。
getChildMeasureSpec ()就是獲取 子View的 widthMeasureSpec、 heightMeasureSpec值。
/** * 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. */ // spec參數(shù) 表示該父View本身所占的widthMeasureSpec 或 heightMeasureSpec值 // padding參數(shù) 表示該父View的邊距大小,見于android:padding屬性 或android:paddingLeft等屬性標(biāo)記 // childDimension參數(shù) 表示該子View內(nèi)部LayoutParams屬性的值,可以是wrap_content、match_parent、一個(gè)精確指(an exactly size), // 例如:由android:width指定等。 public static int getChildMeasureSpec(int spec, int padding, int childDimension) { int specMode = MeasureSpec.getMode(spec); //獲得父View的mode int specSize = MeasureSpec.getSize(spec); //獲得父View的實(shí)際值 int size = Math.max(0, specSize - padding); //父View為子View設(shè)定的大小,減去邊距值, int resultSize = 0; //子View對應(yīng)地 size 實(shí)際值 ,由下面的邏輯條件賦值 int resultMode = 0; //子View對應(yīng)地 mode 值 , 由下面的邏輯條件賦值 switch (specMode) { // Parent has imposed an exact size on us //1、父View是EXACTLY的 ! case MeasureSpec.EXACTLY: //1.1、子View的width或height是個(gè)精確值 (an exactly size) if (childDimension >= 0) { resultSize = childDimension; //size為精確值 resultMode = MeasureSpec.EXACTLY; //mode為 EXACTLY 。 } //1.2、子View的width或height為 MATCH_PARENT/FILL_PARENT else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size. So be it. resultSize = size; //size為父視圖大小 resultMode = MeasureSpec.EXACTLY; //mode為 EXACTLY 。 } //1.3、子View的width或height為 WRAP_CONTENT else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; //size為父視圖大小 resultMode = MeasureSpec.AT_MOST; //mode為AT_MOST 。 } break; // Parent has imposed a maximum size on us //2、父View是AT_MOST的 ! case MeasureSpec.AT_MOST: //2.1、子View的width或height是個(gè)精確值 (an exactly size) if (childDimension >= 0) { // Child wants a specific size... so be it resultSize = childDimension; //size為精確值 resultMode = MeasureSpec.EXACTLY; //mode為 EXACTLY 。 } //2.2、子View的width或height為 MATCH_PARENT/FILL_PARENT 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; //size為父視圖大小 resultMode = MeasureSpec.AT_MOST; //mode為AT_MOST } //2.3、子View的width或height為 WRAP_CONTENT else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; //size為父視圖大小 resultMode = MeasureSpec.AT_MOST; //mode為AT_MOST } break; // Parent asked to see how big we want to be //3、父View是UNSPECIFIED的 ! case MeasureSpec.UNSPECIFIED: //3.1、子View的width或height是個(gè)精確值 (an exactly size) if (childDimension >= 0) { // Child wants a specific size... let him have it resultSize = childDimension; //size為精確值 resultMode = MeasureSpec.EXACTLY; //mode為 EXACTLY } //3.2、子View的width或height為 MATCH_PARENT/FILL_PARENT else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size... find out how big it should // be resultSize = 0; //size為0! ,其值未定 resultMode = MeasureSpec.UNSPECIFIED; //mode為 UNSPECIFIED } //3.3、子View的width或height為 WRAP_CONTENT else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = 0; //size為0! ,其值未定 resultMode = MeasureSpec.UNSPECIFIED; //mode為 UNSPECIFIED } break; } //根據(jù)上面邏輯條件獲取的mode和size構(gòu)建MeasureSpec對象。 return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
為了便于分析,我將上面的邏輯判斷語句使用列表項(xiàng)進(jìn)行了說明.
getChildMeasureSpec ()方法的主要功能如下:
根據(jù)父View的measureSpec值(widthMeasureSpec,heightMeasureSpec)值以及子View的子View內(nèi)部
LayoutParams 屬性值,共同決定子View的measureSpec值的大小。主要判斷條件主要為MeasureSpec的mode
類型 以及 LayoutParams 的寬高實(shí)際值(lp.width,lp.height), 見于以上所貼代碼中的列表項(xiàng): 1、 1.1 ; 1.2 ; 1.3 ;
2、2.1等。
例如,分析列表3:假設(shè)當(dāng)父View為MeasureSpec.UNSPECIFIED類型,即未定義時(shí),只有當(dāng)子View的width
或height指定時(shí),其mode才為 MeasureSpec.EXACTLY ,否者該View size為 0 ,mode為 MeasureSpec.UNSPECIFIED 時(shí)
,即處于未指定狀態(tài)。
由此可以得出, 每個(gè)View大小的設(shè)定都事由其父View以及該View共同決定的。但這只是一個(gè)期望的大小,每個(gè)
View在測量時(shí)最終大小的設(shè)定是由 setMeasuredDimension()最終決定的。因此,最終確定一個(gè)View的“測量長寬“是
由以下幾個(gè)方面影響:
1、父View的 MeasureSpec屬性;
2、子View的 LayoutParams屬性 ;
3、 setMeasuredDimension()或者其它類似設(shè)定 mMeasuredWidth 和 mMeasuredHeight 值的方法。
setMeasuredDimension()原型:
//設(shè)置View在measure過程中寬和高 protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= MEASURED_DIMENSION_SET; //設(shè)置了MEASURED_DIMENSION_SET標(biāo)記 }
將上面列表項(xiàng)轉(zhuǎn)換為表格為:
這張表格更能幫助我們分析View的MeasureSpec的確定條件關(guān)系。
為了幫助大家理解,下面我們分析某個(gè)窗口使用地xml布局文件,我們弄清楚該xml布局文件中每個(gè)View的
MeasureSpec值的組成。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/llayout" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/tv" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hello" /> </LinearLayout>
該布局文件共有兩個(gè)View: ①、id為llayout的 LinearLayout 布局控件 ;
②、id為tv的 TextView 控件。
假設(shè)LinearLayout的父View對應(yīng)地widthSpec和heightSpec值皆為MeasureSpec.EXACTLY類型(Activity窗口
的父View為DecorView,具體原因見第三部分說明)。
對 LinearLayout 而言比較簡單,由于 android:layout_width="match_parent",因此其width對應(yīng)地widthSpec
mode 值為MeasureSpec.EXACTLY , size由父視圖大小指定 ; 由于android:layout_height = "match_parent",
因 此其height對應(yīng)地heightSpec mode 值 為 MeasureSpec.EXACTLY ,size由父視圖大小指定 ;
對
TextView
而言 ,其父View為LinearLayout的widthSpec和heightSpec值皆為MeasureSpec.EXACTLY類型,
由于android:layout_width="match_parent" , 因此其width對應(yīng)地widthSpec mode值為MeasureSpec.EXACTLY ,
size由父視圖大小指定 ; 由于android:layout_width="wrap_content" , 因此其height對應(yīng)地widthSpec mode值為
MeasureSpec.AT_MOST,size由父視圖大小指定 。
我們繼續(xù)窺測下LinearLayout類是如何進(jìn)行measure過程的:
public class LinearLayout extends ViewGroup { ... @Override //onMeasure方法。 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //判斷是垂直方向還是水平方向,這兒我們假設(shè)是VERTICAL垂直方向, if (mOrientation == VERTICAL) { measureVertical(widthMeasureSpec, heightMeasureSpec); } else { measureHorizontal(widthMeasureSpec, heightMeasureSpec); } } //垂直方向布局 void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { mTotalLength = 0; //該LinearLayout測量子View時(shí)的總高度。 float totalWeight = 0; //所有子View的權(quán)重和 , android:layout_weight int maxWidth = 0; //保存子View中最大width值 ... final int count = getVirtualChildCount(); //子View的個(gè)數(shù) final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); ... // See how tall everyone is. Also remember max width. for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i); ... LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); totalWeight += lp.weight; //滿足該條件地View會在該LinearLayout有剩余高度時(shí),才真正調(diào)用measure() if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) { ... } else { int oldHeight = Integer.MIN_VALUE; //如果View的hight值為0,并且設(shè)置了android:layout_weight屬性,重新糾正其height值為WRAP_CONTENT if (lp.height == 0 && lp.weight > 0) { oldHeight = 0; lp.height = LayoutParams.WRAP_CONTENT; } // Determine how big this child would like to be. If this or // previous children have given a weight, then we allow it to // use all available space (and we will shrink things later // if needed). //對每個(gè)子View調(diào)用measure()方法 measureChildBeforeLayout( child, i, widthMeasureSpec, 0, heightMeasureSpec, totalWeight == 0 ? mTotalLength : 0); //這三行代碼做了如下兩件事情: //1、獲得該View的measuredHeight值,每個(gè)View都會根據(jù)他們地屬性正確設(shè)置值 > 0 ; //2、更新mTotalLength值:取當(dāng)前高度mTotalLength值與mTotalLength + childHeight 的最大值 // 于是對于android:layout_height="wrap_height"屬性地LinearLayout控件也就知道了它的確切高度值了。 final int childHeight = child.getMeasuredHeight(); final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child)); ... } final int margin = lp.leftMargin + lp.rightMargin; final int measuredWidth = child.getMeasuredWidth() + margin; maxWidth = Math.max(maxWidth, measuredWidth); ... } //后續(xù)還有很多處理,包括繼續(xù)measure()某些符合條件地子View ... } void measureChildBeforeLayout(View child, int childIndex, int widthMeasureSpec, int totalWidth, int heightMeasureSpec, int totalHeight) { //調(diào)用measureChildWithMargins()方法去設(shè)置子View大小 measureChildWithMargins(child, widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight); } ... }
繼續(xù)看看measureChildWithMargins()方法,該方法定義在ViewGroup.java內(nèi),基本流程同于measureChild()方法,但添加了對子View Margin的處理,即:android:margin屬性或者android:marginLeft等屬性的處理。
measureChildWithMargins@ ViewGroup.java
/** * 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. */ //基本流程同于measureChild()方法,但添加了對子View Margin的處理,即:android:margin屬性或者android:marginLeft等屬性的處理 //widthUsed參數(shù) 表示該父View已經(jīng)使用的寬度 //heightUsed參數(shù) 表示該父View已經(jīng)使用的高度 protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); //獲得子View的childWidthMeasureSpec和childHeightMeasureSpec值 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); }
measure()過程時(shí),LinearLayout類做了如下事情 :
1、遍歷每個(gè)子View,對其調(diào)用measure()方法;
2、子View measure()完成后,需要取得該子View地寬高實(shí)際值,繼而做處理(例如:LinearLayout屬性為
android:widht="wrap_content"時(shí), LinearLayout的實(shí)際width值則是每個(gè)子View的 width值的累加值 )。
2.2 WRAP_CONTENT、MATCH_PARENT以及measure動(dòng)機(jī)揭秘
子View地寬高實(shí)際值 ,即 child.getMeasuredWidth()值得返回最終會是一個(gè)確定值? 難道 WRAP_CONTENT (
其值為-2) 、MATCH_PARENT(值為-1)或者說一個(gè)具體值(an exactly size > 0)。前面我們說過, View最終“測量”值的
確定是有三個(gè)部分組成地:
①、父View的MeasureSpec屬性;
②、子View的 LayoutParams屬性 ;
③、setMeasuredDimension()或者其它類似設(shè)定 mMeasuredWidth 和 mMeasuredHeight 值的方法。
因此,一個(gè)View必須以某種合適地方法確定它地最終大小。例如,如下自定義View:
//自定義View public Class MyView extends View { //針對不同地mode值,設(shè)置本View地大小 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ //獲得父View傳遞給我們地測量需求 int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int width = 0 ; int height = 0 ; //對UNSPECIFIED 則拋出異常 if(widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED) throw new RuntimeException("widthMode or heightMode cannot be UNSPECIFIED"); //精確指定 if(widthMode == MeasureSpec.EXACTLY){ width = 100 ; } //模糊指定 else if(widthMode == MeasureSpec.AT_MOST ) width = 50 ; //精確指定 if(heightMode == MeasureSpec.EXACTLY){ height = 100 ; } //模糊指定 else if(heightMode == MeasureSpec.AT_MOST ) height = 50 ; setMeasuredDimension(width , height) ; } }
該 View的 mMeasuredWidth 和 mMeasuredHeight值。
對于TextView而言,如果它地mode不是Exactly類型 , 它會根據(jù)一些屬性,例如: android:textStyle
、 android:textSize android:typeface 等去確定TextView類地需要占用地長和寬。
因此,如果你地自定義View必須手動(dòng)對不同mode做出處理。否則,則是mode對你而言是無效的。
Android框架中提供地一系列View/ViewGroup都需要去進(jìn)行這個(gè)measure()過程地 ,因?yàn)樵趌ayout()過程中,父
View需要調(diào)用getMeasuredWidth()或getMeasuredHeight()去為每個(gè)子View設(shè)置他們地布局坐標(biāo),只有確定布局
坐標(biāo)后,才能真正地將該View 繪制 (draw) 出來,否則該View的layout大小為0,得不到期望效果。我們繼續(xù)看看
LinearLayout的layout布局過程:
public class LinearLayout extends ViewGroup { ... @Override //layout 過程 protected void onLayout(boolean changed, int l, int t, int r, int b) { //假定是垂直方向布局 if (mOrientation == VERTICAL) { layoutVertical(); } else { layoutHorizontal(); } } //對每個(gè)子View調(diào)用layout過程 void layoutVertical() { ... final int count = getVirtualChildCount(); ... for (int i = 0; i < count; i++) { final View child = getVirtualChildAt(i); if (child == null) { //一般為非null childTop += measureNullChild(i); } else if (child.getVisibility() != GONE) { //獲得子View測量時(shí)的實(shí)際寬高值, final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); ... // 封裝了child.layout()方法,見如下 setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight); childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child); i += getChildrenSkipCount(child, i); } } } //width = getMeasuredWidth() ; height = childHeight(); View的大小就是測量大小 private void setChildFrame(View child, int left, int top, int width, int height) { child.layout(left, top, left + width, top + height); } ... }
對一個(gè)View進(jìn)行measure操作地主要目的就是為了確定該View地布局大小,見上面所示代碼。但measure操作
通常是耗時(shí)的,因此 對自定義ViewGroup而言,我們可以自由控制measure、layout過程,如果我們知道如何layout
一個(gè)View,我們可以跳過該ViewGroup地measure操作(onMeasure()方法中measure 所有子View地 ),直接去layout
在前面一篇博客<< Android中滑屏初探 ---- scrollTo 以及 scrollBy方法使用說明 >> 中,我們自定義了 一個(gè) ViewGroup, 并且重寫了onMeasure()和onLayout()方法去分別操作每個(gè)View。就該ViewGroup而言,我們只需要
重寫onLayout() 操作即可,因?yàn)槲覀冎廊绾蝜ayout每個(gè)子View。 如下代碼所示:
//自定義ViewGroup , 包含了三個(gè)LinearLayout控件,存放在不同的布局位置 public class MultiViewGroup extends ViewGroup { private void init() { // 初始化3個(gè) LinearLayout控件 LinearLayout oneLL = new LinearLayout(mContext); oneLL.setBackgroundColor(Color.RED); addView(oneLL); ... } @Override // 我們知曉每個(gè)子View的layout布局大小,因此我們不需要為每個(gè)子View進(jìn)行measure()操作了。 // protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // setMeasuredDimension(width, height); // // 設(shè)置該ViewGroup的大小 // int width = MeasureSpec.getSize(widthMeasureSpec); // int height = MeasureSpec.getSize(heightMeasureSpec); // int childCount = getChildCount(); // for (int i = 0; i < childCount; i++) { // View child = getChildAt(i); // // 設(shè)置每個(gè)子視圖的大小 , 即全屏 // child.measure(MultiScreenActivity.screenWidth, MultiScreenActivity.scrrenHeight); // } } // layout過程 @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // TODO Auto-generated method stub Log.i(TAG, "--- start onLayout --"); int startLeft = 0; // 每個(gè)子視圖的起始布局坐標(biāo) int startTop = 10; // 間距設(shè)置為10px 相當(dāng)于 android:marginTop= "10px" int childCount = getChildCount(); Log.i(TAG, "--- onLayout childCount is -->" + childCount); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); child.layout(startLeft, startTop, startLeft + MultiScreenActivity.screenWidth, startTop + MultiScreenActivity.scrrenHeight); startLeft = startLeft + MultiScreenActivity.screenWidth ; //校準(zhǔn)每個(gè)子View的起始布局位置 //三個(gè)子視圖的在屏幕中的分布如下 [0 , 320] / [320,640] / [640,960] } } }
更多關(guān)于自定義ViewGroup無須重寫measure動(dòng)作的,可以參考 Android API :
中文翻譯見于:<< Android中View繪制優(yōu)化之三---- 優(yōu)化View >>
3、root View被添加至窗口時(shí),UI框架是如何設(shè)置其LayoutParams值
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/hello" /> </LinearLayout>
//顯示一個(gè)懸浮窗吧 , just so so public void showView() { //解析布局文件 LayoutInflater layoutInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE); //rootView對應(yīng)地LayoutParams屬性值為null,將會在UI繪制時(shí)設(shè)定其值 View rootView = layoutInflater.inflate(R.layout.main, null); WindowManager windowManager = (WindowManager)getSystemService(Context.WINDOW_SERVICE); //設(shè)置WindowManager.LayoutParams參數(shù)值,作為該窗口的各種屬性 WindowManager.LayoutParams winparams = WindowManager.LayoutParams(); // 以屏幕左上角為原點(diǎn),設(shè)置x、y初始值 winparams.x = 0; winparams.y = 0; //設(shè)置懸浮窗口長寬數(shù)據(jù) winparams.width = WindowManager.LayoutParams.WRAP_CONTENT;; winparams.height = WindowManager.LayoutParams.WRAP_CONTENT;; windowManager.addView(rootView, winparams); }
@Override public Object getSystemService(String name) { if (WINDOW_SERVICE.equals(name)) { return WindowManagerImpl.getDefault(); } ... }WindowManager是個(gè)接口,具體返回對象則是WindowManagerImpl的單例對象。
public class WindowManagerImpl implements WindowManager{ public static WindowManagerImpl getDefault() { return mWindowManager; } //以特定Window屬性添加一個(gè)窗口 public void addView(View view, ViewGroup.LayoutParams params) { addView(view, params, false); } //參數(shù)nest表示該窗口是不是一個(gè)字窗口 private void addView(View view, ViewGroup.LayoutParams params, boolean nest) { ... final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params; ViewRoot root; View panelParentView = null; //該子窗口對應(yīng)地父窗口View synchronized (this) { ...//需要對傳遞過來地參數(shù)進(jìn)行檢測... //對每個(gè)窗口皆構(gòu)建一個(gè)ViewRoot對象 root = new ViewRoot(view.getContext()); root.mAddNesting = 1; //設(shè)置root View 的LayoutParams為wparams,即WindowManager.LayoutParams類型 view.setLayoutParams(wparams); ...//對參數(shù)檢測,以及拷貝原有數(shù)組... //將窗口對應(yīng)地view、root、wparams保存在屬性集合中 mViews[index] = view; mRoots[index] = root; mParams[index] = wparams; } // do this last because it fires off messages to start doing things // 調(diào)用ViewRoot對象去通知系統(tǒng)添加一個(gè)窗口 root.setView(view, wparams, panelParentView); } ... //這三個(gè)數(shù)組分別保存了一個(gè)窗口對應(yīng)地屬性 private View[] mViews; //root View對象 , View類型 private ViewRoot[] mRoots; //ViewRoot類型 , 與WMS通信 private WindowManager.LayoutParams[] mParams; //窗口屬性 //WindowManagerImpl實(shí)現(xiàn)了單例模式 private static WindowManagerImpl mWindowManager = new WindowManagerImpl(); }
WindowManagerImpl類的三個(gè)數(shù)組集合保存了每個(gè)窗口相關(guān)屬性,這樣我們可以通過這些屬性去操作特定的
public final class ViewRoot extends Handler implements ViewParent,View.AttachInfo.Callbacks { View mView; //所有窗口地root View final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams(); ... /** * We have one child */ public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this) { if (mView == null) { mView = view; mWindowAttributes.copyFrom(attrs); //保存WindowManager.LayoutParams屬性值 attrs = mWindowAttributes; ... mAdded = true; int res; /* = WindowManagerImpl.ADD_OKAY; */ // Schedule the first layout -before- adding to the window // manager, to make sure we do the relayout before receiving // any other events from the system. requestLayout(); //請求UI開始繪制。 mInputChannel = new InputChannel(); //創(chuàng)建一個(gè)InputChannel對象,接受消息 try { //通知WindowManagerService添加一個(gè)窗口 res = sWindowSession.add(mWindow, mWindowAttributes, getHostVisibility(), mAttachInfo.mContentInsets, mInputChannel); } ... view.assignParent(this); //將root View的父View設(shè)置為該ViewRoot對象(實(shí)現(xiàn)了ViewParent接口) ... } } } }說明:ViewRoot類繼承了Handler,實(shí)現(xiàn)了ViewParent接口
1、保存相關(guān)屬性值,例如:mView、mWindowAttributes等;
2、調(diào)用requestLayout()方法請求UI繪制,由于ViewRoot是個(gè)Handler對象,異步請求;
3、通知WindowManagerService添加一個(gè)窗口;
4、注冊一個(gè)事件監(jiān)聽管道,用來監(jiān)聽:按鍵(KeyEvent)和觸摸(MotionEvent)事件。
/** * {@inheritDoc} */ public void requestLayout() { checkThread(); //檢查是不是UI線程調(diào)用,如果不是UI線程,會報(bào)異常 mLayoutRequested = true; //置為真,表示需要進(jìn)行measure和layout過程 scheduleTraversals(); } //開始UI繪制流程 public void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; //防止多次調(diào)用 sendEmptyMessage(DO_TRAVERSAL); //異步請求UI繪制 } } @Override public void handleMessage(Message msg) { switch (msg.what) { case DO_TRAVERSAL: performTraversals(); //開始UI繪制 break; } }
private void performTraversals() { // cache mView since it is used so much below... final View host = mView; mTraversalScheduled = false; boolean surfaceChanged = false; WindowManager.LayoutParams lp = mWindowAttributes; int desiredWindowWidth; //表示該窗口期望width值 int desiredWindowHeight; //表示該窗口期望width值 int childWidthMeasureSpec; //保存root View的widthMeasureSpec int childHeightMeasureSpec; //保存root View的heightMeasureSpec final View.AttachInfo attachInfo = mAttachInfo; final int viewVisibility = getHostVisibility(); boolean viewVisibilityChanged = mViewVisibility != viewVisibility || mNewSurfaceNeeded; float appScale = mAttachInfo.mApplicationScale; WindowManager.LayoutParams params = null; if (mWindowAttributesChanged) { mWindowAttributesChanged = false; surfaceChanged = true; params = lp; } Rect frame = mWinFrame; if (mFirst) { //mFirst表示是否是第一次繪制該Window fullRedrawNeeded = true; mLayoutRequested = true; DisplayMetrics packageMetrics = mView.getContext().getResources().getDisplayMetrics(); //第一次繪制時(shí)desiredWindowWidth,desiredWindowHeight 值大小為屏幕大小 desiredWindowWidth = packageMetrics.widthPixels; desiredWindowHeight = packageMetrics.heightPixels; ... } else { //不是第一次繪制,則desiredWindowWidth值為frame保存大小,frame值會由WMS填充 desiredWindowWidth = frame.width(); desiredWindowHeight = frame.height(); ... } ... boolean insetsChanged = false; if (mLayoutRequested) { ...//獲得root View的widthMeasureSpec 和 heightMeasureSpec值 childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); //開始measure過程 host.measure(childWidthMeasureSpec, childHeightMeasureSpec); } ... final boolean didLayout = mLayoutRequested; boolean triggerGlobalLayoutListener = didLayout || attachInfo.mRecomputeGlobalAttributes; if (didLayout) { ... //layout過程 host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight); ... } ... if (!cancelDraw && !newSurface) { mFullRedrawNeeded = false; draw(fullRedrawNeeded); ... }
/** * @param windowSize The available width or height of the window * * @param rootDimension The layout params for one dimension (width or height) of the window. */ private int getRootMeasureSpec(int windowSize, int rootDimension) { int measureSpec; switch (rootDimension) { case ViewGroup.LayoutParams.MATCH_PARENT: // Window can't resize. Force root view to be windowSize. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); break; case ViewGroup.LayoutParams.WRAP_CONTENT: // Window can resize. Set max size for root view. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: // Window wants to be an exact size. Force root view to be that size. measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec; }
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長非常感激您!手機(jī)微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
