Android中View和ViewGroup的measure和layout過程分析

轉自:http://blog.csdn.net/aigestudio/article/details/42989325

要在數量上統計中國菜的品種,在地域上毫無爭議地劃分菜系,在今天,是一件幾乎不可能完成的事……Cut…………抱歉……忘吃藥了,再來一遍。如果非要對自定義控件的流程進行一個簡單的劃分,我會嘗試將其分爲三大部分:控件的繪製、控件的測量和控件的交互行爲。前面我們用了六節的篇幅和一個翻頁的例子來對控件的繪製有了一個全新的認識但是我們所做出的所有例子都是不完美的,爲什麼這麼說呢,還是先來看個sample:

  1. /** 
  2.  *  
  3.  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  4.  * @since 2015/1/12 
  5.  *  
  6.  */  
  7. public class ImgView extends View {  
  8.     private Bitmap mBitmap;// 位圖對象  
  9.   
  10.     public ImgView(Context context, AttributeSet attrs) {  
  11.         super(context, attrs);  
  12.     }  
  13.   
  14.     @Override  
  15.     protected void onDraw(Canvas canvas) {  
  16.         // 繪製位圖  
  17.         canvas.drawBitmap(mBitmap, 00null);  
  18.     }  
  19.   
  20.     /** 
  21.      * 設置位圖 
  22.      *  
  23.      * @param bitmap 
  24.      *            位圖對象 
  25.      */  
  26.     public void setBitmap(Bitmap bitmap) {  
  27.         this.mBitmap = bitmap;  
  28.     }  
  29. }  
這個例子呢非常簡單,我們用它來模擬類似ImageView的效果顯示一張圖片,在MainActivity中我們獲取該控件併爲其設置Bitmap:

  1. /** 
  2.  * 主界面 
  3.  *  
  4.  * @author Aige {@link http://blog.csdn.net/aigestudio} 
  5.  * @since 2014/11/17 
  6.  */  
  7. public class MainActivity extends Activity {  
  8.     private ImgView mImgView;  
  9.   
  10.     @Override  
  11.     public void onCreate(Bundle savedInstanceState) {  
  12.         super.onCreate(savedInstanceState);  
  13.         setContentView(R.layout.activity_main);  
  14.   
  15.         mImgView = (ImgView) findViewById(R.id.main_pv);  
  16.         Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.lovestory);  
  17.         mImgView.setBitmap(bitmap);  
  18.     }  
  19. }  
此時運行效果如下:


很簡單對吧,可是上面的代碼其實是有個問題的,至於什麼問題?我們待會再說,就看你通過前面我們的學習能不能發現了。這一節我們重點是控件的測量,大家不知道注意沒有,這個系列文章的命名我用了“控件”而非“View”其實目的就是說明我們的控件不僅包括View也應該包含ViewGroup,當然你也可以以官方的方式將其分爲控件和佈局,不過我更喜歡View和ViewGroup,好了廢話不說,我們先來看看View的測量方式,上面的代碼中MainActivity對應的佈局文件如下:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="match_parent"  
  3.     android:layout_height="match_parent"  
  4.     android:background="#FFFFFFFF"  
  5.     android:orientation="vertical" >  
  6.   
  7.     <com.aigestudio.customviewdemo.views.ImgView  
  8.         android:id="@+id/main_pv"  
  9.         android:layout_width="match_parent"  
  10.         android:layout_height="match_parent" />  
  11. </LinearLayout>  
既然我們的自定義View也算一個控件那麼我們也可以像平時做佈局那樣往我們的LinearLayout中添加各種各樣的其他控件對吧:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="match_parent"  
  3.     android:layout_height="match_parent"  
  4.     android:background="#FFFFFFFF"  
  5.     android:orientation="vertical" >  
  6.   
  7.     <com.aigestudio.customviewdemo.views.ImgView  
  8.         android:id="@+id/main_pv"  
  9.         android:layout_width="match_parent"  
  10.         android:layout_height="match_parent" />  
  11.   
  12.     <Button  
  13.         android:layout_width="wrap_content"  
  14.         android:layout_height="wrap_content"  
  15.         android:text="AigeStudio" />  
  16.   
  17.     <TextView  
  18.         android:layout_width="wrap_content"  
  19.         android:layout_height="wrap_content"  
  20.         android:text="AigeStudio" />  
  21.   
  22. </LinearLayout>  
但是運行後你卻發現我們的Button和TextView卻沒有顯示在屏幕上,這時你可能會說那當然咯,因爲我們的ImgViewlayout_width和layout_height均爲match_parent,可是即便你將其改成wrap_content:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="match_parent"  
  3.     android:layout_height="match_parent"  
  4.     android:background="#FFFFFFFF"  
  5.     android:orientation="vertical" >  
  6.   
  7.     <com.aigestudio.customviewdemo.views.ImgView  
  8.         android:id="@+id/main_pv"  
  9.         android:layout_width="wrap_content"  
  10.         android:layout_height="wrap_content" />  
  11.   
  12.     <!-- ……省略一些代碼…… -->  
  13. </LinearLayout>  
結果也一樣,這時你肯定很困惑,不解的主要原因是沒有搞懂View的測量機制,在前面的幾節中我們或多或少有提到控件的測量,也曾經說過Android提供給我們能夠操縱控件測量的方法是onMeasure:

  1. @Override  
  2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  3.     super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
  4. }  
默認情況下onMeasure方法中只是簡單地將簽名列表中的兩個int型參數回傳給父類的onMeasure方法,然後由父類的方法去計算出最終的測量值。但是,這裏有個問題非常重要,就是onMeasure簽名列表中的這兩個參數是從何而來,這裏可以告訴大家的是,這兩個參數是由view的父容器,代碼中也就是我們的LinearLayout傳遞進來的,很多初學Android的朋友會將位於xml佈局文件頂端的控件稱之爲根佈局,比如這裏我們的LinearLayout,而事實上在Android的GUI框架中,這個LinearLayout還稱不上根佈局,我們知道一個Activity可以對應一個View(也可以是ViewGroup),很多情況下我們會通過Activity的setContentView方法去設置我們的View:

  1. @Override  
  2. public void onCreate(Bundle savedInstanceState) {  
  3.     super.onCreate(savedInstanceState);  
  4.     setContentView(R.layout.activity_main);  
  5. }  
setContentView在Activity內的實現也非常簡單,就是調用getWindow方法獲取一個Window類型的對象並調用其setContentView方法:

  1. public void setContentView(int layoutResID) {  
  2.     getWindow().setContentView(layoutResID);  
  3.     initActionBar();  
  4. }  
而這個Window對象

  1. public Window getWindow() {  
  2.     return mWindow;  
  3. }  
其本質也就是一個PhoneWindow,在Activity的attach方法中通過makeNewWindow生成:

  1. final void attach(Context context, ActivityThread aThread,  
  2.     // 此處省去一些代碼……  
  3.   
  4.     mWindow = PolicyManager.makeNewWindow(this);  
  5.     mWindow.setCallback(this);  
  6.     mWindow.getLayoutInflater().setPrivateFactory(this);  
  7.     if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {  
  8.         mWindow.setSoftInputMode(info.softInputMode);  
  9.     }  
  10.     if (info.uiOptions != 0) {  
  11.         mWindow.setUiOptions(info.uiOptions);  
  12.     }  
  13.       
  14.     // 此處省去巨量代碼……  
  15. }  
