如果非要對自定義控件的流程進行一個簡單的劃分,我會嘗試將其分為三大部分:控件的繪制、控件的測量和控件的交互行為。前面我們用了六節的篇幅和一個翻頁的例子來對控件的繪制有了一個全新的認識但是我們所做出的所有例子都是不完美的,為什么這么說呢,還是先來看個sample:
~~~
/**
*
* @author AigeStudio {@link http://blog.csdn.net/aigestudio}
* @since 2015/1/12
*
*/
public class ImgView extends View {
private Bitmap mBitmap;// 位圖對象
public ImgView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDraw(Canvas canvas) {
// 繪制位圖
canvas.drawBitmap(mBitmap, 0, 0, null);
}
/**
* 設置位圖
*
* @param bitmap
* 位圖對象
*/
public void setBitmap(Bitmap bitmap) {
this.mBitmap = bitmap;
}
}
~~~
這個例子呢非常簡單,我們用它來模擬類似ImageView的效果顯示一張圖片,在MainActivity中我們獲取該控件并為其設置Bitmap:
~~~
/**
* 主界面
*
* @author Aige {@link http://blog.csdn.net/aigestudio}
* @since 2014/11/17
*/
public class MainActivity extends Activity {
private ImgView mImgView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mImgView = (ImgView) findViewById(R.id.main_pv);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lovestory);
mImgView.setBitmap(bitmap);
}
}
~~~
此時運行效果如下:

很簡單對吧,可是上面的代碼其實是有個問題的,至于什么問題?我們待會再說,就看你通過前面我們的學習能不能發現了。這一節我們重點是控件的測量,大家不知道注意沒有,這個系列文章的命名我用了“控件”而非“View”其實目的就是說明我們的控件不僅包括View也應該包含ViewGroup,當然你也可以以官方的方式將其分為控件和布局,不過我更喜歡View和ViewGroup,好了廢話不說,我們先來看看View的測量方式,上面的代碼中MainActivity對應的布局文件如下:
~~~
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFFFF"
android:orientation="vertical" >
<com.aigestudio.customviewdemo.views.ImgView
android:id="@+id/main_pv"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
~~~
既然我們的自定義View也算一個控件那么我們也可以像平時做布局那樣往我們的LinearLayout中添加各種各樣的其他控件對吧:
~~~
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFFFF"
android:orientation="vertical" >
<com.aigestudio.customviewdemo.views.ImgView
android:id="@+id/main_pv"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
</LinearLayout>
~~~
但是運行后你卻發現我們的Button和TextView卻沒有顯示在屏幕上,這時你可能會說那當然咯,因為我們的ImgViewlayout_width和layout_height均為match_parent,可是即便你將其改成wrap_content:
~~~
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFFFF"
android:orientation="vertical" >
<com.aigestudio.customviewdemo.views.ImgView
android:id="@+id/main_pv"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<!-- ……省略一些代碼…… -->
</LinearLayout>
~~~
結果也一樣,這時你肯定很困惑,不解的主要原因是沒有搞懂View的測量機制,在前面的幾節中我們或多或少有提到控件的測量,也曾經說過Android提供給我們能夠操縱控件測量的方法是onMeasure:
~~~
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
~~~
默認情況下onMeasure方法中只是簡單地將簽名列表中的兩個int型參數回傳給父類的onMeasure方法,然后由父類的方法去計算出最終的測量值。但是,這里有個問題非常重要,就是onMeasure簽名列表中的這兩個參數是從何而來,這里可以告訴大家的是,這兩個參數是由view的父容器,代碼中也就是我們的LinearLayout傳遞進來的,很多初學Android的朋友會將位于xml布局文件頂端的控件稱之為根布局,比如這里我們的LinearLayout,而事實上在Android的GUI框架中,這個LinearLayout還稱不上根布局,我們知道一個Activity可以對應一個View(也可以是ViewGroup),很多情況下我們會通過Activity的setContentView方法去設置我們的View:
~~~
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
~~~
setContentView在Activity內的實現也非常簡單,就是調用getWindow方法獲取一個Window類型的對象并調用其setContentView方法:
~~~
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
initActionBar();
}
~~~
而這個Window對象
~~~
public Window getWindow() {
return mWindow;
}
~~~
其本質也就是一個PhoneWindow,在Activity的attach方法中通過makeNewWindow生成:
~~~
final void attach(Context context, ActivityThread aThread,
// 此處省去一些代碼……
mWindow = PolicyManager.makeNewWindow(this);
mWindow.setCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}
if (info.uiOptions != 0) {
mWindow.setUiOptions(info.uiOptions);
}
// 此處省去巨量代碼……
}
~~~
在PolicyManager中通過反射的方式獲取com.android.internal.policy.impl.Policy的一個實例:
~~~
public final class PolicyManager {
private static final String POLICY_IMPL_CLASS_NAME =
"com.android.internal.policy.impl.Policy";
private static final IPolicy sPolicy;
static {
try {
Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME);
sPolicy = (IPolicy)policyClass.newInstance();
} catch (ClassNotFoundException ex) {
throw new RuntimeException(
POLICY_IMPL_CLASS_NAME + " could not be loaded", ex);
} catch (InstantiationException ex) {
throw new RuntimeException(
POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
} catch (IllegalAccessException ex) {
throw new RuntimeException(
POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
}
}
// 省去構造方法……
public static Window makeNewWindow(Context context) {
return sPolicy.makeNewWindow(context);
}
// 省去無關代碼……
}
~~~
并通過其內部的makeNewWindow實現返回一個PhoneWindow對象:
~~~
public Window makeNewWindow(Context context) {
return new PhoneWindow(context);
}
~~~
PhoneWindow是Window的一個子類,其對Window中定義的大量抽象方法作了具體的實現,比如我們的setContentView方法在Window中僅做了一個抽象方法定義:
~~~
public abstract class Window {
// 省去不可估量的代碼……
public abstract void setContentView(int layoutResID);
public abstract void setContentView(View view);
public abstract void setContentView(View view, ViewGroup.LayoutParams params);
// 省去數以億計的代碼……
}
~~~
其在PhoneWindow中都有具體的實現:
~~~
public class PhoneWindow extends Window implements MenuBuilder.Callback {
// 省去草泥馬個代碼……
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else {
mContentParent.removeAllViews();
}
mLayoutInflater.inflate(layoutResID, mContentParent);
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
@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();
}
}
// 省去法克魷個代碼……
}
~~~
當然如果你要是使用了TV的SDK那么這里就不是PhoneWindow而是TVWindow了,至于是不是呢?留給大家去驗證。到這里我們都還沒完,在PhoneWindow的setContentView方法中先會去判斷mContentParent這個引用是否為空,如果為空則表示我們是第一次生成那么調用installDecor方法去生成一些具體的對象否則清空該mContentParent下的所有子元素(注意mContentParent是一個ViewGroup)并通過LayoutInflater將xml布局轉換為View Tree添加至mContentParent中(這里根據setContentView(int layoutResID)方法分析,其他重載方法類似),installDecor方法做的事相對多但不復雜,首先是對DecorView類型的mDecor成員變量賦值繼而將其注入generateLayout方法生成我們的mContentParent:
~~~
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();
// 省省省……
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
// 省省省……
}
// 省省省……
}
~~~
generateLayout方法中做的事就多了,簡直可以跟performTraversals拼,這里不貼代碼了簡單分析一下,generateLayout方法中主要根據當前我們的Style類型為當前Window選擇不同的布局文件,看到這里,想必大家也該意識到,這才是我們的“根布局”,其會指定一個用來存放我們自定義布局文件(也就是我們口頭上常說的根布局比如我們例子中的LinearLayout)的ViewGroup,一般情況下這個ViewGroup的重任由FrameLayout來承擔,這也是為什么我們在獲取我們xml布局文件中的頂層布局時調用其getParent()方法會返回FrameLayout對象的原因,其id為android:id="@android:id/content":
~~~
protected ViewGroup generateLayout(DecorView decor) {
// 省去巨量代碼……
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
// 省去一些代碼……
}
~~~
在這個Window布局文件被確定后,mDecor則會將該布局所生成的對應View添加進來并獲取id為content的View將其賦給mContentParent,至此mContentParent和mDecor均已生成,而我們xml布局文件中的布局則會被添加至mContentParent。對應關系類似下圖:

說了大半天才理清這個小關系,但是我們還沒說到重點…………………………就是widthMeasureSpec和heightMeasureSpec究竟是從哪來的……………………如果我們不做上面的一個分析,很多童鞋壓根無從下手,有了上面一個分析,我們知道我們界面的真正根視圖應該是DecorView,那么我們的widthMeasureSpec和heightMeasureSpec應該從這里或者更上一層PhoneWindow傳遞進來對吧,但是DecorView是FrameLayout的一個實例,在FrameLayout的onMeasure中我們確實有對子元素的測量,但是問題是FrameLayout:onMeasure方法中的widthMeasureSpec和heightMeasureSpec又是從何而來呢?追溯上去我們又回到了View…………………………………………………………不了解Android GUI框架的童鞋邁出的第一步就被無情地煽了回去。其實在Android中我們可以在很多方面看到類似MVC架構的影子,比如最最常見的就是我們的xml界面布局——Activity等組件——model數據之間的關系,而在整個GUI的框架中,我們也可以對其做出類似的規劃,View在設計過程中就注定了其只會對顯示數據進行處理比如我們的測量布局和繪制還有動畫等等,而承擔Controller控制器重任的是誰呢?在Android中這一功能由ViewRootImpl承擔,我們在前面提到過這個類,其負責的東西很多,比如我們窗口的顯示、用戶的輸入輸出當然還有關于處理我們繪制流程的方法:
~~~
private void performTraversals() {
// ………………啦啦啦啦………………
}
~~~
performTraversals方法是處理繪制流程的一個開始,內部邏輯相當相當多&復雜,雖然沒有View類復雜……但是讓我選的話我寧愿看整個View類也不愿看performTraversals方法那邪惡的邏輯…………囧,在該方法中我們可以看到如下的一段邏輯(具體各類變量的賦值就不貼了實在太多):
~~~
private void performTraversals() {
// ………省略宇宙塵埃數量那么多的代碼………
if (!mStopped) {
// ……省略一些代碼
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// ……省省省
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
// ………省略人體細胞數量那么多的代碼………
}
~~~
可以看到在performTraversals方法中通過getRootMeasureSpec獲取原始的測量規格并將其作為參數傳遞給performMeasure方法處理,這里我們重點來看getRootMeasureSpec方法是如何確定測量規格的,首先我們要知道mWidth, lp.width和mHeight, lp.height這兩組參數的意義,其中lp.width和lp.height均為MATCH_PARENT,其在mWindowAttributes(WindowManager.LayoutParams類型)將值賦予給lp時就已被確定,mWidth和mHeight表示當前窗口的大小,其值由performTraversals中一系列邏輯計算確定,這里跳過,而在getRootMeasureSpec中作了如下判斷:
~~~
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window不能調整其大小,強制使根視圖大小與Window一致
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window可以調整其大小,為根視圖設置一個最大值
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window想要一個確定的尺寸,強制將根視圖的尺寸作為其尺寸
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
~~~
也就是說不管如何,我們的根視圖大小必定都是全屏的……
至此,我們算是真正接觸到根視圖的測量規格,爾后這個規格會被由上至下傳遞下去,并由當前view與其父容器共同作用決定最終的測量大小,在View與ViewGroup遞歸調用實現測量的過程中有幾個重要的方法,對于View而言則是measure方法:
~~~
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 省略部分代碼……
/*
* 判斷當前mPrivateFlags是否帶有PFLAG_FORCE_LAYOUT強制布局標記
* 判斷當前widthMeasureSpec和heightMeasureSpec是否發生了改變
*/
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
// 如果發生了改變表示需要重新進行測量此時清除掉mPrivateFlags中已測量的標識位PFLAG_MEASURED_DIMENSION_SET
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
resolveRtlPropertiesIfNeeded();
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// 測量View的尺寸
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex);
setMeasuredDimension((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
/*
* 如果mPrivateFlags里沒有表示已測量的標識位PFLAG_MEASURED_DIMENSION_SET則會拋出異常
*/
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
}
// 如果已測量View那么就可以往mPrivateFlags添加標識位PFLAG_LAYOUT_REQUIRED表示可以進行布局了
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
}
// 最后存儲測量完成的測量規格
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec;
mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
~~~
可以看到,View對控件的測量是在onMeasure方法中進行的,也就是文章開頭我們在自定義View中重寫的onMeasure方法,但是我們并沒有對其做任何的處理,也就是說保持了其在父類View中的默認實現,其默認實現也很簡單:
~~~
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
~~~
其直接調用了setMeasuredDimension方法為其設置了兩個計算后的測量值:
~~~
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
// 省去部分代碼……
// 設置測量后的寬高
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
// 重新將已測量標識位存入mPrivateFlags標識測量的完成
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
~~~
回到onMeasure方法,我們來看看這兩個測量值具體是怎么獲得的,其實非常簡單,首先來看getSuggestedMinimumWidth方法:
~~~
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
~~~
如果背景為空那么我們直接返回mMinWidth最小寬度否則就在mMinWidth和背景最小寬度之間取一個最大值,getSuggestedMinimumHeight類同,mMinWidth和mMinHeight我沒記錯的話應該都是100px,而getDefaultSize方法呢也很簡單:
~~~
public static int getDefaultSize(int size, int measureSpec) {
// 將我們獲得的最小值賦給result
int result = size;
// 從measureSpec中解算出測量規格的模式和尺寸
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
/*
* 根據測量規格模式確定最終的測量尺寸
*/
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
~~~
注意上述代碼中當模式為AT_MOST和EXACTLY時均會返回解算出的測量尺寸,還記得上面我們說的PhoneWindow、DecorView么從它們那里獲取到的測量規格層層傳遞到我們的自定義View中,這就是為什么我們的View在默認情況下不管是math_parent還是warp_content都能占滿父容器的剩余空間(這里面還有父布局LinearLayout的作用就先略過了了解即可)。上述onMeasure的過程則是View默認的處理過程,如果我們不喜歡Android幫我們處理那么我們可以自己重寫onMeasure實現自己的測量邏輯:
~~~
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 設置測量尺寸
setMeasuredDimension(250, 250);
}
~~~
最簡單的粗暴的就是直接將兩個值作為參數傳入setMeasuredDimension方法,效果如下:

當然這樣不好,用Android官方的話來說就是太過“專政”,因為它完全摒棄了父容器的意愿,完全由自己決定了大小,如果大家逛blog看技術文章或者聽別人討論常常會聽到別人這么說view的最終測量尺寸是由view本身何其父容器共同決定的,至于如何共同決定我們呆會再說,這里我們先看看如何能在一定程度上順應爹的“意愿”呢?從View默認的測量模式中我們可以看到它頻繁使用了一個叫做MeasureSpec的類,而在ViewRootImpl中呢也有大量用到該類,該類的具體說明大家可以圍觀我早期的一篇文章:http://blog.csdn.net/aigestudio/article/details/38636531,里面有對MeasureSpec類的詳細說明,這里我就簡單概述下MeasureSpec類中的三個Mode常量值的意義,其中UNSPECIFIED表示未指定,爹不會對兒子作任何的束縛,兒子想要多大都可以;EXACTLY表示完全的,意為兒子多大爹心里有數,爹早已算好了;AT_MOST表示至多,爹已經為兒子設置好了一個最大限制,兒子你不能比這個值大,不能再多了!父容器所謂的“意圖”其實就由上述三個常量值表現,既然如此我們就該對這三個Mode常量做一個判斷才行,不然怎么知道爹的意圖呢:
~~~
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 聲明一個臨時變量來存儲計算出的測量值
int resultWidth = 0;
// 獲取寬度測量規格中的mode
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
// 獲取寬度測量規格中的size
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
/*
* 如果爹心里有數
*/
if (modeWidth == MeasureSpec.EXACTLY) {
// 那么兒子也不要讓爹難做就取爹給的大小吧
resultWidth = sizeWidth;
}
/*
* 如果爹心里沒數
*/
else {
// 那么兒子可要自己看看自己需要多大了
resultWidth = mBitmap.getWidth();
/*
* 如果爹給兒子的是一個限制值
*/
if (modeWidth == MeasureSpec.AT_MOST) {
// 那么兒子自己的需求就要跟爹的限制比比看誰小要誰
resultWidth = Math.min(resultWidth, sizeWidth);
}
}
int resultHeight = 0;
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
if (modeHeight == MeasureSpec.EXACTLY) {
resultHeight = sizeHeight;
} else {
resultHeight = mBitmap.getHeight();
if (modeHeight == MeasureSpec.AT_MOST) {
resultHeight = Math.min(resultHeight, sizeHeight);
}
}
// 設置測量尺寸
setMeasuredDimension(resultWidth, resultHeight);
}
~~~
如上代碼所示我們從父容器傳來的MeasureSpec中分離出了mode和size,size只是一個期望值我們需要根據mode來計算最終的size,如果父容器對子元素沒有一個確切的大小那么我們就需要嘗試去計算子元素也就是我們的自定義View的大小,而這部分大小更多的是由我們也就是開發者去根據實際情況計算的,這里我們模擬的是一個顯示圖片的控件,那么控件的實際大小就應該跟我們的圖片一致,但是雖然我們可以做出一定的決定也要考慮父容器的限制值,當mode為AT_MOST時size則是父容器給予我們的一個最大值,我們控件的大小就不應該超過這個值。下面是運行效果:

