本文原創, 轉載請注明出處 : http://blog.csdn.net/qinjuning
上篇文章<< Android中measure過程、WRAP_CONTENT詳解以及xml布局文件解析流程淺析(上) >>中,我們
了解了View樹的轉換過程以及如何設置View的LayoutParams的。本文繼續沿著既定軌跡繼續未完成的job。
主要知識點如下:
1、MeasureSpc類說明
2、measure過程詳解(揭秘其細節);
3、root View被添加至窗口時,UI框架是如何設置其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)要求。每個MeasureSpc實例代表寬度或者高度
(只能 是其一)要求。 它有三種模式:
①、UNSPECIFIED(未指定),父元素部隊自元素施加任何束縛,子元素可以得到任意想要的大小;
②、EXACTLY(完全),父元素決定自元素的確切大小,子元素將被限定在給定的邊界里而忽略它本身大小;
③、AT_MOST(至多),子元素至多達到指定大小的值。
常用的三個函數:
static int getMode(int measureSpec) : 根據提供的測量值(格式)提取模式(上述三個模式之一)
static int getSize(int measureSpec) : 根據提供的測量值(格式)提取大小值(這個大小也就是我們通常所說的大小)
static int makeMeasureSpec(int size,int mode) : 根據提供的大小值和模式創建一個測量值(格式)
以上摘取自: <<
MeasureSpec介紹及使用詳解
>>
1.2 MeasureSpc類源碼分析 其為View.java類的內部類,路徑:\frameworks\base\core\java\android\view\View.java
public class View implements ... {
...
public static class MeasureSpec {
private static final int MODE_SHIFT = 30; //移位位數為30
//int類型占32位,向右移位30位,該屬性表示掩碼值,用來與size和mode進行"&"運算,獲取對應值。
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
//向右移位30位,其值為00 + (30位0) , 即 0x0000(16進制表示)
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
//向右移位30位,其值為01 + (30位0) , 即0x1000(16進制表示)
public static final int EXACTLY = 1 << MODE_SHIFT;
//向右移位30位,其值為02 + (30位0) , 即0x2000(16進制表示)
public static final int AT_MOST = 2 << MODE_SHIFT;
//創建一個整形值,其高兩位代表mode類型,其余30位代表長或寬的實際值。可以是WRAP_CONTENT、MATCH_PARENT或具體大小exactly size
public static int makeMeasureSpec(int size, int mode) {
return size + mode;
}
//獲取模式 ,與運算
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
//獲取長或寬的實際值 ,與運算
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
...
}
MeasureSpec類的處理思路是:
①、右移運算,使int 類型的高兩位表示模式的實際值,其余30位表示其余30位代表長或寬的實際值----可以是
WRAP_CONTENT、MATCH_PARENT或具體大小exactly size。
②、通過掩碼MODE_MASK進行與運算 “&”,取得模式(mode)以及長或寬(value)的實際值。
2、measure過程詳解
2.1 measure過程深入分析
之前的一篇博文 << Android中View繪制流程以及invalidate()等相關方法分析 >> ,我們從”二B程序員”的角度簡單 解了measure過程的調用過程。過了這么多,我們也該 升級了,- - 。現在請開始從”普通程序員”角度去理解這個
過 程。我們重點查看measure過程中地相關方法。
我們說過,當UI框架開始繪制時,皆是從ViewRoot.java類開始繪制的。
ViewRoot類簡要說明
: 任何顯示在設備中的窗口,例如:Activity、Dialog等,都包含一個ViewRoot實例,該
類 主要用來與遠端 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(){
...
//這兩個值我們在后面討論時,在回過頭來看看是怎么賦值的。現在只需要記住其值MeasureSpec.makeMeasureSpec()構建的。
int childWidthMeasureSpec; //其值由MeasureSpec類構建 , makeMeasureSpec
int childHeightMeasureSpec;//其值由MeasureSpec類構建 , makeMeasureSpec
// Ask host how big it wants to be
host.measure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}
...
}
Step 2 、
調用measure()方法去做一些前期準備
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) {
//判斷是否為強制布局,即帶有“FORCE_LAYOUT”標記 以及 widthMeasureSpec或heightMeasureSpec發生了改變
if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
// first clears the measured dimension flag
//清除MEASURED_DIMENSION_SET標記 ,該標記會在onMeasure()方法后被設置
mPrivateFlags &= ~MEASURED_DIMENSION_SET;
// measure ourselves, this should set the measured dimension flag back
// 1、 測量該View本身的大小 ; 2 、 設置MEASURED_DIMENSION_SET標記,否則接寫來會報異常。
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標記
}
mOldWidthMeasureSpec = widthMeasureSpec; //保存值
mOldHeightMeasureSpec = heightMeasureSpec; //保存值
}
...
}
參數widthMeasureSpec和heightMeasureSpec 由父View構建,表示父View給子View的測量要求。其值地構建
會在下面步驟中詳解。
measure()方法顯示判斷是否需要重新調用設置改View大小,即調用onMeasure()方法,然后操作兩個標識符:
①、重置 MEASURED_DIMENSION_SET : onMeasure()方法中,需要添加該標識符,否則,會報異常;
②、添加 LAYOUT_REQUIRED : 表示需要進行layout操作。
最后,保存當前的widthMeasureSpec和heightMeasureSpec值。
Step 3 、 調用onMeasure()方法去真正設置View的長寬值,其默認實現為:
/**
* 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
*/
//設置該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參數一般表示設置了android:minHeight屬性或者該View背景圖片的大小值
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
//根據不同的mode值,取得寬和高的實際值。
switch (specMode) {
case MeasureSpec.UNSPECIFIED: //表示該View的大小父視圖未定,設置為默認值
result = size;
break;
case MeasureSpec.AT_MOST: //表示該View的大小由父視圖指定了
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
//獲得設置了android:minHeight屬性或者該View背景圖片的大小值, 最為該View的參考值
protected int getSuggestedMinimumWidth() {
int suggestedMinWidth = mMinWidth; // android:minHeight
if (mBGDrawable != null) { // 背景圖片對應地Width。
final int bgMinWidth = mBGDrawable.getMinimumWidth();
if (suggestedMinWidth < bgMinWidth) {
suggestedMinWidth = bgMinWidth;
}
}
return suggestedMinWidth;
}
//設置View在measure過程中寬和高
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= MEASURED_DIMENSION_SET; //設置了MEASURED_DIMENSION_SET標記
}
主要功能就是根據該View屬性(android:minWidth和背景圖片大小)和父View對該子View的"測量要求",設置該 View 的 mMeasuredWidth 和 mMeasuredHeight 值。
這兒只是一般的View類型地實現方法。一般來說,父View,也就是ViewGroup類型,都需要在重寫onMeasure() 方法,遍歷 所有子View,設置每個子View的大小。基本思想如下: 遍歷所有子View,設置每個子View的大小。偽
代 碼表示為:
//某個ViewGroup類型的視圖
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//必須調用super.ononMeasure()或者直接調用setMeasuredDimension()方法設置該View大小,否則會報異常。
super.onMeasure(widthMeasureSpec , heightMeasureSpec)
//setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
// getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
//遍歷每個子View
for(int i = 0 ; i < getChildCount() ; i++){
View child = getChildAt(i);
//調用子View的onMeasure,設置他們的大小。childWidthMeasureSpec , childHeightMeasureSpec ?
child.onMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
Step 2、 Step 3 代碼也比較好理解,但問題是我們示例代碼中widthMeasureSpec、 heightMeasureSpec是如何
確定的呢?父View是如何設定其值的?
要想回答這個問題,我們看是去源代碼里找找答案吧。在ViewGroup.java類中,為我們提供了三個方法,去設置
每 個子View的大小,基本思想也如同我們 之前描述的思想:遍歷所有子View,設置每個子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的布局要求
//遍歷每個子View,然后調用measureChild()方法去實現每個子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” 狀態
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
*/
//測量每個子View高寬時,清楚了該View本身的邊距大小,即android:padding屬性 或android:paddingLeft等屬性標記
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams(); // LayoutParams屬性
//設置子View的childWidthMeasureSpec屬性,去除了該父View的邊距值 mPaddingLeft + mPaddingRight
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
//設置子View的childHeightMeasureSpec屬性,去除了該父View的邊距值 mPaddingTop + mPaddingBottom
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
measureChildren()方法:遍歷所有子View,調用 measureChild()方法去設置該子View的屬性值。
measureChild() 方法 : 獲取特定子View的 widthMeasureSpec、 heightMeasureSpec,調用measure()方法
設置子View的實際寬高值。
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參數 表示該父View本身所占的widthMeasureSpec 或 heightMeasureSpec值
// padding參數 表示該父View的邊距大小,見于android:padding屬性 或android:paddingLeft等屬性標記
// childDimension參數 表示該子View內部LayoutParams屬性的值,可以是wrap_content、match_parent、一個精確指(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的實際值
int size = Math.max(0, specSize - padding); //父View為子View設定的大小,減去邊距值,
int resultSize = 0; //子View對應地 size 實際值 ,由下面的邏輯條件賦值
int resultMode = 0; //子View對應地 mode 值 , 由下面的邏輯條件賦值
switch (specMode) {
// Parent has imposed an exact size on us
//1、父View是EXACTLY的 !
case MeasureSpec.EXACTLY:
//1.1、子View的width或height是個精確值 (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是個精確值 (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是個精確值 (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;
}
//根據上面邏輯條件獲取的mode和size構建MeasureSpec對象。
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
為了便于分析,我將上面的邏輯判斷語句使用列表項進行了說明.
getChildMeasureSpec ()方法的主要功能如下:
根據父View的measureSpec值(widthMeasureSpec,heightMeasureSpec)值以及子View的子View內部
LayoutParams 屬性值,共同決定子View的measureSpec值的大小。主要判斷條件主要為MeasureSpec的mode
類型 以及 LayoutParams 的寬高實際值(lp.width,lp.height), 見于以上所貼代碼中的列表項: 1、 1.1 ; 1.2 ; 1.3 ;
2、2.1等。
例如,分析列表3:假設當父View為MeasureSpec.UNSPECIFIED類型,即未定義時,只有當子View的width
或height指定時,其mode才為 MeasureSpec.EXACTLY ,否者該View size為 0 ,mode為 MeasureSpec.UNSPECIFIED 時
,即處于未指定狀態。
由此可以得出, 每個View大小的設定都事由其父View以及該View共同決定的。但這只是一個期望的大小,每個
View在測量時最終大小的設定是由 setMeasuredDimension()最終決定的。因此,最終確定一個View的“測量長寬“是
由以下幾個方面影響:
1、父View的 MeasureSpec屬性;
2、子View的 LayoutParams屬性 ;
3、 setMeasuredDimension()或者其它類似設定 mMeasuredWidth 和 mMeasuredHeight 值的方法。
setMeasuredDimension()原型:
//設置View在measure過程中寬和高
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= MEASURED_DIMENSION_SET; //設置了MEASURED_DIMENSION_SET標記
}
將上面列表項轉換為表格為:
這張表格更能幫助我們分析View的MeasureSpec的確定條件關系。
為了幫助大家理解,下面我們分析某個窗口使用地xml布局文件,我們弄清楚該xml布局文件中每個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>
該布局文件共有兩個View: ①、id為llayout的 LinearLayout 布局控件 ;
②、id為tv的 TextView 控件。
假設LinearLayout的父View對應地widthSpec和heightSpec值皆為MeasureSpec.EXACTLY類型(Activity窗口
的父View為DecorView,具體原因見第三部分說明)。
對 LinearLayout 而言比較簡單,由于 android:layout_width="match_parent",因此其width對應地widthSpec
mode 值為MeasureSpec.EXACTLY , size由父視圖大小指定 ; 由于android:layout_height = "match_parent",
因 此其height對應地heightSpec mode 值 為 MeasureSpec.EXACTLY ,size由父視圖大小指定 ;
對
TextView
而言 ,其父View為LinearLayout的widthSpec和heightSpec值皆為MeasureSpec.EXACTLY類型,
由于android:layout_width="match_parent" , 因此其width對應地widthSpec mode值為MeasureSpec.EXACTLY ,
size由父視圖大小指定 ; 由于android:layout_width="wrap_content" , 因此其height對應地widthSpec mode值為
MeasureSpec.AT_MOST,size由父視圖大小指定 。
我們繼續窺測下LinearLayout類是如何進行measure過程的:
public class LinearLayout extends ViewGroup {
...
@Override //onMeasure方法。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//判斷是垂直方向還是水平方向,這兒我們假設是VERTICAL垂直方向,
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
//垂直方向布局
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
mTotalLength = 0; //該LinearLayout測量子View時的總高度。
float totalWeight = 0; //所有子View的權重和 , android:layout_weight
int maxWidth = 0; //保存子View中最大width值
...
final int count = getVirtualChildCount(); //子View的個數
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有剩余高度時,才真正調用measure()
if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {
...
} else {
int oldHeight = Integer.MIN_VALUE;
//如果View的hight值為0,并且設置了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).
//對每個子View調用measure()方法
measureChildBeforeLayout(
child, i, widthMeasureSpec, 0, heightMeasureSpec,
totalWeight == 0 ? mTotalLength : 0);
//這三行代碼做了如下兩件事情:
//1、獲得該View的measuredHeight值,每個View都會根據他們地屬性正確設置值 > 0 ;
//2、更新mTotalLength值:取當前高度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);
...
}
//后續還有很多處理,包括繼續measure()某些符合條件地子View
...
}
void measureChildBeforeLayout(View child, int childIndex,
int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
int totalHeight) {
//調用measureChildWithMargins()方法去設置子View大小
measureChildWithMargins(child, widthMeasureSpec, totalWidth,
heightMeasureSpec, totalHeight);
}
...
}
繼續看看measureChildWithMargins()方法,該方法定義在ViewGroup.java內,基本流程同于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參數 表示該父View已經使用的寬度
//heightUsed參數 表示該父View已經使用的高度
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()過程時,LinearLayout類做了如下事情 :
1、遍歷每個子View,對其調用measure()方法;
2、子View measure()完成后,需要取得該子View地寬高實際值,繼而做處理(例如:LinearLayout屬性為
android:widht="wrap_content"時, LinearLayout的實際width值則是每個子View的 width值的累加值 )。
2.2 WRAP_CONTENT、MATCH_PARENT以及measure動機揭秘
子View地寬高實際值 ,即 child.getMeasuredWidth()值得返回最終會是一個確定值? 難道 WRAP_CONTENT (
其值為-2) 、MATCH_PARENT(值為-1)或者說一個具體值(an exactly size > 0)。前面我們說過, View最終“測量”值的
確定是有三個部分組成地:
①、父View的MeasureSpec屬性;
②、子View的 LayoutParams屬性 ;
③、setMeasuredDimension()或者其它類似設定 mMeasuredWidth 和 mMeasuredHeight 值的方法。
因此,一個View必須以某種合適地方法確定它地最終大小。例如,如下自定義View:
//自定義View
public Class MyView extends View {
//針對不同地mode值,設置本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類型 , 它會根據一些屬性,例如: android:textStyle
、 android:textSize android:typeface 等去確定TextView類地需要占用地長和寬。
因此,如果你地自定義View必須手動對不同mode做出處理。否則,則是mode對你而言是無效的。
Android框架中提供地一系列View/ViewGroup都需要去進行這個measure()過程地 ,因為在layout()過程中,父
View需要調用getMeasuredWidth()或getMeasuredHeight()去為每個子View設置他們地布局坐標,只有確定布局
坐標后,才能真正地將該View 繪制 (draw) 出來,否則該View的layout大小為0,得不到期望效果。我們繼續看看
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();
}
}
//對每個子View調用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測量時的實際寬高值,
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);
}
...
}
對一個View進行measure操作地主要目的就是為了確定該View地布局大小,見上面所示代碼。但measure操作
通常是耗時的,因此 對自定義ViewGroup而言,我們可以自由控制measure、layout過程,如果我們知道如何layout
一個View,我們可以跳過該ViewGroup地measure操作(onMeasure()方法中measure 所有子View地 ),直接去layout
在前面一篇博客<< Android中滑屏初探 ---- scrollTo 以及 scrollBy方法使用說明 >> 中,我們自定義了 一個 ViewGroup, 并且重寫了onMeasure()和onLayout()方法去分別操作每個View。就該ViewGroup而言,我們只需要
重寫onLayout() 操作即可,因為我們知道如何layout每個子View。 如下代碼所示:
//自定義ViewGroup , 包含了三個LinearLayout控件,存放在不同的布局位置
public class MultiViewGroup extends ViewGroup {
private void init() {
// 初始化3個 LinearLayout控件
LinearLayout oneLL = new LinearLayout(mContext);
oneLL.setBackgroundColor(Color.RED);
addView(oneLL);
...
}
@Override
// 我們知曉每個子View的layout布局大小,因此我們不需要為每個子View進行measure()操作了。
// protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// setMeasuredDimension(width, height);
// // 設置該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);
// // 設置每個子視圖的大小 , 即全屏
// 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; // 每個子視圖的起始布局坐標
int startTop = 10; // 間距設置為10px 相當于 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 ; //校準每個子View的起始布局位置
//三個子視圖的在屏幕中的分布如下 [0 , 320] / [320,640] / [640,960]
}
}
}
更多關于自定義ViewGroup無須重寫measure動作的,可以參考 Android API :
中文翻譯見于:<< Android中View繪制優化之三---- 優化View >>
3、root View被添加至窗口時,UI框架是如何設置其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>
//顯示一個懸浮窗吧 , just so so
public void showView()
{
//解析布局文件
LayoutInflater layoutInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
//rootView對應地LayoutParams屬性值為null,將會在UI繪制時設定其值
View rootView = layoutInflater.inflate(R.layout.main, null);
WindowManager windowManager = (WindowManager)getSystemService(Context.WINDOW_SERVICE);
//設置WindowManager.LayoutParams參數值,作為該窗口的各種屬性
WindowManager.LayoutParams winparams = WindowManager.LayoutParams();
// 以屏幕左上角為原點,設置x、y初始值
winparams.x = 0;
winparams.y = 0;
//設置懸浮窗口長寬數據
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是個接口,具體返回對象則是WindowManagerImpl的單例對象。
public class WindowManagerImpl implements WindowManager{
public static WindowManagerImpl getDefault()
{
return mWindowManager;
}
//以特定Window屬性添加一個窗口
public void addView(View view, ViewGroup.LayoutParams params)
{
addView(view, params, false);
}
//參數nest表示該窗口是不是一個字窗口
private void addView(View view, ViewGroup.LayoutParams params, boolean nest)
{ ...
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
ViewRoot root;
View panelParentView = null; //該子窗口對應地父窗口View
synchronized (this) {
...//需要對傳遞過來地參數進行檢測...
//對每個窗口皆構建一個ViewRoot對象
root = new ViewRoot(view.getContext());
root.mAddNesting = 1;
//設置root View 的LayoutParams為wparams,即WindowManager.LayoutParams類型
view.setLayoutParams(wparams);
...//對參數檢測,以及拷貝原有數組...
//將窗口對應地view、root、wparams保存在屬性集合中
mViews[index] = view;
mRoots[index] = root;
mParams[index] = wparams;
}
// do this last because it fires off messages to start doing things
// 調用ViewRoot對象去通知系統添加一個窗口
root.setView(view, wparams, panelParentView);
}
...
//這三個數組分別保存了一個窗口對應地屬性
private View[] mViews; //root View對象 , View類型
private ViewRoot[] mRoots; //ViewRoot類型 , 與WMS通信
private WindowManager.LayoutParams[] mParams; //窗口屬性
//WindowManagerImpl實現了單例模式
private static WindowManagerImpl mWindowManager = new WindowManagerImpl();
}
WindowManagerImpl類的三個數組集合保存了每個窗口相關屬性,這樣我們可以通過這些屬性去操作特定的
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(); //創建一個InputChannel對象,接受消息
try {
//通知WindowManagerService添加一個窗口
res = sWindowSession.add(mWindow, mWindowAttributes,
getHostVisibility(), mAttachInfo.mContentInsets,
mInputChannel);
}
...
view.assignParent(this); //將root View的父View設置為該ViewRoot對象(實現了ViewParent接口)
...
}
}
}
}
說明:ViewRoot類繼承了Handler,實現了ViewParent接口
1、保存相關屬性值,例如:mView、mWindowAttributes等;
2、調用requestLayout()方法請求UI繪制,由于ViewRoot是個Handler對象,異步請求;
3、通知WindowManagerService添加一個窗口;
4、注冊一個事件監聽管道,用來監聽:按鍵(KeyEvent)和觸摸(MotionEvent)事件。
/**
* {@inheritDoc}
*/
public void requestLayout() {
checkThread(); //檢查是不是UI線程調用,如果不是UI線程,會報異常
mLayoutRequested = true; //置為真,表示需要進行measure和layout過程
scheduleTraversals();
}
//開始UI繪制流程
public void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true; //防止多次調用
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();
//第一次繪制時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;
}
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061
微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元