在PolicyManager中通過反射的方式獲取com.android.internal.policy.impl.Policy的一個實例:

  1. public final class PolicyManager {  
  2.     private static final String POLICY_IMPL_CLASS_NAME =  
  3.         "com.android.internal.policy.impl.Policy";  
  4.   
  5.     private static final IPolicy sPolicy;  
  6.   
  7.     static {  
  8.         try {  
  9.             Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME);  
  10.             sPolicy = (IPolicy)policyClass.newInstance();  
  11.         } catch (ClassNotFoundException ex) {  
  12.             throw new RuntimeException(  
  13.                     POLICY_IMPL_CLASS_NAME + " could not be loaded", ex);  
  14.         } catch (InstantiationException ex) {  
  15.             throw new RuntimeException(  
  16.                     POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);  
  17.         } catch (IllegalAccessException ex) {  
  18.             throw new RuntimeException(  
  19.                     POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);  
  20.         }  
  21.     }  
  22.   
  23.     // 省去構造方法……  
  24.   
  25.     public static Window makeNewWindow(Context context) {  
  26.         return sPolicy.makeNewWindow(context);  
  27.     }  
  28.   
  29.     // 省去無關代碼……  
  30. }  
並通過其內部的makeNewWindow實現返回一個PhoneWindow對象:

  1. public Window makeNewWindow(Context context) {  
  2.     return new PhoneWindow(context);  
  3. }  
PhoneWindow是Window的一個子類,其對Window中定義的大量抽象方法作了具體的實現,比如我們的setContentView方法在Window中僅做了一個抽象方法定義:

  1. public abstract class Window {  
  2.     // 省去不可估量的代碼……  
  3.   
  4.     public abstract void setContentView(int layoutResID);  
  5.   
  6.     public abstract void setContentView(View view);  
  7.   
  8.     public abstract void setContentView(View view, ViewGroup.LayoutParams params);  
  9.   
  10.     // 省去數以億計的代碼……  
  11. }  
其在PhoneWindow中都有具體的實現:

  1. public class PhoneWindow extends Window implements MenuBuilder.Callback {  
  2.     // 省去草泥馬個代碼……  
  3.   
  4.     @Override  
  5.     public void setContentView(int layoutResID) {  
  6.         if (mContentParent == null) {  
  7.             installDecor();  
  8.         } else {  
  9.             mContentParent.removeAllViews();  
  10.         }  
  11.         mLayoutInflater.inflate(layoutResID, mContentParent);  
  12.         final Callback cb = getCallback();  
  13.         if (cb != null && !isDestroyed()) {  
  14.             cb.onContentChanged();  
  15.         }  
  16.     }  
  17.   
  18.     @Override  
  19.     public void setContentView(View view) {  
  20.         setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));  
  21.     }  
  22.   
  23.     @Override  
  24.     public void setContentView(View view, ViewGroup.LayoutParams params) {  
  25.         if (mContentParent == null) {  
  26.             installDecor();  
  27.         } else {  
  28.             mContentParent.removeAllViews();  
  29.         }  
  30.         mContentParent.addView(view, params);  
  31.         final Callback cb = getCallback();  
  32.         if (cb != null && !isDestroyed()) {  
  33.             cb.onContentChanged();  
  34.         }  
  35.     }  
  36.   
  37.     // 省去法克魷個代碼……  
  38. }  
當然如果你要是使用了TV的SDK那麼這裏就不是PhoneWindow而是TVWindow了,至於是不是呢?留給大家去驗證。到這裏我們都還沒完,在PhoneWindow的setContentView方法中先會去判斷mContentParent這個引用是否爲空,如果爲空則表示我們是第一次生成那麼調用installDecor方法去生成一些具體的對象否則清空該mContentParent下的所有子元素(注意mContentParent是一個ViewGroup)並通過LayoutInflater將xml佈局轉換爲View Tree添加至mContentParent中(這裏根據setContentView(int layoutResID)方法分析,其他重載方法類似),installDecor方法做的事相對多但不復雜,首先是對DecorView類型的mDecor成員變量賦值繼而將其注入generateLayout方法生成我們的mContentParent:

  1. private void installDecor() {  
  2.     if (mDecor == null) {  
  3.         mDecor = generateDecor();  
  4.         //  省省省……  
  5.     }  
  6.   
  7.     if (mContentParent == null) {  
  8.         mContentParent = generateLayout(mDecor);  
  9.   
  10.         //  省省省……  
  11.     }  
  12.   
  13.     //  省省省……  
  14. }  
generateLayout方法中做的事就多了,簡直可以跟performTraversals拼,這裏不貼代碼了簡單分析一下,generateLayout方法中主要根據當前我們的Style類型爲當前Window選擇不同的佈局文件,看到這裏,想必大家也該意識到,這纔是我們的“根佈局”,其會指定一個用來存放我們自定義佈局文件(也就是我們口頭上常說的根佈局比如我們例子中的LinearLayout)的ViewGroup,一般情況下這個ViewGroup的重任由FrameLayout來承擔,這也是爲什麼我們在獲取我們xml佈局文件中的頂層佈局時調用其getParent()方法會返回FrameLayout對象的原因,其id爲android:id="@android:id/content":

  1. protected ViewGroup generateLayout(DecorView decor) {  
  2.     // 省去巨量代碼……  
  3.   
  4.     ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);  
  5.   
  6.     // 省去一些代碼……  
  7. }  
在這個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承擔,我們在前面提到過這個類,其負責的東西很多,比如我們窗口的顯示、用戶的輸入輸出當然還有關於處理我們繪製流程的方法:

  1. private void performTraversals() {  
  2.     // ………………啦啦啦啦………………  
  3. }  
performTraversals方法是處理繪製流程的一個開始,內部邏輯相當相當多&複雜,雖然沒有View類複雜……但是讓我選的話我寧願看整個View類也不願看performTraversals方法那邪惡的邏輯…………囧,在該方法中我們可以看到如下的一段邏輯(具體各類變量的賦值就不貼了實在太多):

  1. private void performTraversals() {  
  2.     // ………省略宇宙塵埃數量那麼多的代碼………  
  3.   
  4.     if (!mStopped) {  
  5.         // ……省略一些代碼  
  6.   
  7.         int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);  
  8.         int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);  
  9.   
  10.         // ……省省省  
  11.   
  12.         performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);  
  13.     }  
  14.   
  15.     // ………省略人體細胞數量那麼多的代碼………  
  16. }  
可以看到在performTraversals方法中通過getRootMeasureSpec獲取原始的測量規格並將其作爲參數傳遞給performMeasure方法處理,這裏我們重點來看getRootMeasureSpec方法是如何確定測量規格的,首先我們要知道mWidth, lp.width和mHeight, lp.height這兩組參數的意義,其中lp.width和lp.height均爲MATCH_PARENT,其在mWindowAttributes(WindowManager.LayoutParams類型)將值賦予給lp時就已被確定,mWidth和mHeight表示當前窗口的大小,其值由performTraversals中一系列邏輯計算確定,這裏跳過,而在getRootMeasureSpec中作了如下判斷:

  1. private static int getRootMeasureSpec(int windowSize, int rootDimension) {  
  2.     int measureSpec;  
  3.     switch (rootDimension) {  
  4.   
  5.     case ViewGroup.LayoutParams.MATCH_PARENT:  
  6.         // Window不能調整其大小,強制使根視圖大小與Window一致  
  7.         measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);  
  8.         break;  
  9.     case ViewGroup.LayoutParams.WRAP_CONTENT:  
  10.         // Window可以調整其大小,爲根視圖設置一個最大值  
  11.         measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);  
  12.         break;  
  13.     default:  
  14.         // Window想要一個確定的尺寸,強制將根視圖的尺寸作爲其尺寸  
  15.         measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);  
  16.         break;  
  17.     }  
  18.     return measureSpec;  
  19. }  