如我所說,控件的實際大小需要根據我們的實際需求去計算,這里我更改一下xml為我們的ImgView加一個內邊距值:
~~~
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFFFF"
android:orientation="vertical" >
<com.aigestudio.customviewdemo.views.ImgView
android:id="@+id/main_pv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="20dp" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
</LinearLayout>
~~~
這時你會發現蛋疼了……毫無內邊距的效果,而在這種情況下我們則需在計算控件尺寸時考慮內邊距的大小:
~~~
resultWidth = mBitmap.getWidth() + getPaddingLeft() + getPaddingRight();
resultHeight = mBitmap.getHeight() + getPaddingTop() + getPaddingBottom();
~~~
這時我們就有了內邊距的效果對吧:

誒、等等,好像不對啊,上邊距和左邊距為什么沒有了?原因很簡單,因為我們在繪制時并沒有考慮到Padding的影響,下面我們更改一下繪制邏輯:
~~~
@Override
protected void onDraw(Canvas canvas) {
// 繪制位圖
canvas.drawBitmap(mBitmap, getPaddingLeft(), getPaddingTop(), null);
}
~~~
這時我們的內邊距就完美了:

很多朋友問那Margin外邊距呢??淡定,外邊距輪不到view來算,Andorid將其封裝在LayoutParams內交由父容器統一處理。很多時候我們的控件往往不只是一張簡單的圖片那么乏味,比如類似圖標的效果:

一個圖標常常除了一張圖片外底部還有一個title,這時我們的測量邏輯就應該做出相應的改變了,這里我用一個新的IconView去做:
~~~
/**
*
* @author AigeStudio {@link http://blog.csdn.net/aigestudio}
* @since 2015/1/13
*
*/
public class IconView extends View {
private Bitmap mBitmap;// 位圖
private TextPaint mPaint;// 繪制文本的畫筆
private String mStr;// 繪制的文本
private float mTextSize;// 畫筆的文本尺寸
/**
* 寬高枚舉類
*
* @author AigeStudio {@link http://blog.csdn.net/aigestudio}
*
*/
private enum Ratio {
WIDTH, HEIGHT
}
public IconView(Context context, AttributeSet attrs) {
super(context, attrs);
// 計算參數
calArgs(context);
// 初始化
init();
}
/**
* 參數計算
*
* @param context
* 上下文環境引用
*/
private void calArgs(Context context) {
// 獲取屏幕寬
int sreenW = MeasureUtil.getScreenSize((Activity) context)[0];
// 計算文本尺寸
mTextSize = sreenW * 1 / 10F;
}
/**
* 初始化
*/
private void init() {
/*
* 獲取Bitmap
*/
if (null == mBitmap) {
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.logo);
}
/*
* 為mStr賦值
*/
if (null == mStr || mStr.trim().length() == 0) {
mStr = "AigeStudio";
}
/*
* 初始化畫筆并設置參數
*/
mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.LINEAR_TEXT_FLAG);
mPaint.setColor(Color.LTGRAY);
mPaint.setTextSize(mTextSize);
mPaint.setTextAlign(Paint.Align.CENTER);
mPaint.setTypeface(Typeface.DEFAULT_BOLD);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 設置測量后的尺寸
setMeasuredDimension(getMeasureSize(widthMeasureSpec, Ratio.WIDTH), getMeasureSize(heightMeasureSpec, Ratio.HEIGHT));
}
/**
* 獲取測量后的尺寸
*
* @param measureSpec
* 測量規格
* @param ratio
* 寬高標識
* @return 寬或高的測量值
*/
private int getMeasureSize(int measureSpec, Ratio ratio) {
// 聲明臨時變量保存測量值
int result = 0;
/*
* 獲取mode和size
*/
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
/*
* 判斷mode的具體值
*/
switch (mode) {
case MeasureSpec.EXACTLY:// EXACTLY時直接賦值
result = size;
break;
default:// 默認情況下將UNSPECIFIED和AT_MOST一并處理
if (ratio == Ratio.WIDTH) {
float textWidth = mPaint.measureText(mStr);
result = ((int) (textWidth >= mBitmap.getWidth() ? textWidth : mBitmap.getWidth())) + getPaddingLeft() + getPaddingRight();
} else if (ratio == Ratio.HEIGHT) {
result = ((int) ((mPaint.descent() - mPaint.ascent()) * 2 + mBitmap.getHeight())) + getPaddingTop() + getPaddingBottom();
}
/*
* AT_MOST時判斷size和result的大小取小值
*/
if (mode == MeasureSpec.AT_MOST) {
result = Math.min(result, size);
}
break;
}
return result;
}
@Override
protected void onDraw(Canvas canvas) {
/*
* 繪制
* 參數就不做單獨處理了因為只會Draw一次不會頻繁調用
*/
canvas.drawBitmap(mBitmap, getWidth() / 2 - mBitmap.getWidth() / 2, getHeight() / 2 - mBitmap.getHeight() / 2, null);
canvas.drawText(mStr, getWidth() / 2, mBitmap.getHeight() + getHeight() / 2 - mBitmap.getHeight() / 2 - mPaint.ascent(), mPaint);
}
}
~~~
在xml文件中對其引用并加入一些系統自帶的控件:
~~~
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFFFF"
android:orientation="vertical" >
<com.aigestudio.customviewdemo.views.IconView
android:id="@+id/main_pv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="50dp" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
</LinearLayout>
~~~
效果如下:

好了就先這樣吧,上面我們曾說過View的測量大小是由View和其父容器共同決定的,但是上述源碼的分析中我們其實并沒有體現,因為它們都在ViewGroup中,這里我們就要涉及ViewGroup中與測量相關的另外幾個方法:measureChildren、measureChild和measureChildWithMargins還有getChildMeasureSpec,見名知意這幾個方法都跟ViewGroup測量子元素有關,其中measureChildWithMargins和measureChildren類似只是加入了對Margins外邊距的處理,ViewGroup提供對子元素測量的方法從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);
}
}
}
~~~
measureChildren的邏輯很簡單,通過父容器傳入的widthMeasureSpec和heightMeasureSpec遍歷子元素并調用measureChild方法去測量每一個子元素的寬高:
~~~
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
// 獲取子元素的布局參數
final LayoutParams lp = child.getLayoutParams();
/*
* 將父容器的測量規格已經上下和左右的邊距還有子元素本身的布局參數傳入getChildMeasureSpec方法計算最終測量規格
*/
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
// 調用子元素的measure傳入計算好的測量規格
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
~~~
這里我們主要就是看看getChildMeasureSpec方法是如何確定最終測量規格的:
~~~
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 獲取父容器的測量模式和尺寸大小
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
// 這個尺寸應該減去內邊距的值
int size = Math.max(0, specSize - padding);
// 聲明臨時變量存值
int resultSize = 0;
int resultMode = 0;
/*
* 根據模式判斷
*/
switch (specMode) {
case MeasureSpec.EXACTLY: // 父容器尺寸大小是一個確定的值
/*
* 根據子元素的布局參數判斷
*/
if (childDimension >= 0) { //如果childDimension是一個具體的值
// 那么就將該值作為結果
resultSize = childDimension;
// 而這個值也是被確定的
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的布局參數為MATCH_PARENT
// 那么就將父容器的大小作為結果
resultSize = size;
// 因為父容器的大小是被確定的所以子元素大小也是可以被確定的
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的布局參數為WRAP_CONTENT
// 那么就將父容器的大小作為結果
resultSize = size;
// 但是子元素的大小包裹了其內容后不能超過父容器
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST: // 父容器尺寸大小擁有一個限制值
/*
* 根據子元素的布局參數判斷
*/
if (childDimension >= 0) { //如果childDimension是一個具體的值
// 那么就將該值作為結果
resultSize = childDimension;
// 而這個值也是被確定的
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的布局參數為MATCH_PARENT
// 那么就將父容器的大小作為結果
resultSize = size;
// 因為父容器的大小是受到限制值的限制所以子元素的大小也應該受到父容器的限制
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的布局參數為WRAP_CONTENT
// 那么就將父容器的大小作為結果
resultSize = size;
// 但是子元素的大小包裹了其內容后不能超過父容器
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.UNSPECIFIED: // 父容器尺寸大小未受限制
/*
* 根據子元素的布局參數判斷
*/
if (childDimension >= 0) { //如果childDimension是一個具體的值
// 那么就將該值作為結果
resultSize = childDimension;
// 而這個值也是被確定的
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的布局參數為MATCH_PARENT
// 因為父容器的大小不受限制而對子元素來說也可以是任意大小所以不指定也不限制子元素的大小
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的布局參數為WRAP_CONTENT
// 因為父容器的大小不受限制而對子元素來說也可以是任意大小所以不指定也不限制子元素的大小
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
// 返回封裝后的測量規格
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
~~~
至此我們可以看到一個View的大小由其父容器的測量規格MeasureSpec和View本身的布局參數LayoutParams共同決定,但是即便如此,最終封裝的測量規格也是一個期望值,究竟有多大還是我們調用setMeasuredDimension方法設置的。上面的代碼中有些朋友看了可能會有疑問為什么childDimension >= 0就表示一個確切值呢?原因很簡單,因為在LayoutParams中MATCH_PARENT和WRAP_CONTENT均為負數、哈哈!!正是基于這點,Android巧妙地將實際值和相對的布局參數分離開來。那么我們該如何對ViewGroup進行測量呢?這里為了說明問題,我們自定義一個ViewGroup:
~~~
/**
*
* @author AigeStudio {@link http://blog.csdn.net/aigestudio}
* @since 2015/1/15
*
*/
public class CustomLayout extends ViewGroup {
public CustomLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
}
~~~
ViewGroup中的onLayout方法是一個抽象方法,這意味著你在繼承時必須實現,onLayout的目的是為了確定子元素在父容器中的位置,那么這個步驟理應該由父容器來決定而不是子元素,因此,我們可以猜到View中的onLayout方法應該是一個空實現:
~~~
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
// 省去無數代碼………………
/**
* Called from layout when this view should
* assign a size and position to each of its children.
*
* Derived classes with children should override
* this method and call layout on each of
* their children.
* @param changed This is a new size or position for this view
* @param left Left position, relative to parent
* @param top Top position, relative to parent
* @param right Right position, relative to parent
* @param bottom Bottom position, relative to parent
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
// 省去無數代碼………………
}
~~~
與View不同的是,ViewGroup表示一個容器,其內可以包含多個元素,既可以是一個布局也可以是一個普通的控件,那么在對ViewGroup測量時我們也應該對這些子元素進行測量:
~~~
/**
*
* @author AigeStudio {@link http://blog.csdn.net/aigestudio}
* @since 2015/1/15
*
*/
public class CustomLayout extends ViewGroup {
public CustomLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
/*
* 如果有子元素
*/
if (getChildCount() > 0) {
// 那么對子元素進行測量
measureChildren(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
}
~~~
然后我們在xml布局文件中替換原來的LinearLayout使用我們自定義的布局:
~~~
<com.aigestudio.customviewdemo.views.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFFFF"
android:orientation="vertical" >
<com.aigestudio.customviewdemo.views.IconView
android:id="@+id/main_pv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="50dp" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
</com.aigestudio.customviewdemo.views.CustomLayout>
~~~
運行后你會發現沒有任何東西顯示,為什么呢?如上所說我們需要父容器告訴子元素它的出現位置,而這個過程由onLayout方法去實現,但是此時我們的onLayout方法什么都沒有,子元素自然也不知道自己該往哪擱,自然就什么都沒有咯……知道了原因我們就來實現onLayout的邏輯:
~~~
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
/*
* 如果有子元素
*/
if (getChildCount() > 0) {
// 那么遍歷子元素并對其進行定位布局
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.layout(0, 0, getMeasuredWidth(), getMeasuredHeight());
}
}
}
~~~
邏輯很簡單,如果有子元素那么我們遍歷這些子元素并調用其layout方法告訴它們自己該在的位置,這里我們就直接讓所有的子元素都從父容器的[0, 0]點開始到[getMeasuredWidth(), getMeasuredHeight()]父容器的測量寬高結束,這么一來,所有的子元素應該都是填充了父容器的對吧:

看到屏幕上的巨大Button我不禁吸了一口屁!這樣的布局太蛋疼,全被Button一個玩完了還搞毛,可不可以像LinearLayout那樣挨個顯示呢?答案是肯定的!我們來修改下onLayout的邏輯:
~~~
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
/*
* 如果有子元素
*/
if (getChildCount() > 0) {
// 聲明一個臨時變量存儲高度倍增值
int mutilHeight = 0;
// 那么遍歷子元素并對其進行定位布局
for (int i = 0; i < getChildCount(); i++) {
// 獲取一個子元素
View child = getChildAt(i);
// 通知子元素進行布局
child.layout(0, mutilHeight, child.getMeasuredWidth(), child.getMeasuredHeight() + mutilHeight);
// 改變高度倍增值
mutilHeight += child.getMeasuredHeight();
}
}
}
~~~
可以看到我們通過一個mutilHeight來存儲高度倍增值,每一次子元素布局完后將當前mutilHeight與當前子元素的高度相加并在下一個子元素布局時在高度上加上mutilHeight,效果如下:

是不是和上面LinearLayout效果有點一樣了?當然LinearLayout的布局邏輯遠比我們的復雜得多,我們呢也只是對其進行一個簡單的模擬而已。大家注意到ViewGroup的onLayout方法的簽名列表中有五個參數,其中boolean changed表示是否與上一次位置不同,其具體值在View的layout方法中通過setFrame等方法確定:
~~~
public void layout(int l, int t, int r, int b) {
// 省略一些代碼……
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
// 省略大量代碼……
}
~~~
而剩下的四個參數則表示當前View與父容器的相對距離,如下圖:

好了,說到這里想必大家對ViewGroup的測量也有一定的了解了,但是這必定不是測量過程全部,如我上面所說,測量的具體過程因控件而異,上面我們曾因為給我們的自定義View加了內邊距后修改了繪制的邏輯,因為我們需要在繪制時考慮內邊距的影響,而我們的自定義ViewGroup呢?是不是也一樣呢?這里我給其加入60dp的內邊距:
~~~
<com.aigestudio.customviewdemo.views.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="60dp"
android:background="#FFFFFFFF"
android:orientation="vertical" >
<com.aigestudio.customviewdemo.views.IconView
android:id="@+id/main_pv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="50dp" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
</com.aigestudio.customviewdemo.views.CustomLayout>
~~~
運行后效果如下:

內邊距把我們的子元素給“吃”掉了,那么也就是說我們在對子元素進行定位時應該進一步考慮到父容器內邊距的影響對吧,OK,我們重理onLayout的邏輯:
~~~
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 獲取父容器內邊距
int parentPaddingLeft = getPaddingLeft();
int parentPaddingTop = getPaddingTop();
/*
* 如果有子元素
*/
if (getChildCount() > 0) {
// 聲明一個臨時變量存儲高度倍增值
int mutilHeight = 0;
// 那么遍歷子元素并對其進行定位布局
for (int i = 0; i < getChildCount(); i++) {
// 獲取一個子元素
View child = getChildAt(i);
// 通知子元素進行布局
// 此時考慮父容器內邊距的影響
child.layout(parentPaddingLeft, mutilHeight + parentPaddingTop, child.getMeasuredWidth() + parentPaddingLeft, child.getMeasuredHeight() + mutilHeight + parentPaddingTop);
// 改變高度倍增值
mutilHeight += child.getMeasuredHeight();
}
}
}
~~~
此時的效果如下:

既然內邊距如此,那么Margins外邊距呢?我們來看看,在xml布局文件中為我們的CustomLayout加一個margins:
~~~
<com.aigestudio.customviewdemo.views.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="30dp"
android:padding="20dp"
android:background="#FF597210"
android:orientation="vertical" >
<com.aigestudio.customviewdemo.views.IconView
android:id="@+id/main_pv"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="AigeStudio" />
</com.aigestudio.customviewdemo.views.CustomLayout>
~~~
效果如下:

OK,目測沒什么問題,可是當我們為子元素設置外邊距時,問題就來了……不管你怎么設都不會有任何效果,原因很簡單,我們上面也說了,Margins是由父容器來處理,而我們的CustomLayout中并沒有對其做任何的處理,那么我們應該怎么做呢?首先要知道Margins封裝在LayoutParams中,如果我們想實現自己對其的處理那么我們必然也有必要實現自己布局的LayoutParams:
~~~
/**
*
* @author AigeStudio {@link http://blog.csdn.net/aigestudio}
* @since 2015/1/15
*
*/
public class CustomLayout extends ViewGroup {
// 省略部分代碼…………
/**
*
* @author AigeStudio {@link http://blog.csdn.net/aigestudio}
*
*/
public static class CustomLayoutParams extends MarginLayoutParams {
public CustomLayoutParams(MarginLayoutParams source) {
super(source);
}
public CustomLayoutParams(android.view.ViewGroup.LayoutParams source) {
super(source);
}
public CustomLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public CustomLayoutParams(int width, int height) {
super(width, height);
}
}
}
~~~
我們在我們的CustomLayout中生成了一個靜態內部類CustomLayoutParams,保持其默認的構造方法即可,這里我們什么也沒做,當然你可以定義自己的一些屬性或邏輯處理,因控件而異這里不多說了,后面慢慢會用到。然后在我們的CustomLayout中重寫所有與LayoutParams相關的方法,返回我們自己的CustomLayoutParams:
~~~
/**
*
* @author AigeStudio {@link http://blog.csdn.net/aigestudio}
* @since 2015/1/15
*
*/
public class CustomLayout extends ViewGroup {
// 省略部分代碼…………
/**
* 生成默認的布局參數
*/
@Override
protected CustomLayoutParams generateDefaultLayoutParams() {
return new CustomLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
}
/**
* 生成布局參數
* 將布局參數包裝成我們的
*/
@Override
protected android.view.ViewGroup.LayoutParams generateLayoutParams(android.view.ViewGroup.LayoutParams p) {
return new CustomLayoutParams(p);
}
/**
* 生成布局參數
* 從屬性配置中生成我們的布局參數
*/
@Override
public android.view.ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new CustomLayoutParams(getContext(), attrs);
}
/**
* 檢查當前布局參數是否是我們定義的類型這在code聲明布局參數時常常用到
*/
@Override
protected boolean checkLayoutParams(android.view.ViewGroup.LayoutParams p) {
return p instanceof CustomLayoutParams;
}
// 省略部分代碼…………
}
~~~
最后更改我們的測量邏輯:
~~~
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 聲明臨時變量存儲父容器的期望值
int parentDesireWidth = 0;
int parentDesireHeight = 0;
/*
* 如果有子元素
*/
if (getChildCount() > 0) {
// 那么遍歷子元素并對其進行測量
for (int i = 0; i < getChildCount(); i++) {
// 獲取子元素
View child = getChildAt(i);
// 獲取子元素的布局參數
CustomLayoutParams clp = (CustomLayoutParams) child.getLayoutParams();
// 測量子元素并考慮外邊距
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
// 計算父容器的期望值
parentDesireWidth += child.getMeasuredWidth() + clp.leftMargin + clp.rightMargin;
parentDesireHeight += child.getMeasuredHeight() + clp.topMargin + clp.bottomMargin;
}
// 考慮父容器的內邊距
parentDesireWidth += getPaddingLeft() + getPaddingRight();
parentDesireHeight += getPaddingTop() + getPaddingBottom();
// 嘗試比較建議最小值和期望值的大小并取大值
parentDesireWidth = Math.max(parentDesireWidth, getSuggestedMinimumWidth());
parentDesireHeight = Math.max(parentDesireHeight, getSuggestedMinimumHeight());
}
// 設置最終測量值O
setMeasuredDimension(resolveSize(parentDesireWidth, widthMeasureSpec), resolveSize(parentDesireHeight, heightMeasureSpec));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 獲取父容器內邊距
int parentPaddingLeft = getPaddingLeft();
int parentPaddingTop = getPaddingTop();
/*
* 如果有子元素
*/
if (getChildCount() > 0) {
// 聲明一個臨時變量存儲高度倍增值
int mutilHeight = 0;
// 那么遍歷子元素并對其進行定位布局
for (int i = 0; i < getChildCount(); i++) {
// 獲取一個子元素
View child = getChildAt(i);
CustomLayoutParams clp = (CustomLayoutParams) child.getLayoutParams();
// 通知子元素進行布局
// 此時考慮父容器內邊距和子元素外邊距的影響
child.layout(parentPaddingLeft + clp.leftMargin, mutilHeight + parentPaddingTop + clp.topMargin, child.getMeasuredWidth() + parentPaddingLeft + clp.leftMargin, child.getMeasuredHeight() + mutilHeight + parentPaddingTop + clp.topMargin);
// 改變高度倍增值
mutilHeight += child.getMeasuredHeight() + clp.topMargin + clp.bottomMargin;
}
}
}
~~~
布局文件如下:
~~~
<com.aigestudio.customviewdemo.views.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF597210"
android:orientation="vertical" >
<com.aigestudio.customviewdemo.views.IconView
android:id="@+id/main_pv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="30dp"
android:layout_marginTop="5dp" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginLeft="2dp"
android:layout_marginRight="8dp"
android:layout_marginTop="4dp"
android:text="AigeStudio" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="28dp"
android:layout_marginLeft="7dp"
android:layout_marginRight="19dp"
android:layout_marginTop="14dp"
android:background="#FF166792"
android:text="AigeStudio" />
</com.aigestudio.customviewdemo.views.CustomLayout>
~~~
運行效果如下:

~~~~~~~~好了好了、不講了,View的基本測量過程大致就是這樣,如我所說測量并不是定式的過程,總會因控件而已,我們在自定義控件時要準確地測量,一定要準確,測量的結果會直接影響后面的布局定位、繪制甚至交互,所以馬虎不得,你也可以看到Android給我們提供的LinearLayout、FrameLayout等布局都有極其嚴謹的測量邏輯,為的就是確保測量結果的準確。
本篇幅雖長,但是我們其實就講了三點:
一個界面窗口的元素構成
framework對View測量的控制處理
View和ViewGroup的簡單測量
好了、不說了、實在說不動了………………到此為止&¥……#¥……%#¥%#¥%#%¥哦!對了,文章開頭我給各位設了一個問題,不知道大家發現沒有,本來說這節順帶講了,看著篇幅太長下節再說吧……
源碼下載:[傳送門](http://download.csdn.net/detail/aigestudio/8384821)