也就是說不管如何,我們的根視圖大小必定都是全屏的……

至此,我們算是真正接觸到根視圖的測量規格,爾後這個規格會被由上至下傳遞下去,並由當前view與其父容器共同作用決定最終的測量大小,在View與ViewGroup遞歸調用實現測量的過程中有幾個重要的方法,對於View而言則是measure方法:

  1. public final void measure(int widthMeasureSpec, int heightMeasureSpec) {  
  2.     // 省略部分代碼……  
  3.   
  4.     /* 
  5.      * 判斷當前mPrivateFlags是否帶有PFLAG_FORCE_LAYOUT強制佈局標記 
  6.      * 判斷當前widthMeasureSpec和heightMeasureSpec是否發生了改變 
  7.      */  
  8.     if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||  
  9.             widthMeasureSpec != mOldWidthMeasureSpec ||  
  10.             heightMeasureSpec != mOldHeightMeasureSpec) {  
  11.   
  12.         // 如果發生了改變表示需要重新進行測量此時清除掉mPrivateFlags中已測量的標識位PFLAG_MEASURED_DIMENSION_SET  
  13.         mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;  
  14.   
  15.         resolveRtlPropertiesIfNeeded();  
  16.   
  17.         int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :  
  18.                 mMeasureCache.indexOfKey(key);  
  19.         if (cacheIndex < 0 || sIgnoreMeasureCache) {  
  20.             // 測量View的尺寸  
  21.             onMeasure(widthMeasureSpec, heightMeasureSpec);  
  22.             mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;  
  23.         } else {  
  24.             long value = mMeasureCache.valueAt(cacheIndex);  
  25.   
  26.             setMeasuredDimension((int) (value >> 32), (int) value);  
  27.             mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;  
  28.         }  
  29.   
  30.         /* 
  31.          * 如果mPrivateFlags裏沒有表示已測量的標識位PFLAG_MEASURED_DIMENSION_SET則會拋出異常 
  32.          */  
  33.         if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {  
  34.             throw new IllegalStateException("onMeasure() did not set the"  
  35.                     + " measured dimension by calling"  
  36.                     + " setMeasuredDimension()");  
  37.         }  
  38.   
  39.         // 如果已測量View那麼就可以往mPrivateFlags添加標識位PFLAG_LAYOUT_REQUIRED表示可以進行佈局了  
  40.         mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;  
  41.     }  
  42.   
  43.     // 最後存儲測量完成的測量規格  
  44.     mOldWidthMeasureSpec = widthMeasureSpec;  
  45.     mOldHeightMeasureSpec = heightMeasureSpec;  
  46.   
  47.     mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |  
  48.             (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension  
  49. }  
可以看到,View對控件的測量是在onMeasure方法中進行的,也就是文章開頭我們在自定義View中重寫的onMeasure方法,但是我們並沒有對其做任何的處理,也就是說保持了其在父類View中的默認實現,其默認實現也很簡單:

  1. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  2.     setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
  3.             getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  
  4. }  
其直接調用了setMeasuredDimension方法爲其設置了兩個計算後的測量值:

  1. protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {  
  2.     // 省去部分代碼……  
  3.   
  4.     // 設置測量後的寬高  
  5.     mMeasuredWidth = measuredWidth;  
  6.     mMeasuredHeight = measuredHeight;  
  7.   
  8.     // 重新將已測量標識位存入mPrivateFlags標識測量的完成  
  9.     mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;  
  10. }  
回到onMeasure方法,我們來看看這兩個測量值具體是怎麼獲得的,其實非常簡單,首先來看getSuggestedMinimumWidth方法:

  1. protected int getSuggestedMinimumWidth() {  
  2.     return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());  
  3. }  
如果背景爲空那麼我們直接返回mMinWidth最小寬度否則就在mMinWidth和背景最小寬度之間取一個最大值,getSuggestedMinimumHeight類同,mMinWidth和mMinHeight我沒記錯的話應該都是100px,而getDefaultSize方法呢也很簡單:

  1. public static int getDefaultSize(int size, int measureSpec) {  
  2.     // 將我們獲得的最小值賦給result  
  3.     int result = size;  
  4.   
  5.     // 從measureSpec中解算出測量規格的模式和尺寸  
  6.     int specMode = MeasureSpec.getMode(measureSpec);  
  7.     int specSize = MeasureSpec.getSize(measureSpec);  
  8.   
  9.     /* 
  10.      * 根據測量規格模式確定最終的測量尺寸 
  11.      */  
  12.     switch (specMode) {  
  13.     case MeasureSpec.UNSPECIFIED:  
  14.         result = size;  
  15.         break;  
  16.     case MeasureSpec.AT_MOST:  
  17.     case MeasureSpec.EXACTLY:  
  18.         result = specSize;  
  19.         break;  
  20.     }  
  21.     return result;  
  22. }  
注意上述代碼中當模式爲AT_MOST和EXACTLY時均會返回解算出的測量尺寸,還記得上面我們說的PhoneWindow、DecorView麼從它們那裏獲取到的測量規格層層傳遞到我們的自定義View中,這就是爲什麼我們的View在默認情況下不管是math_parent還是warp_content都能佔滿父容器的剩餘空間(這裏面還有父佈局LinearLayout的作用就先略過了瞭解即可)。上述onMeasure的過程則是View默認的處理過程,如果我們不喜歡Android幫我們處理那麼我們可以自己重寫onMeasure實現自己的測量邏輯:

  1. @Override  
  2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  3.     // 設置測量尺寸  
  4.     setMeasuredDimension(250250);  
  5. }  
最簡單的粗暴的就是直接將兩個值作爲參數傳入setMeasuredDimension方法,效果如下:


當然這樣不好,用Android官方的話來說就是太過“專政”,因爲它完全摒棄了父容器的意願,完全由自己決定了大小,如果大家逛blog看技術文章或者聽別人討論常常會聽到別人這麼說view的最終測量尺寸是由view本身何其父容器共同決定的,至於如何共同決定我們呆會再說,這裏我們先看看如何能在一定程度上順應爹的“意願”呢?從View默認的測量模式中我們可以看到它頻繁使用了一個叫做MeasureSpec的類,而在ViewRootImpl中呢也有大量用到該類,該類的具體說明大家可以圍觀我早期的一篇文章:http://blog.csdn.net/aigestudio/article/details/38636531,裏面有對MeasureSpec類的詳細說明,這裏我就簡單概述下MeasureSpec類中的三個Mode常量值的意義,其中UNSPECIFIED表示未指定,爹不會對兒子作任何的束縛,兒子想要多大都可以;EXACTLY表示完全的,意爲兒子多大爹心裏有數,爹早已算好了;AT_MOST表示至多,爹已經爲兒子設置好了一個最大限制,兒子你不能比這個值大,不能再多了!父容器所謂的“意圖”其實就由上述三個常量值表現,既然如此我們就該對這三個Mode常量做一個判斷纔行,不然怎麼知道爹的意圖呢:

  1. @Override  
  2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  3.     // 聲明一個臨時變量來存儲計算出的測量值  
  4.     int resultWidth = 0;  
  5.   
  6.     // 獲取寬度測量規格中的mode  
  7.     int modeWidth = MeasureSpec.getMode(widthMeasureSpec);  
  8.   
  9.     // 獲取寬度測量規格中的size  
  10.     int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);  
  11.   
  12.     /* 
  13.      * 如果爹心裏有數 
  14.      */  
  15.     if (modeWidth == MeasureSpec.EXACTLY) {  
  16.         // 那麼兒子也不要讓爹難做就取爹給的大小吧  
  17.         resultWidth = sizeWidth;  
  18.     }  
  19.     /* 
  20.      * 如果爹心裏沒數 
  21.      */  
  22.     else {  
  23.         // 那麼兒子可要自己看看自己需要多大了  
  24.         resultWidth = mBitmap.getWidth();  
  25.   
  26.         /* 
  27.          * 如果爹給兒子的是一個限制值 
  28.          */  
  29.         if (modeWidth == MeasureSpec.AT_MOST) {  
  30.             // 那麼兒子自己的需求就要跟爹的限制比比看誰小要誰  
  31.             resultWidth = Math.min(resultWidth, sizeWidth);  
  32.         }  
  33.     }  
  34.   
  35.     int resultHeight = 0;  
  36.     int modeHeight = MeasureSpec.getMode(heightMeasureSpec);  
  37.     int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);  
  38.   
  39.     if (modeHeight == MeasureSpec.EXACTLY) {  
  40.         resultHeight = sizeHeight;  
  41.     } else {  
  42.         resultHeight = mBitmap.getHeight();  
  43.         if (modeHeight == MeasureSpec.AT_MOST) {  
  44.             resultHeight = Math.min(resultHeight, sizeHeight);  
  45.         }  
  46.     }  
  47.   
  48.     // 設置測量尺寸  
  49.     setMeasuredDimension(resultWidth, resultHeight);  
  50. }  
如上代碼所示我們從父容器傳來的MeasureSpec中分離出了mode和size,size只是一個期望值我們需要根據mode來計算最終的size,如果父容器對子元素沒有一個確切的大小那麼我們就需要嘗試去計算子元素也就是我們的自定義View的大小,而這部分大小更多的是由我們也就是開發者去根據實際情況計算的,這裏我們模擬的是一個顯示圖片的控件,那麼控件的實際大小就應該跟我們的圖片一致,但是雖然我們可以做出一定的決定也要考慮父容器的限制值,當mode爲AT_MOST時size則是父容器給予我們的一個最大值,我們控件的大小就不應該超過這個值。下面是運行效果:


如我所說,控件的實際大小需要根據我們的實際需求去計算,這裏我更改一下xml爲我們的ImgView加一個內邊距值:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="match_parent"  
  3.     android:layout_height="match_parent"  
  4.     android:background="#FFFFFFFF"  
  5.     android:orientation="vertical" >  
  6.   
  7.     <com.aigestudio.customviewdemo.views.ImgView  
  8.         android:id="@+id/main_pv"  
  9.         android:layout_width="wrap_content"  
  10.         android:layout_height="wrap_content"  
  11.         android:padding="20dp" />  
  12.   
  13.     <Button  
  14.         android:layout_width="wrap_content"  
  15.         android:layout_height="wrap_content"  
  16.         android:text="AigeStudio" />  
  17.   
  18.     <TextView  
  19.         android:layout_width="wrap_content"  
  20.         android:layout_height="wrap_content"  
  21.         android:text="AigeStudio" />  
  22.   
  23. </LinearLayout>  
這時你會發現蛋疼了……毫無內邊距的效果,而在這種情況下我們則需在計算控件尺寸時考慮內邊距的大小:

  1. resultWidth = mBitmap.getWidth() + getPaddingLeft() + getPaddingRight();  
  2. resultHeight = mBitmap.getHeight() + getPaddingTop() + getPaddingBottom();  
這時我們就有了內邊距的效果對吧:


誒、等等,好像不對啊,上邊距和左邊距爲什麼沒有了?原因很簡單,因爲我們在繪製時並沒有考慮到Padding的影響,下面我們更改一下繪製邏輯:

  1. @Override  
  2. protected void onDraw(Canvas canvas) {  
  3.     // 繪製位圖  
  4.     canvas.drawBitmap(mBitmap, getPaddingLeft(), getPaddingTop(), null);  
  5. }  
這時我們的內邊距就完美了:


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


一個圖標常常除了一張圖片外底部還有一個title,這時我們的測量邏輯就應該做出相應的改變了,這裏我用一個新的IconView去做:

  1. /** 
  2.  *  
  3.  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  4.  * @since 2015/1/13 
  5.  *  
  6.  */  
  7. public class IconView extends View {  
  8.     private Bitmap mBitmap;// 位圖  
  9.     private TextPaint mPaint;// 繪製文本的畫筆  
  10.     private String mStr;// 繪製的文本  
  11.   
  12.     private float mTextSize;// 畫筆的文本尺寸  
  13.   
  14.     /** 
  15.      * 寬高枚舉類 
  16.      *  
  17.      * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  18.      *  
  19.      */  
  20.     private enum Ratio {  
  21.         WIDTH, HEIGHT  
  22.     }  
  23.   
  24.     public IconView(Context context, AttributeSet attrs) {  
  25.         super(context, attrs);  
  26.   
  27.         // 計算參數  
  28.         calArgs(context);  
  29.   
  30.         // 初始化  
  31.         init();  
  32.     }  
  33.   
  34.     /** 
  35.      * 參數計算 
  36.      *  
  37.      * @param context 
  38.      *            上下文環境引用 
  39.      */  
  40.     private void calArgs(Context context) {  
  41.         // 獲取屏幕寬  
  42.         int sreenW = MeasureUtil.getScreenSize((Activity) context)[0];  
  43.   
  44.         // 計算文本尺寸  
  45.         mTextSize = sreenW * 1 / 10F;  
  46.     }  
  47.   
  48.     /** 
  49.      * 初始化 
  50.      */  
  51.     private void init() {  
  52.         /* 
  53.          * 獲取Bitmap 
  54.          */  
  55.         if (null == mBitmap) {  
  56.             mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.logo);  
  57.         }  
  58.   
  59.         /* 
  60.          * 爲mStr賦值 
  61.          */  
  62.         if (null == mStr || mStr.trim().length() == 0) {  
  63.             mStr = "AigeStudio";  
  64.         }  
  65.   
  66.         /* 
  67.          * 初始化畫筆並設置參數 
  68.          */  
  69.         mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG | Paint.LINEAR_TEXT_FLAG);  
  70.         mPaint.setColor(Color.LTGRAY);  
  71.         mPaint.setTextSize(mTextSize);  
  72.         mPaint.setTextAlign(Paint.Align.CENTER);  
  73.         mPaint.setTypeface(Typeface.DEFAULT_BOLD);  
  74.     }  
  75.   
  76.     @Override  
  77.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  78.         // 設置測量後的尺寸  
  79.         setMeasuredDimension(getMeasureSize(widthMeasureSpec, Ratio.WIDTH), getMeasureSize(heightMeasureSpec, Ratio.HEIGHT));  
  80.     }  
  81.   
  82.     /** 
  83.      * 獲取測量後的尺寸 
  84.      *  
  85.      * @param measureSpec 
  86.      *            測量規格 
  87.      * @param ratio 
  88.      *            寬高標識 
  89.      * @return 寬或高的測量值 
  90.      */  
  91.     private int getMeasureSize(int measureSpec, Ratio ratio) {  
  92.         // 聲明臨時變量保存測量值  
  93.         int result = 0;  
  94.   
  95.         /* 
  96.          * 獲取mode和size 
  97.          */  
  98.         int mode = MeasureSpec.getMode(measureSpec);  
  99.         int size = MeasureSpec.getSize(measureSpec);  
  100.   
  101.         /* 
  102.          * 判斷mode的具體值 
  103.          */  
  104.         switch (mode) {  
  105.         case MeasureSpec.EXACTLY:// EXACTLY時直接賦值  
  106.             result = size;  
  107.             break;  
  108.         default:// 默認情況下將UNSPECIFIED和AT_MOST一併處理  
  109.             if (ratio == Ratio.WIDTH) {  
  110.                 float textWidth = mPaint.measureText(mStr);  
  111.                 result = ((int) (textWidth >= mBitmap.getWidth() ? textWidth : mBitmap.getWidth())) + getPaddingLeft() + getPaddingRight();  
  112.             } else if (ratio == Ratio.HEIGHT) {  
  113.                 result = ((int) ((mPaint.descent() - mPaint.ascent()) * 2 + mBitmap.getHeight())) + getPaddingTop() + getPaddingBottom();  
  114.             }  
  115.   
  116.             /* 
  117.              * AT_MOST時判斷size和result的大小取小值 
  118.              */  
  119.             if (mode == MeasureSpec.AT_MOST) {  
  120.                 result = Math.min(result, size);  
  121.             }  
  122.             break;  
  123.         }  
  124.         return result;  
  125.     }  
  126.   
  127.     @Override  
  128.     protected void onDraw(Canvas canvas) {  
  129.         /* 
  130.          * 繪製 
  131.          * 參數就不做單獨處理了因爲只會Draw一次不會頻繁調用 
  132.          */  
  133.         canvas.drawBitmap(mBitmap, getWidth() / 2 - mBitmap.getWidth() / 2, getHeight() / 2 - mBitmap.getHeight() / 2null);  
  134.         canvas.drawText(mStr, getWidth() / 2, mBitmap.getHeight() + getHeight() / 2 - mBitmap.getHeight() / 2 - mPaint.ascent(), mPaint);  
  135.     }  
  136. }  
在xml文件中對其引用並加入一些系統自帶的控件:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="match_parent"  
  3.     android:layout_height="match_parent"  
  4.     android:background="#FFFFFFFF"  
  5.     android:orientation="vertical" >  
  6.   
  7.     <com.aigestudio.customviewdemo.views.IconView  
  8.         android:id="@+id/main_pv"  
  9.         android:layout_width="wrap_content"  
  10.         android:layout_height="wrap_content"  
  11.         android:padding="50dp" />  
  12.   
  13.     <Button  
  14.         android:layout_width="wrap_content"  
  15.         android:layout_height="wrap_content"  
  16.         android:text="AigeStudio" />  
  17.   
  18.     <TextView  
  19.         android:layout_width="wrap_content"  
  20.         android:layout_height="wrap_content"  
  21.         android:text="AigeStudio" />  
  22.   
  23. </LinearLayout>  
效果如下:


好了就先這樣吧,上面我們曾說過View的測量大小是由View和其父容器共同決定的,但是上述源碼的分析中我們其實並沒有體現,因爲它們都在ViewGroup中,這裏我們就要涉及ViewGroup中與測量相關的另外幾個方法:measureChildren、measureChild和measureChildWithMargins還有getChildMeasureSpec,見名知意這幾個方法都跟ViewGroup測量子元素有關,其中measureChildWithMargins和measureChildren類似只是加入了對Margins外邊距的處理,ViewGroup提供對子元素測量的方法從measureChildren開始:

  1. protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {  
  2.     final int size = mChildrenCount;  
  3.     final View[] children = mChildren;  
  4.     for (int i = 0; i < size; ++i) {  
  5.         final View child = children[i];  
  6.         if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {  
  7.             measureChild(child, widthMeasureSpec, heightMeasureSpec);  
  8.         }  
  9.     }  
  10. }  
measureChildren的邏輯很簡單,通過父容器傳入的widthMeasureSpec和heightMeasureSpec遍歷子元素並調用measureChild方法去測量每一個子元素的寬高:

  1. protected void measureChild(View child, int parentWidthMeasureSpec,  
  2.         int parentHeightMeasureSpec) {  
  3.     // 獲取子元素的佈局參數  
  4.     final LayoutParams lp = child.getLayoutParams();  
  5.   
  6.     /* 
  7.      * 將父容器的測量規格已經上下和左右的邊距還有子元素本身的佈局參數傳入getChildMeasureSpec方法計算最終測量規格 
  8.      */  
  9.     final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,  
  10.             mPaddingLeft + mPaddingRight, lp.width);  
  11.     final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,  
  12.             mPaddingTop + mPaddingBottom, lp.height);  
  13.   
  14.     // 調用子元素的measure傳入計算好的測量規格  
  15.     child.measure(childWidthMeasureSpec, childHeightMeasureSpec);  
  16. }  
這裏我們主要就是看看getChildMeasureSpec方法是如何確定最終測量規格的:

  1. public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  
  2.     // 獲取父容器的測量模式和尺寸大小  
  3.     int specMode = MeasureSpec.getMode(spec);  
  4.     int specSize = MeasureSpec.getSize(spec);  
  5.   
  6.     // 這個尺寸應該減去內邊距的值  
  7.     int size = Math.max(0, specSize - padding);  
  8.   
  9.     // 聲明臨時變量存值  
  10.     int resultSize = 0;  
  11.     int resultMode = 0;  
  12.   
  13.     /* 
  14.      * 根據模式判斷 
  15.      */  
  16.     switch (specMode) {  
  17.     case MeasureSpec.EXACTLY: // 父容器尺寸大小是一個確定的值  
  18.         /* 
  19.          * 根據子元素的佈局參數判斷 
  20.          */  
  21.         if (childDimension >= 0) { //如果childDimension是一個具體的值  
  22.             // 那麼就將該值作爲結果  
  23.             resultSize = childDimension;  
  24.   
  25.             // 而這個值也是被確定的  
  26.             resultMode = MeasureSpec.EXACTLY;  
  27.         } else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的佈局參數爲MATCH_PARENT  
  28.             // 那麼就將父容器的大小作爲結果  
  29.             resultSize = size;  
  30.   
  31.             // 因爲父容器的大小是被確定的所以子元素大小也是可以被確定的  
  32.             resultMode = MeasureSpec.EXACTLY;  
  33.         } else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的佈局參數爲WRAP_CONTENT  
  34.             // 那麼就將父容器的大小作爲結果  
  35.             resultSize = size;  
  36.   
  37.             // 但是子元素的大小包裹了其內容後不能超過父容器  
  38.             resultMode = MeasureSpec.AT_MOST;  
  39.         }  
  40.         break;  
  41.   
  42.     case MeasureSpec.AT_MOST: // 父容器尺寸大小擁有一個限制值  
  43.         /* 
  44.          * 根據子元素的佈局參數判斷 
  45.          */  
  46.         if (childDimension >= 0) { //如果childDimension是一個具體的值  
  47.             // 那麼就將該值作爲結果  
  48.             resultSize = childDimension;  
  49.   
  50.             // 而這個值也是被確定的  
  51.             resultMode = MeasureSpec.EXACTLY;  
  52.         } else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的佈局參數爲MATCH_PARENT  
  53.             // 那麼就將父容器的大小作爲結果  
  54.             resultSize = size;  
  55.   
  56.             // 因爲父容器的大小是受到限制值的限制所以子元素的大小也應該受到父容器的限制  
  57.             resultMode = MeasureSpec.AT_MOST;  
  58.         } else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的佈局參數爲WRAP_CONTENT  
  59.             // 那麼就將父容器的大小作爲結果  
  60.             resultSize = size;  
  61.   
  62.             // 但是子元素的大小包裹了其內容後不能超過父容器  
  63.             resultMode = MeasureSpec.AT_MOST;  
  64.         }  
  65.         break;  
  66.   
  67.     case MeasureSpec.UNSPECIFIED: // 父容器尺寸大小未受限制  
  68.         /* 
  69.          * 根據子元素的佈局參數判斷 
  70.          */  
  71.         if (childDimension >= 0) { //如果childDimension是一個具體的值  
  72.             // 那麼就將該值作爲結果  
  73.             resultSize = childDimension;  
  74.   
  75.             // 而這個值也是被確定的  
  76.             resultMode = MeasureSpec.EXACTLY;  
  77.         } else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的佈局參數爲MATCH_PARENT  
  78.             // 因爲父容器的大小不受限制而對子元素來說也可以是任意大小所以不指定也不限制子元素的大小  
  79.             resultSize = 0;  
  80.             resultMode = MeasureSpec.UNSPECIFIED;  
  81.         } else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的佈局參數爲WRAP_CONTENT  
  82.             // 因爲父容器的大小不受限制而對子元素來說也可以是任意大小所以不指定也不限制子元素的大小  
  83.             resultSize = 0;  
  84.             resultMode = MeasureSpec.UNSPECIFIED;  
  85.         }  
  86.         break;  
  87.     }  
  88.   
  89.     // 返回封裝後的測量規格  
  90.     return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  
  91. }  
至此我們可以看到一個View的大小由其父容器的測量規格MeasureSpec和View本身的佈局參數LayoutParams共同決定,但是即便如此,最終封裝的測量規格也是一個期望值,究竟有多大還是我們調用setMeasuredDimension方法設置的。上面的代碼中有些朋友看了可能會有疑問爲什麼childDimension >= 0就表示一個確切值呢?原因很簡單,因爲在LayoutParams中MATCH_PARENT和WRAP_CONTENT均爲負數、哈哈!!正是基於這點,Android巧妙地將實際值和相對的佈局參數分離開來。那麼我們該如何對ViewGroup進行測量呢?這裏爲了說明問題,我們自定義一個ViewGroup:

  1. /** 
  2.  *  
  3.  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  4.  * @since 2015/1/15 
  5.  *  
  6.  */  
  7. public class CustomLayout extends ViewGroup {  
  8.   
  9.     public CustomLayout(Context context, AttributeSet attrs) {  
  10.         super(context, attrs);  
  11.     }  
  12.   
  13.     @Override  
  14.     protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  15.   
  16.     }  
  17.   
  18. }  
ViewGroup中的onLayout方法是一個抽象方法,這意味着你在繼承時必須實現,onLayout的目的是爲了確定子元素在父容器中的位置,那麼這個步驟理應該由父容器來決定而不是子元素,因此,我們可以猜到View中的onLayout方法應該是一個空實現:

  1. public class View implements Drawable.Callback, KeyEvent.Callback,  
  2.         AccessibilityEventSource {  
  3.     // 省去無數代碼………………  
  4.   
  5.     /** 
  6.      * Called from layout when this view should 
  7.      * assign a size and position to each of its children. 
  8.      * 
  9.      * Derived classes with children should override 
  10.      * this method and call layout on each of 
  11.      * their children. 
  12.      * @param changed This is a new size or position for this view 
  13.      * @param left Left position, relative to parent 
  14.      * @param top Top position, relative to parent 
  15.      * @param right Right position, relative to parent 
  16.      * @param bottom Bottom position, relative to parent 
  17.      */  
  18.     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {  
  19.     }  
  20.   
  21.     // 省去無數代碼………………  
  22. }  
與View不同的是,ViewGroup表示一個容器,其內可以包含多個元素,既可以是一個佈局也可以是一個普通的控件,那麼在對ViewGroup測量時我們也應該對這些子元素進行測量:

  1. /** 
  2.  *  
  3.  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  4.  * @since 2015/1/15 
  5.  *  
  6.  */  
  7. public class CustomLayout extends ViewGroup {  
  8.   
  9.     public CustomLayout(Context context, AttributeSet attrs) {  
  10.         super(context, attrs);  
  11.     }  
  12.   
  13.     @Override  
  14.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  15.         super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
  16.   
  17.         /* 
  18.          * 如果有子元素 
  19.          */  
  20.         if (getChildCount() > 0) {  
  21.             // 那麼對子元素進行測量  
  22.             measureChildren(widthMeasureSpec, heightMeasureSpec);  
  23.         }  
  24.     }  
  25.   
  26.     @Override  
  27.     protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  28.   
  29.     }  
  30.   
  31. }  
然後我們在xml佈局文件中替換原來的LinearLayout使用我們自定義的佈局:

  1. <com.aigestudio.customviewdemo.views.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="match_parent"  
  3.     android:layout_height="match_parent"  
  4.     android:background="#FFFFFFFF"  
  5.     android:orientation="vertical" >  
  6.   
  7.     <com.aigestudio.customviewdemo.views.IconView  
  8.         android:id="@+id/main_pv"  
  9.         android:layout_width="wrap_content"  
  10.         android:layout_height="wrap_content"  
  11.         android:padding="50dp" />  
  12.   
  13.     <Button  
  14.         android:layout_width="wrap_content"  
  15.         android:layout_height="wrap_content"  
  16.         android:text="AigeStudio" />  
  17.   
  18.     <TextView  
  19.         android:layout_width="wrap_content"  
  20.         android:layout_height="wrap_content"  
  21.         android:text="AigeStudio" />  
  22.   
  23. </com.aigestudio.customviewdemo.views.CustomLayout>  
運行後你會發現沒有任何東西顯示,爲什麼呢?如上所說我們需要父容器告訴子元素它的出現位置,而這個過程由onLayout方法去實現,但是此時我們的onLayout方法什麼都沒有,子元素自然也不知道自己該往哪擱,自然就什麼都沒有咯……知道了原因我們就來實現onLayout的邏輯:

  1. @Override  
  2. protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  3.   
  4.     /* 
  5.      * 如果有子元素 
  6.      */  
  7.     if (getChildCount() > 0) {  
  8.         // 那麼遍歷子元素並對其進行定位佈局  
  9.         for (int i = 0; i < getChildCount(); i++) {  
  10.             View child = getChildAt(i);  
  11.             child.layout(00, getMeasuredWidth(), getMeasuredHeight());  
  12.         }  
  13.     }  
  14. }  
邏輯很簡單,如果有子元素那麼我們遍歷這些子元素並調用其layout方法告訴它們自己該在的位置,這裏我們就直接讓所有的子元素都從父容器的[0, 0]點開始到[getMeasuredWidth(), getMeasuredHeight()]父容器的測量寬高結束,這麼一來,所有的子元素應該都是填充了父容器的對吧:


看到屏幕上的巨大Button我不禁吸了一口屁!這樣的佈局太蛋疼,全被Button一個玩完了還搞毛,可不可以像LinearLayout那樣挨個顯示呢?答案是肯定的!我們來修改下onLayout的邏輯:

  1. @Override  
  2. protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  3.   
  4.     /* 
  5.      * 如果有子元素 
  6.      */  
  7.     if (getChildCount() > 0) {  
  8.         // 聲明一個臨時變量存儲高度倍增值  
  9.         int mutilHeight = 0;  
  10.   
  11.         // 那麼遍歷子元素並對其進行定位佈局  
  12.         for (int i = 0; i < getChildCount(); i++) {  
  13.             // 獲取一個子元素  
  14.             View child = getChildAt(i);  
  15.   
  16.             // 通知子元素進行佈局  
  17.             child.layout(0, mutilHeight, child.getMeasuredWidth(), child.getMeasuredHeight() + mutilHeight);  
  18.   
  19.             // 改變高度倍增值  
  20.             mutilHeight += child.getMeasuredHeight();  
  21.         }  
  22.     }  
  23. }  
可以看到我們通過一個mutilHeight來存儲高度倍增值,每一次子元素佈局完後將當前mutilHeight與當前子元素的高度相加並在下一個子元素佈局時在高度上加上mutilHeight,效果如下:


是不是和上面LinearLayout效果有點一樣了?當然LinearLayout的佈局邏輯遠比我們的複雜得多,我們呢也只是對其進行一個簡單的模擬而已。大家注意到ViewGroup的onLayout方法的簽名列表中有五個參數,其中boolean changed表示是否與上一次位置不同,其具體值在View的layout方法中通過setFrame等方法確定:

  1. public void layout(int l, int t, int r, int b) {  
  2.     // 省略一些代碼……  
  3.   
  4.     boolean changed = isLayoutModeOptical(mParent) ?  
  5.             setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);  
  6.   
  7.     // 省略大量代碼……  
  8. }  
而剩下的四個參數則表示當前View與父容器的相對距離,如下圖:


好了,說到這裏想必大家對ViewGroup的測量也有一定的瞭解了,但是這必定不是測量過程全部,如我上面所說,測量的具體過程因控件而異,上面我們曾因爲給我們的自定義View加了內邊距後修改了繪製的邏輯,因爲我們需要在繪製時考慮內邊距的影響,而我們的自定義ViewGroup呢?是不是也一樣呢?這裏我給其加入60dp的內邊距:

  1. <com.aigestudio.customviewdemo.views.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="match_parent"  
  3.     android:layout_height="match_parent"  
  4.     android:padding="60dp"  
  5.     android:background="#FFFFFFFF"  
  6.     android:orientation="vertical" >  
  7.   
  8.     <com.aigestudio.customviewdemo.views.IconView  
  9.         android:id="@+id/main_pv"  
  10.         android:layout_width="wrap_content"  
  11.         android:layout_height="wrap_content"  
  12.         android:padding="50dp" />  
  13.   
  14.     <Button  
  15.         android:layout_width="wrap_content"  
  16.         android:layout_height="wrap_content"  
  17.         android:text="AigeStudio" />  
  18.   
  19.     <TextView  
  20.         android:layout_width="wrap_content"  
  21.         android:layout_height="wrap_content"  
  22.         android:text="AigeStudio" />  
  23.   
  24. </com.aigestudio.customviewdemo.views.CustomLayout>  
運行後效果如下:


內邊距把我們的子元素給“喫”掉了,那麼也就是說我們在對子元素進行定位時應該進一步考慮到父容器內邊距的影響對吧,OK,我們重理onLayout的邏輯:

  1. @Override  
  2. protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  3.     // 獲取父容器內邊距  
  4.     int parentPaddingLeft = getPaddingLeft();  
  5.     int parentPaddingTop = getPaddingTop();  
  6.   
  7.     /* 
  8.      * 如果有子元素 
  9.      */  
  10.     if (getChildCount() > 0) {  
  11.         // 聲明一個臨時變量存儲高度倍增值  
  12.         int mutilHeight = 0;  
  13.   
  14.         // 那麼遍歷子元素並對其進行定位佈局  
  15.         for (int i = 0; i < getChildCount(); i++) {  
  16.             // 獲取一個子元素  
  17.             View child = getChildAt(i);  
  18.   
  19.             // 通知子元素進行佈局  
  20.             // 此時考慮父容器內邊距的影響  
  21.             child.layout(parentPaddingLeft, mutilHeight + parentPaddingTop, child.getMeasuredWidth() + parentPaddingLeft, child.getMeasuredHeight() + mutilHeight + parentPaddingTop);  
  22.   
  23.             // 改變高度倍增值  
  24.             mutilHeight += child.getMeasuredHeight();  
  25.         }  
  26.     }  
  27. }  
此時的效果如下:


既然內邊距如此,那麼Margins外邊距呢?我們來看看,在xml佈局文件中爲我們的CustomLayout加一個margins:

  1. <com.aigestudio.customviewdemo.views.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="match_parent"  
  3.     android:layout_height="match_parent"  
  4.     android:layout_margin="30dp"  
  5.     android:padding="20dp"  
  6.     android:background="#FF597210"  
  7.     android:orientation="vertical" >  
  8.   
  9.     <com.aigestudio.customviewdemo.views.IconView  
  10.         android:id="@+id/main_pv"  
  11.         android:layout_width="wrap_content"  
  12.         android:layout_height="wrap_content" />  
  13.   
  14.     <Button  
  15.         android:layout_width="wrap_content"  
  16.         android:layout_height="wrap_content"  
  17.         android:text="AigeStudio" />  
  18.   
  19.     <TextView  
  20.         android:layout_width="wrap_content"  
  21.         android:layout_height="wrap_content"  
  22.         android:text="AigeStudio" />  
  23.   
  24. </com.aigestudio.customviewdemo.views.CustomLayout>  
效果如下:


OK,目測沒什麼問題,可是當我們爲子元素設置外邊距時,問題就來了……不管你怎麼設都不會有任何效果,原因很簡單,我們上面也說了,Margins是由父容器來處理,而我們的CustomLayout中並沒有對其做任何的處理,那麼我們應該怎麼做呢?首先要知道Margins封裝在LayoutParams中,如果我們想實現自己對其的處理那麼我們必然也有必要實現自己佈局的LayoutParams:

  1. /** 
  2.  *  
  3.  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  4.  * @since 2015/1/15 
  5.  *  
  6.  */  
  7. public class CustomLayout extends ViewGroup {  
  8.     // 省略部分代碼…………  
  9.   
  10.     /** 
  11.      *  
  12.      * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  13.      *  
  14.      */  
  15.     public static class CustomLayoutParams extends MarginLayoutParams {  
  16.   
  17.         public CustomLayoutParams(MarginLayoutParams source) {  
  18.             super(source);  
  19.         }  
  20.   
  21.         public CustomLayoutParams(android.view.ViewGroup.LayoutParams source) {  
  22.             super(source);  
  23.         }  
  24.   
  25.         public CustomLayoutParams(Context c, AttributeSet attrs) {  
  26.             super(c, attrs);  
  27.         }  
  28.   
  29.         public CustomLayoutParams(int width, int height) {  
  30.             super(width, height);  
  31.         }  
  32.     }  
  33. }  
我們在我們的CustomLayout中生成了一個靜態內部類CustomLayoutParams,保持其默認的構造方法即可,這裏我們什麼也沒做,當然你可以定義自己的一些屬性或邏輯處理,因控件而異這裏不多說了,後面慢慢會用到。然後在我們的CustomLayout中重寫所有與LayoutParams相關的方法,返回我們自己的CustomLayoutParams:

  1. /** 
  2.  *  
  3.  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  4.  * @since 2015/1/15 
  5.  *  
  6.  */  
  7. public class CustomLayout extends ViewGroup {  
  8.     // 省略部分代碼…………  
  9.   
  10.     /** 
  11.      * 生成默認的佈局參數 
  12.      */  
  13.     @Override  
  14.     protected CustomLayoutParams generateDefaultLayoutParams() {  
  15.         return new CustomLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);  
  16.     }  
  17.   
  18.     /** 
  19.      * 生成佈局參數 
  20.      * 將佈局參數包裝成我們的 
  21.      */  
  22.     @Override  
  23.     protected android.view.ViewGroup.LayoutParams generateLayoutParams(android.view.ViewGroup.LayoutParams p) {  
  24.         return new CustomLayoutParams(p);  
  25.     }  
  26.   
  27.     /** 
  28.      * 生成佈局參數 
  29.      * 從屬性配置中生成我們的佈局參數 
  30.      */  
  31.     @Override  
  32.     public android.view.ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {  
  33.         return new CustomLayoutParams(getContext(), attrs);  
  34.     }  
  35.   
  36.     /** 
  37.      * 檢查當前佈局參數是否是我們定義的類型這在code聲明佈局參數時常常用到 
  38.      */  
  39.     @Override  
  40.     protected boolean checkLayoutParams(android.view.ViewGroup.LayoutParams p) {  
  41.         return p instanceof CustomLayoutParams;  
  42.     }  
  43.   
  44.     // 省略部分代碼…………  
  45. }  
最後更改我們的測量邏輯:

  1. @Override  
  2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  3.     // 聲明臨時變量存儲父容器的期望值  
  4.     int parentDesireWidth = 0;  
  5.     int parentDesireHeight = 0;  
  6.   
  7.     /* 
  8.      * 如果有子元素 
  9.      */  
  10.     if (getChildCount() > 0) {  
  11.         // 那麼遍歷子元素並對其進行測量  
  12.         for (int i = 0; i < getChildCount(); i++) {  
  13.   
  14.             // 獲取子元素  
  15.             View child = getChildAt(i);  
  16.   
  17.             // 獲取子元素的佈局參數  
  18.             CustomLayoutParams clp = (CustomLayoutParams) child.getLayoutParams();  
  19.   
  20.             // 測量子元素並考慮外邊距  
  21.             measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);  
  22.   
  23.             // 計算父容器的期望值  
  24.             parentDesireWidth += child.getMeasuredWidth() + clp.leftMargin + clp.rightMargin;  
  25.             parentDesireHeight += child.getMeasuredHeight() + clp.topMargin + clp.bottomMargin;  
  26.         }  
  27.   
  28.         // 考慮父容器的內邊距  
  29.         parentDesireWidth += getPaddingLeft() + getPaddingRight();  
  30.         parentDesireHeight += getPaddingTop() + getPaddingBottom();  
  31.   
  32.         // 嘗試比較建議最小值和期望值的大小並取大值  
  33.         parentDesireWidth = Math.max(parentDesireWidth, getSuggestedMinimumWidth());  
  34.         parentDesireHeight = Math.max(parentDesireHeight, getSuggestedMinimumHeight());  
  35.     }  
  36.   
  37.     // 設置最終測量值O  
  38.     setMeasuredDimension(resolveSize(parentDesireWidth, widthMeasureSpec), resolveSize(parentDesireHeight, heightMeasureSpec));  
  39. }  
  40.   
  41. @Override  
  42. protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  43.     // 獲取父容器內邊距  
  44.     int parentPaddingLeft = getPaddingLeft();  
  45.     int parentPaddingTop = getPaddingTop();  
  46.   
  47.     /* 
  48.      * 如果有子元素 
  49.      */  
  50.     if (getChildCount() > 0) {  
  51.         // 聲明一個臨時變量存儲高度倍增值  
  52.         int mutilHeight = 0;  
  53.   
  54.         // 那麼遍歷子元素並對其進行定位佈局  
  55.         for (int i = 0; i < getChildCount(); i++) {  
  56.             // 獲取一個子元素  
  57.             View child = getChildAt(i);  
  58.   
  59.             CustomLayoutParams clp = (CustomLayoutParams) child.getLayoutParams();  
  60.   
  61.             // 通知子元素進行佈局  
  62.             // 此時考慮父容器內邊距和子元素外邊距的影響  
  63.             child.layout(parentPaddingLeft + clp.leftMargin, mutilHeight + parentPaddingTop + clp.topMargin, child.getMeasuredWidth() + parentPaddingLeft + clp.leftMargin, child.getMeasuredHeight() + mutilHeight + parentPaddingTop + clp.topMargin);  
  64.   
  65.             // 改變高度倍增值  
  66.             mutilHeight += child.getMeasuredHeight() + clp.topMargin + clp.bottomMargin;  
  67.         }  
  68.     }  
  69. }  
佈局文件如下:

  1. <com.aigestudio.customviewdemo.views.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="match_parent"  
  3.     android:layout_height="match_parent"  
  4.     android:background="#FF597210"  
  5.     android:orientation="vertical" >  
  6.   
  7.     <com.aigestudio.customviewdemo.views.IconView  
  8.         android:id="@+id/main_pv"  
  9.         android:layout_width="wrap_content"  
  10.         android:layout_height="wrap_content"  
  11.         android:layout_marginBottom="10dp"  
  12.         android:layout_marginLeft="20dp"  
  13.         android:layout_marginRight="30dp"  
  14.         android:layout_marginTop="5dp" />  
  15.   
  16.     <Button  
  17.         android:layout_width="wrap_content"  
  18.         android:layout_height="wrap_content"  
  19.         android:layout_marginBottom="16dp"  
  20.         android:layout_marginLeft="2dp"  
  21.         android:layout_marginRight="8dp"  
  22.         android:layout_marginTop="4dp"  
  23.         android:text="AigeStudio" />  
  24.   
  25.     <TextView  
  26.         android:layout_width="wrap_content"  
  27.         android:layout_height="wrap_content"  
  28.         android:layout_marginBottom="28dp"  
  29.         android:layout_marginLeft="7dp"  
  30.         android:layout_marginRight="19dp"  
  31.         android:layout_marginTop="14dp"  
  32.         android:background="#FF166792"  
  33.         android:text="AigeStudio" />  
  34.   
  35. </com.aigestudio.customviewdemo.views.CustomLayout>  
運行效果如下:


~~~~~~~~好了好了、不講了,View的基本測量過程大致就是這樣,如我所說測量並不是定式的過程,總會因控件而已,我們在自定義控件時要準確地測量,一定要準確,測量的結果會直接影響後面的佈局定位、繪製甚至交互,所以馬虎不得,你也可以看到Android給我們提供的LinearLayout、FrameLayout等佈局都有極其嚴謹的測量邏輯,爲的就是確保測量結果的準確。
本篇幅雖長,但是我們其實就講了三點:

  1. 一個界面窗口的元素構成
  2. framework對View測量的控制處理
  3. View和ViewGroup的簡單測量

好了、不說了、實在說不動了………………到此爲止&¥……#¥……%#¥%#¥%#%¥哦!對了,文章開頭我給各位設了一個問題,不知道大家發現沒有,本來說這節順帶講了,看着篇幅太長下節再說吧……

源碼下載:傳送門

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章