自定義控件其實很簡單2/3

尊重原創轉載請註明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵權必究!

炮兵鎮樓

又要開始雞凍人心的一刻了有木有!有木有雞凍! = = ……通過上一節的講解呢我們大致對Android測量控件有個初步的瞭解,而此後呢也有不少盆友Q小窗我問了不少問題,不過其實這些問題大多都不是問題,至於到底是不是問題呢,還要等我研究下究竟可不可以把這些問題歸爲問題……稍等、我吃個藥先。大多數盆友的反應是在對控件測量的具體方法還不是很瞭解,不過不要着急,上一節的內容就當飯前甜點,接下來我們會用一個例子來說明這一切不是問題的問題,這個例子中的控件呢我稱其爲SquareLayout,意爲方形佈局(注:該例子僅作演示,實際應用意義並不大),我們將置於該佈局下的所有子元素都強制變爲一個正方形~~說起簡單,但是如我上一節所說控件的設計要儘可能考慮到所有的可能性才能趨於完美~~但是沒有絕對的完美……在5/12時我曾說過不要將自己當作一個coder而要把自己看成一個designer,控件是我們設計出來的而不是敲出來的,在我們code之前就該對這個控件有一個較爲perfect的design,考慮到控件的屬性設計、行爲設計、交互設計等等,這裏呢我也對我們的SquareLayout做了一個簡單的設計:


非常簡單,如上我們所說,SquareLayout內部的子元素都會以正方形的形狀顯示,我們可以給其定義一個orientation屬性來表示子元素排列方式,如上是orientation爲橫向時的排列方式,而下面則是縱向的排列方式:


指定了排列方式後我們的子元素就會以此爲基準排列下去,但是如果子元素超出了父容器的區域怎麼辦呢?這時我們可以指定兩種處理方式:一、不管,任由子元素被父容器的邊距裁剪;二、強制被裁剪的子元素捨棄其原有佈局重新佈局。暫時我們先默認第一種吧。接着看,如果我們的子元素只能是橫着一排或豎着一排着實單調,我們可以考慮定義兩個屬性控制其最大排列個數,比如縱向排列時,我們可以指定一個max_row屬性,當排列的行數超過該值時自動換列:


當然我們也可以在子元素橫向排列時爲其指定max_column屬性,當橫向排列列數超過該值時自動換行:


仔細想想,當max_column爲1時,我們的排列方式其實就是縱向的而當max_row爲1時就是橫向的,那麼我們的orientation屬性豈不是成了擺設?會不會呢?留給各位去想。好吧、暫時就先定義這倆屬性,光這倆已經夠折騰的了,來來來創建我們的佈局:

  1. /** 
  2.  *  
  3.  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  4.  * @since 2015/1/23 
  5.  *  
  6.  */  
  7. public class SquareLayout extends ViewGroup {  
  8.     private int mMaxRow;// 最大行數  
  9.     private int mMaxColumn;// 最大列數  
  10.   
  11.     private int mOrientation;// 排列方向  
  12.   
  13.     public SquareLayout(Context context, AttributeSet attrs) {  
  14.         super(context, attrs);  
  15.     }  
  16.   
  17.     @Override  
  18.     protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  19.     }  
  20. }  
上一節我們曾說過,要讓我們父容器下子元素的margins外邊距能夠被正確計算,我們必需重寫父容器的三個相關方法並返回一個MarginLayoutParams的子類:

  1. /** 
  2.  *  
  3.  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  4.  * @since 2015/1/23 
  5.  *  
  6.  */  
  7. public class SquareLayout extends ViewGroup {  
  8.     // 省去各種蛋疼的成員變量…………  
  9.   
  10.     // 省去構造方法…………  
  11.   
  12.     // 省去onLayout方法…………  
  13.   
  14.     @Override  
  15.     protected LayoutParams generateDefaultLayoutParams() {  
  16.         return new MarginLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);  
  17.     }  
  18.   
  19.     @Override  
  20.     protected android.view.ViewGroup.LayoutParams generateLayoutParams(android.view.ViewGroup.LayoutParams p) {  
  21.         return new MarginLayoutParams(p);  
  22.     }  
  23.   
  24.     @Override  
  25.     public android.view.ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {  
  26.         return new MarginLayoutParams(getContext(), attrs);  
  27.     }  
  28. }  
這裏我直接返回一個MarginLayoutParams的實例對象,因爲我不需要在LayoutParams處理自己的邏輯,單純地計算margins就沒必要去實現一個自定義的MarginLayoutParams子類了,除此之外,你還可以重寫checkLayoutParams方法去驗證當前所使用的LayoutParams對象是否MarginLayoutParams的一個實例:

  1. /** 
  2.  *  
  3.  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  4.  * @since 2015/1/23 
  5.  *  
  6.  */  
  7. public class SquareLayout extends ViewGroup {  
  8.     // 省去各種蛋疼的成員變量…………  
  9.   
  10.     // 省去構造方法…………  
  11.   
  12.     // 省去onLayout方法…………  
  13.   
  14.     // 省去三個屌毛方法……  
  15.   
  16.     @Override  
  17.     protected boolean checkLayoutParams(android.view.ViewGroup.LayoutParams p) {  
  18.         return p instanceof MarginLayoutParams;  
  19.     }  
  20. }  
然後呢我們就要開始對控件進行測量了,首先重寫onMeasure方法是肯定的,那麼我們就先在onMeasure中先把測量的邏輯處理了先,不過我們還是按部就班一步一步來,先把排列方式搞定:

  1. /** 
  2.  *  
  3.  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  4.  * @since 2015/1/23 
  5.  *  
  6.  */  
  7. public class SquareLayout extends ViewGroup {  
  8.     private static final int ORIENTATION_HORIZONTAL = 0, ORIENTATION_VERTICAL = 1;// 排列方向的常量標識值  
  9.     private static final int DEFAULT_MAX_ROW = Integer.MAX_VALUE, DEFAULT_MAX_COLUMN = Integer.MAX_VALUE;// 最大行列默認值  
  10.   
  11.     private int mMaxRow = DEFAULT_MAX_ROW;// 最大行數  
  12.     private int mMaxColumn = DEFAULT_MAX_COLUMN;// 最大列數  
  13.   
  14.     private int mOrientation = ORIENTATION_HORIZONTAL;// 排列方向默認橫向  
  15.   
  16.     // 省去構造方法…………  
  17.   
  18.     @SuppressLint("NewApi")  
  19.     @Override  
  20.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  21.         /* 
  22.          * 聲明臨時變量存儲父容器的期望值 
  23.          * 該值應該等於父容器的內邊距加上所有子元素的測量寬高和外邊距 
  24.          */  
  25.         int parentDesireWidth = 0;  
  26.         int parentDesireHeight = 0;  
  27.   
  28.         // 聲明臨時變量存儲子元素的測量狀態  
  29.         int childMeasureState = 0;  
  30.   
  31.         /* 
  32.          * 如果父容器內有子元素 
  33.          */  
  34.         if (getChildCount() > 0) {  
  35.             /* 
  36.              * 那麼就遍歷子元素 
  37.              */  
  38.             for (int i = 0; i < getChildCount(); i++) {  
  39.                 // 獲取對應遍歷下標的子元素  
  40.                 View child = getChildAt(i);  
  41.   
  42.                 /* 
  43.                  * 如果該子元素沒有以“不佔用空間”的方式隱藏則表示其需要被測量計算 
  44.                  */  
  45.                 if (child.getVisibility() != View.GONE) {  
  46.   
  47.                     // 測量子元素並考量其外邊距  
  48.                     measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);  
  49.   
  50.                     // 比較子元素測量寬高並比較取其較大值  
  51.                     int childMeasureSize = Math.max(child.getMeasuredWidth(), child.getMeasuredHeight());  
  52.   
  53.                     // 重新封裝子元素測量規格  
  54.                     int childMeasureSpec = MeasureSpec.makeMeasureSpec(childMeasureSize, MeasureSpec.EXACTLY);  
  55.   
  56.                     // 重新測量子元素  
  57.                     child.measure(childMeasureSpec, childMeasureSpec);  
  58.   
  59.                     // 獲取子元素佈局參數  
  60.                     MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams();  
  61.   
  62.                     /* 
  63.                      * 考量外邊距計算子元素實際寬高 
  64.                      */  
  65.                     int childActualWidth = child.getMeasuredWidth() + mlp.leftMargin + mlp.rightMargin;  
  66.                     int childActualHeight = child.getMeasuredHeight() + mlp.topMargin + mlp.bottomMargin;  
  67.   
  68.                     /* 
  69.                      * 如果爲橫向排列 
  70.                      */  
  71.                     if (mOrientation == ORIENTATION_HORIZONTAL) {  
  72.                         // 累加子元素的實際寬度  
  73.                         parentDesireWidth += childActualWidth;  
  74.   
  75.                         // 獲取子元素中高度最大值  
  76.                         parentDesireHeight = Math.max(parentDesireHeight, childActualHeight);  
  77.                     }  
  78.   
  79.                     /* 
  80.                      * 如果爲豎向排列 
  81.                      */  
  82.                     else if (mOrientation == ORIENTATION_VERTICAL) {  
  83.                         // 累加子元素的實際高度  
  84.                         parentDesireHeight += childActualHeight;  
  85.   
  86.                         // 獲取子元素中寬度最大值  
  87.                         parentDesireWidth = Math.max(parentDesireWidth, childActualWidth);  
  88.                     }  
  89.   
  90.                     // 合併子元素的測量狀態  
  91.                     childMeasureState = combineMeasuredStates(childMeasureState, child.getMeasuredState());  
  92.                 }  
  93.             }  
  94.   
  95.             /* 
  96.              * 考量父容器內邊距將其累加到期望值 
  97.              */  
  98.             parentDesireWidth += getPaddingLeft() + getPaddingRight();  
  99.             parentDesireHeight += getPaddingTop() + getPaddingBottom();  
  100.   
  101.             /* 
  102.              * 嘗試比較父容器期望值與Android建議的最小值大小並取較大值 
  103.              */  
  104.             parentDesireWidth = Math.max(parentDesireWidth, getSuggestedMinimumWidth());  
  105.             parentDesireHeight = Math.max(parentDesireHeight, getSuggestedMinimumHeight());  
  106.         }  
  107.   
  108.         // 確定父容器的測量寬高  
  109.         setMeasuredDimension(resolveSizeAndState(parentDesireWidth, widthMeasureSpec, childMeasureState),  
  110.                 resolveSizeAndState(parentDesireHeight, heightMeasureSpec, childMeasureState << MEASURED_HEIGHT_STATE_SHIFT));  
  111.     }  
  112.   
  113.     // 省去onLayout方法…………  
  114.   
  115.     // 省去四個屌毛方法……  
  116. }  
上面代碼註釋很清楚,具體的我就不扯了,小窗我的童鞋有一部分問過我上一節中我在確定測量尺寸時候使用的resolveSize方法作用(以下代碼源自上一節的CustomLayout):

  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.     // 省去N多代碼  
  9.   
  10.     @Override  
  11.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  12.         // 省省省………………  
  13.   
  14.         // 設置最終測量值  
  15.         setMeasuredDimension(resolveSize(parentDesireWidth, widthMeasureSpec), resolveSize(parentDesireHeight, heightMeasureSpec));  
  16.     }  
  17.   
  18.     // 省去N+1多代碼  
  19. }  
那麼這個resolveSize方法其實是View提供給我們解算尺寸大小的一個工具方法,其具體實現在API 11後交由另一個方法resolveSizeAndState也就是我們這一節例子所用到的去處理:

  1. public static int resolveSize(int size, int measureSpec) {  
  2.     return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;  
  3. }  
而這個resolveSizeAndState方法具體實現其實跟我們上一節開頭解算Bitmap尺寸的邏輯類似:

  1. public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {  
  2.     int result = size;  
  3.     int specMode = MeasureSpec.getMode(measureSpec);  
  4.     int specSize =  MeasureSpec.getSize(measureSpec);  
  5.     switch (specMode) {  
  6.     case MeasureSpec.UNSPECIFIED:  
  7.         result = size;  
  8.         break;  
  9.     case MeasureSpec.AT_MOST:  
  10.         if (specSize < size) {  
  11.             result = specSize | MEASURED_STATE_TOO_SMALL;  
  12.         } else {  
  13.             result = size;  
  14.         }  
  15.         break;  
  16.     case MeasureSpec.EXACTLY:  
  17.         result = specSize;  
  18.         break;  
  19.     }  
  20.     return result | (childMeasuredState&MEASURED_STATE_MASK);  
  21. }  
是不是很類似呢?如果沒看過我上一節的內容,可以回頭去閱讀一下自定義控件其實很簡單7/12,與我們不同的是這個方法多了一個childMeasuredState參數,而上面例子我們在具體測量時也引入了一個childMeasureState臨時變量的計算,那麼這個值的作用是什麼呢?有何意義呢?說到這裏不得不提API 11後引入的幾個標識位:


這些標識位上面的代碼中我們都有用到,而官方文檔對其作用的說明也是模棱兩可,源碼裏的運用也不明朗,比如說我們看其它幾個與其相關的幾個方法:

  1. public final int getMeasuredWidth() {  
  2.     return mMeasuredWidth & MEASURED_SIZE_MASK;  
  3. }  
  4. public final int getMeasuredHeight() {  
  5.     return mMeasuredHeight & MEASURED_SIZE_MASK;  
  6. }  
  7. public final int getMeasuredState() {  
  8.     return (mMeasuredWidth&MEASURED_STATE_MASK)  
  9.             | ((mMeasuredHeight>>MEASURED_HEIGHT_STATE_SHIFT)  
  10.                     & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));  
  11. }  
這裏大家注意getMeasuredWidth和getMeasuredHeight這兩個我們用來獲取控件測量寬高的方法,在其之中對其做了一個按位與的運算,然後才把這個測量值返回給我們,也就是說這個mMeasuredWidth和mMeasuredHeight裏面應該還封裝了些什麼對吧,那麼我們來看其賦值,其賦值是在setMeasuredDimension方法下進行的:

  1. protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {  
  2.     // 省去無關代碼……  
  3.     mMeasuredWidth = measuredWidth;  
  4.     mMeasuredHeight = measuredHeight;  
  5.   
  6.     // 省去一行代碼……  
  7. }  
也就是說當我們給控件設置最終測量尺寸時這個值就直接被賦予給了mMeasuredWidth和mMeasuredHeight這兩個成員變量……看到這裏很多朋友蛋疼了,那有啥區別和意義呢?我們嘗試來翻翻系統自帶控件關於它的處理,其中TextView沒有涉及到這個參數的應用,而ImageView裏則有:

  1. @Override  
  2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  3.     // 省去海量代碼…………  
  4.   
  5.     widthSize = resolveSizeAndState(w, widthMeasureSpec, 0);  
  6.     heightSize = resolveSizeAndState(h, heightMeasureSpec, 0);  
  7.   
  8.     // 省去一點代碼…………  
  9. }  
在ImageView的onMeasure方法中使用resolveSizeAndState再次對計算得出的寬高進行解算,而這裏的第三個參數直接傳的0,也就是不作任何處理~~~~~~~~蛋疼!真蛋疼,以前寡人也曾糾結過一段時間,後來在stackoverflow在找到兩個比較靠譜的答案:


大概意思就是當控件的測量尺寸比其父容器大時將會設置MEASURED_STATE_TOO_SMALL這個二進制值,而另一個stackoverflow的回答就更官方了:


注意右下角的用戶名和頭像,你就知道爲什麼這個回答有權威性了,鄙人是他腦殘粉。來我們好好翻一下Romain這段話的意思:“childMeasuredState這個值呢由View.getMeasuredState()這個方法返回,一個佈局(或者按我的說法父容器)通過View.combineMeasuredStates()這個方法來統計其子元素的測量狀態。在大多數情況下你可以簡單地只傳遞0作爲參數值,而子元素狀態值目前的作用只是用來告訴父容器在對其進行測量得出的測量值比它自身想要的尺寸要小,如果有必要的話一個對話框將會根據這個原因來重新校正它的尺寸。”So、可以看出,測量狀態對谷歌官方而言也還算個測試性的功能,具體鄙人也沒有找到很好的例證,如果大家誰找到了其具體的使用方法可以分享一下,這裏我們還是就按照谷歌官方的建議依葫蘆畫瓢。好了這個問題就先到這裏爲止,我們繼續看,在測量子元素尺寸時我分了兩種情況:

  1. /* 
  2.  * 如果爲橫向排列 
  3.  */  
  4. if (mOrientation == ORIENTATION_HORIZONTAL) {  
  5.     // 累加子元素的實際寬度  
  6.     parentDesireWidth += childActualWidth;  
  7.   
  8.     // 獲取子元素中高度最大值  
  9.     parentDesireHeight = Math.max(parentDesireHeight, childActualHeight);  
  10. }  
  11.   
  12. /* 
  13.  * 如果爲豎向排列 
  14.  */  
  15. else if (mOrientation == ORIENTATION_VERTICAL) {  
  16.     // 累加子元素的實際高度  
  17.     parentDesireHeight += childActualHeight;  
  18.   
  19.     // 獲取子元素中寬度最大值  
  20.     parentDesireWidth = Math.max(parentDesireWidth, childActualWidth);  
  21. }  
如果爲橫/豎向排列,那麼我們應該統計各個子元素的寬/高,而高/寬呢則不需要統計,我們取其最高/最寬的那個子元素的值即可,注意在上一節的處理中我們並沒有這樣去做哦!不知道大家發現沒~~~好了,onMeasure方法的邏輯就是這樣,如果你覺得好長,那麼恭喜你,這只是我們的第一步,爾後還有幾個參數的處理~~~~~這時候你如果運行會發現什麼都沒有,因爲onMeasure方法的作用僅僅是測量的一步,按照官方的說法,Android對Viewgroup的測量由兩方面構成:一是對父容器和子元素大小尺寸的測量主要體現在onMeasure方法,二是對父容器的子元素在其區域內的定位主要體現在onLayout方法。也就是會說,即便我們完成了測量但沒告訴兒子們該出現在哪的話也不會有任何顯示效果,OK,現在我們來看看onLayout方法的邏輯處理:

  1. /** 
  2.  *  
  3.  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  4.  * @since 2015/1/23 
  5.  *  
  6.  */  
  7. public class SquareLayout extends ViewGroup {  
  8.     private static final int ORIENTATION_HORIZONTAL = 0, ORIENTATION_VERTICAL = 1;// 排列方向的常量標識值  
  9.     private static final int DEFAULT_MAX_ROW = Integer.MAX_VALUE, DEFAULT_MAX_COLUMN = Integer.MAX_VALUE;// 最大行列默認值  
  10.   
  11.     private int mMaxRow = DEFAULT_MAX_ROW;// 最大行數  
  12.     private int mMaxColumn = DEFAULT_MAX_COLUMN;// 最大列數  
  13.   
  14.     private int mOrientation = ORIENTATION_HORIZONTAL;// 排列方向默認橫向  
  15.   
  16.     // 省去構造方法…………  
  17.   
  18.     // 省去上面已經給過的onMeasure方法…………  
  19.   
  20.     @Override  
  21.     protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  22.         /* 
  23.          * 如果父容器下有子元素 
  24.          */  
  25.         if (getChildCount() > 0) {  
  26.             // 聲明臨時變量存儲寬高倍增值  
  27.             int multi = 0;  
  28.   
  29.             /* 
  30.              * 遍歷子元素 
  31.              */  
  32.             for (int i = 0; i < getChildCount(); i++) {  
  33.                 // 獲取對應遍歷下標的子元素  
  34.                 View child = getChildAt(i);  
  35.   
  36.                 /* 
  37.                  * 如果該子元素沒有以“不佔用空間”的方式隱藏則表示其需要被測量計算 
  38.                  */  
  39.                 if (child.getVisibility() != View.GONE) {  
  40.                     // 獲取子元素佈局參數  
  41.                     MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams();  
  42.   
  43.                     // 獲取控件尺寸  
  44.                     int childActualSize = child.getMeasuredWidth();// child.getMeasuredHeight()  
  45.   
  46.                     /* 
  47.                      * 如果爲橫向排列 
  48.                      */  
  49.                     if (mOrientation == ORIENTATION_HORIZONTAL) {  
  50.                         // 確定子元素左上、右下座標  
  51.                         child.layout(getPaddingLeft() + mlp.leftMargin + multi, getPaddingTop() + mlp.topMargin, childActualSize + getPaddingLeft()  
  52.                                 + mlp.leftMargin + multi, childActualSize + getPaddingTop() + mlp.topMargin);  
  53.   
  54.                         // 累加倍增值  
  55.                         multi += childActualSize + mlp.leftMargin + mlp.rightMargin;  
  56.                     }  
  57.   
  58.                     /* 
  59.                      * 如果爲豎向排列 
  60.                      */  
  61.                     else if (mOrientation == ORIENTATION_VERTICAL) {  
  62.                         // 確定子元素左上、右下座標  
  63.                         child.layout(getPaddingLeft() + mlp.leftMargin, getPaddingTop() + mlp.topMargin + multi, childActualSize + getPaddingLeft()  
  64.                                 + mlp.leftMargin, childActualSize + getPaddingTop() + mlp.topMargin + multi);  
  65.   
  66.                         // 累加倍增值  
  67.                         multi += childActualSize + mlp.topMargin + mlp.bottomMargin;  
  68.                     }  
  69.                 }  
  70.             }  
  71.         }  
  72.     }  
  73.   
  74.     // 省去四個屌毛方法……  
  75. }  
比起對onMeasure方法的邏輯處理,onLayout方法相對簡單,主要是在對子元素layout的地方需要我們一點計算思維,也不是很複雜,哥相信你能懂,畢竟註釋如此清楚,來我們嘗試用一下我們的佈局:

  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:layout_gravity="center"  
  5.     android:background="#ffffff" >  
  6.   
  7.     <com.aigestudio.customviewdemo.views.SquareLayout  
  8.         android:layout_width="match_parent"  
  9.         android:layout_height="wrap_content"  
  10.         android:paddingLeft="5dp"  
  11.         android:paddingTop="12dp"  
  12.         android:layout_margin="5dp"  
  13.         android:paddingRight="7dp"  
  14.         android:paddingBottom="20dp"  
  15.         android:layout_gravity="center"  
  16.         android:background="#679135" >  
  17.   
  18.         <Button  
  19.             android:layout_width="wrap_content"  
  20.             android:layout_height="wrap_content"  
  21.             android:background="#125793"  
  22.             android:text="tomorrow"  
  23.             android:textSize="24sp"  
  24.             android:textStyle="bold"  
  25.             android:typeface="serif" />  
  26.   
  27.         <Button  
  28.             android:layout_width="50dp"  
  29.             android:layout_height="100dp"  
  30.             android:layout_marginBottom="5dp"  
  31.             android:layout_marginLeft="10dp"  
  32.             android:layout_marginRight="20dp"  
  33.             android:layout_marginTop="30dp"  
  34.             android:background="#495287"  
  35.             android:text="AigeStudio" />  
  36.   
  37.         <ImageView  
  38.             android:layout_width="wrap_content"  
  39.             android:layout_height="wrap_content"  
  40.             android:layout_marginBottom="50dp"  
  41.             android:layout_marginLeft="5dp"  
  42.             android:layout_marginRight="20dp"  
  43.             android:layout_marginTop="15dp"  
  44.             android:background="#976234"  
  45.             android:scaleType="centerCrop"  
  46.             android:src="@drawable/lovestory_little" />  
  47.   
  48.         <Button  
  49.             android:layout_width="wrap_content"  
  50.             android:layout_height="wrap_content"  
  51.             android:background="#594342"  
  52.             android:text="AigeStudio" />  
  53.   
  54.         <Button  
  55.             android:layout_width="wrap_content"  
  56.             android:layout_height="wrap_content"  
  57.             android:background="#961315"  
  58.             android:text="AigeStudio" />  
  59.     </com.aigestudio.customviewdemo.views.SquareLayout>  
  60.   
  61. </LinearLayout>  

下面是運行後顯示的效果:


將排列方式改爲縱向排列:

  1. private int mOrientation = ORIENTATION_VERTICAL;// 排列方向默認橫向  
再來瞅瞅ADT的顯示效果:


在運行看看:


看樣子目測還是很完美,不過這只是我們偉大的第一步而已!如我多次強調,控件的測量一定要儘可能地考慮到所有因素,這樣你的控件才能立於N次不倒的暴力測試中,現在開始我們的第二步,max_row和max_column屬性的計算:

  1. /** 
  2.  *  
  3.  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  4.  * @since 2015/1/23 
  5.  *  
  6.  */  
  7. public class SquareLayout extends ViewGroup {  
  8.     private static final int ORIENTATION_HORIZONTAL = 0, ORIENTATION_VERTICAL = 1;// 排列方向的常量標識值  
  9.     private static final int DEFAULT_MAX_ROW = Integer.MAX_VALUE, DEFAULT_MAX_COLUMN = Integer.MAX_VALUE;// 最大行列默認值  
  10.   
  11.     private int mMaxRow = DEFAULT_MAX_ROW;// 最大行數  
  12.     private int mMaxColumn = DEFAULT_MAX_COLUMN;// 最大列數  
  13.   
  14.     private int mOrientation = ORIENTATION_HORIZONTAL;// 排列方向默認橫向  
  15.   
  16.     public SquareLayout(Context context, AttributeSet attrs) {  
  17.         super(context, attrs);  
  18.   
  19.         // 初始化最大行列數  
  20.         mMaxRow = mMaxColumn = 2;  
  21.     }  
  22.   
  23.     // 省去onMeasure方法…………  
  24.   
  25.     // 省去onLayout方法…………  
  26.   
  27.     // 省去四個屌毛方法……  
  28. }  
首先呢在構造方法內初始化我們的最大行列數,不然我們可不可能造出Integer.MAX_VALUE這麼多的子元素~~~~~

  1. // 初始化最大行列數  
  2. mMaxRow = mMaxColumn = 2;  
我們的SquareLayout中有5個子元素,那麼這裏就暫定我們的最大行列均爲2好了,首先來看看onMeasure方法的邏輯處理,變動較大我先貼代碼好了:

  1. /** 
  2.  *  
  3.  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  4.  * @since 2015/1/23 
  5.  *  
  6.  */  
  7. public class SquareLayout extends ViewGroup {  
  8.     private static final int ORIENTATION_HORIZONTAL = 0, ORIENTATION_VERTICAL = 1;// 排列方向的常量標識值  
  9.     private static final int DEFAULT_MAX_ROW = Integer.MAX_VALUE, DEFAULT_MAX_COLUMN = Integer.MAX_VALUE;// 最大行列默認值  
  10.   
  11.     private int mMaxRow = DEFAULT_MAX_ROW;// 最大行數  
  12.     private int mMaxColumn = DEFAULT_MAX_COLUMN;// 最大列數  
  13.   
  14.     private int mOrientation = ORIENTATION_HORIZONTAL;// 排列方向默認橫向  
  15.   
  16.     // 省去構造方法…………  
  17.   
  18.     @SuppressLint("NewApi")  
  19.     @Override  
  20.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  21.         /* 
  22.          * 聲明臨時變量存儲父容器的期望值 
  23.          * 該值應該等於父容器的內邊距加上所有子元素的測量寬高和外邊距 
  24.          */  
  25.         int parentDesireWidth = 0;  
  26.         int parentDesireHeight = 0;  
  27.   
  28.         // 聲明臨時變量存儲子元素的測量狀態  
  29.         int childMeasureState = 0;  
  30.   
  31.         /* 
  32.          * 如果父容器內有子元素 
  33.          */  
  34.         if (getChildCount() > 0) {  
  35.             // 聲明兩個一維數組存儲子元素寬高數據  
  36.             int[] childWidths = new int[getChildCount()];  
  37.             int[] childHeights = new int[getChildCount()];  
  38.   
  39.             /* 
  40.              * 那麼就遍歷子元素 
  41.              */  
  42.             for (int i = 0; i < getChildCount(); i++) {  
  43.                 // 獲取對應遍歷下標的子元素  
  44.                 View child = getChildAt(i);  
  45.   
  46.                 /* 
  47.                  * 如果該子元素沒有以“不佔用空間”的方式隱藏則表示其需要被測量計算 
  48.                  */  
  49.                 if (child.getVisibility() != View.GONE) {  
  50.   
  51.                     // 測量子元素並考量其外邊距  
  52.                     measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);  
  53.   
  54.                     // 比較子元素測量寬高並比較取其較大值  
  55.                     int childMeasureSize = Math.max(child.getMeasuredWidth(), child.getMeasuredHeight());  
  56.   
  57.                     // 重新封裝子元素測量規格  
  58.                     int childMeasureSpec = MeasureSpec.makeMeasureSpec(childMeasureSize, MeasureSpec.EXACTLY);  
  59.   
  60.                     // 重新測量子元素  
  61.                     child.measure(childMeasureSpec, childMeasureSpec);  
  62.   
  63.                     // 獲取子元素佈局參數  
  64.                     MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams();  
  65.   
  66.                     /* 
  67.                      * 考量外邊距計算子元素實際寬高並將數據存入數組 
  68.                      */  
  69.                     childWidths[i] = child.getMeasuredWidth() + mlp.leftMargin + mlp.rightMargin;  
  70.                     childHeights[i] = child.getMeasuredHeight() + mlp.topMargin + mlp.bottomMargin;  
  71.   
  72.                     // 合併子元素的測量狀態  
  73.                     childMeasureState = combineMeasuredStates(childMeasureState, child.getMeasuredState());  
  74.                 }  
  75.             }  
  76.   
  77.             // 聲明臨時變量存儲行/列寬高  
  78.             int indexMultiWidth = 0, indexMultiHeight = 0;  
  79.   
  80.             /* 
  81.              * 如果爲橫向排列 
  82.              */  
  83.             if (mOrientation == ORIENTATION_HORIZONTAL) {  
  84.                 /* 
  85.                  * 如果子元素數量大於限定值則進行折行計算 
  86.                  */  
  87.                 if (getChildCount() > mMaxColumn) {  
  88.   
  89.                     // 計算產生的行數  
  90.                     int row = getChildCount() / mMaxColumn;  
  91.   
  92.                     // 計算餘數  
  93.                     int remainder = getChildCount() % mMaxColumn;  
  94.   
  95.                     // 聲明臨時變量存儲子元素寬高數組下標值  
  96.                     int index = 0;  
  97.   
  98.                     /* 
  99.                      * 遍歷數組計算父容器期望寬高值 
  100.                      */  
  101.                     for (int x = 0; x < row; x++) {  
  102.                         for (int y = 0; y < mMaxColumn; y++) {  
  103.                             // 單行寬度累加  
  104.                             indexMultiWidth += childWidths[index];  
  105.   
  106.                             // 單行高度取最大值  
  107.                             indexMultiHeight = Math.max(indexMultiHeight, childHeights[index++]);  
  108.                         }  
  109.                         // 每一行遍歷完後將該行寬度與上一行寬度比較取最大值  
  110.                         parentDesireWidth = Math.max(parentDesireWidth, indexMultiWidth);  
  111.   
  112.                         // 每一行遍歷完後累加各行高度  
  113.                         parentDesireHeight += indexMultiHeight;  
  114.   
  115.                         // 重置參數  
  116.                         indexMultiWidth = indexMultiHeight = 0;  
  117.                     }  
  118.   
  119.                     /* 
  120.                      * 如果有餘數表示有子元素未能佔據一行 
  121.                      */  
  122.                     if (remainder != 0) {  
  123.                         /* 
  124.                          * 遍歷剩下的這些子元素將其寬高計算到父容器期望值 
  125.                          */  
  126.                         for (int i = getChildCount() - remainder; i < getChildCount(); i++) {  
  127.                             indexMultiWidth += childWidths[i];  
  128.                             indexMultiHeight = Math.max(indexMultiHeight, childHeights[i]);  
  129.                         }  
  130.                         parentDesireWidth = Math.max(parentDesireWidth, indexMultiWidth);  
  131.                         parentDesireHeight += indexMultiHeight;  
  132.                         indexMultiWidth = indexMultiHeight = 0;  
  133.                     }  
  134.                 }  
  135.   
  136.                 /* 
  137.                  * 如果子元素數量還沒有限制值大那麼直接計算即可不須折行 
  138.                  */  
  139.                 else {  
  140.                     for (int i = 0; i < getChildCount(); i++) {  
  141.                         // 累加子元素的實際高度  
  142.                         parentDesireHeight += childHeights[i];  
  143.   
  144.                         // 獲取子元素中寬度最大值  
  145.                         parentDesireWidth = Math.max(parentDesireWidth, childWidths[i]);  
  146.                     }  
  147.                 }  
  148.             }  
  149.   
  150.             /* 
  151.              * 如果爲豎向排列 
  152.              */  
  153.             else if (mOrientation == ORIENTATION_VERTICAL) {  
  154.                 if (getChildCount() > mMaxRow) {  
  155.                     int column = getChildCount() / mMaxRow;  
  156.                     int remainder = getChildCount() % mMaxRow;  
  157.                     int index = 0;  
  158.   
  159.                     for (int x = 0; x < column; x++) {  
  160.                         for (int y = 0; y < mMaxRow; y++) {  
  161.                             indexMultiHeight += childHeights[index];  
  162.                             indexMultiWidth = Math.max(indexMultiWidth, childWidths[index++]);  
  163.                         }  
  164.                         parentDesireHeight = Math.max(parentDesireHeight, indexMultiHeight);  
  165.                         parentDesireWidth += indexMultiWidth;  
  166.                         indexMultiWidth = indexMultiHeight = 0;  
  167.                     }  
  168.   
  169.                     if (remainder != 0) {  
  170.                         for (int i = getChildCount() - remainder; i < getChildCount(); i++) {  
  171.                             indexMultiHeight += childHeights[i];  
  172.                             indexMultiWidth = Math.max(indexMultiHeight, childWidths[i]);  
  173.                         }  
  174.                         parentDesireHeight = Math.max(parentDesireHeight, indexMultiHeight);  
  175.                         parentDesireWidth += indexMultiWidth;  
  176.                         indexMultiWidth = indexMultiHeight = 0;  
  177.                     }  
  178.                 } else {  
  179.                     for (int i = 0; i < getChildCount(); i++) {  
  180.                         // 累加子元素的實際寬度  
  181.                         parentDesireWidth += childWidths[i];  
  182.   
  183.                         // 獲取子元素中高度最大值  
  184.                         parentDesireHeight = Math.max(parentDesireHeight, childHeights[i]);  
  185.                     }  
  186.                 }  
  187.             }  
  188.   
  189.             /* 
  190.              * 考量父容器內邊距將其累加到期望值 
  191.              */  
  192.             parentDesireWidth += getPaddingLeft() + getPaddingRight();  
  193.             parentDesireHeight += getPaddingTop() + getPaddingBottom();  
  194.   
  195.             /* 
  196.              * 嘗試比較父容器期望值與Android建議的最小值大小並取較大值 
  197.              */  
  198.             parentDesireWidth = Math.max(parentDesireWidth, getSuggestedMinimumWidth());  
  199.             parentDesireHeight = Math.max(parentDesireHeight, getSuggestedMinimumHeight());  
  200.         }  
  201.   
  202.         // 確定父容器的測量寬高  
  203.         setMeasuredDimension(resolveSizeAndState(parentDesireWidth, widthMeasureSpec, childMeasureState),  
  204.                 resolveSizeAndState(parentDesireHeight, heightMeasureSpec, childMeasureState << MEASURED_HEIGHT_STATE_SHIFT));  
  205.     }  
  206.   
  207.     // 省去onLayout方法…………  
  208.   
  209.     // 省去四個屌毛方法……  
  210. }  
邏輯計算變動較大,首先在遍歷子元素時我沒有直接對橫縱向排列進行計算而是先用兩個數組將子元素的寬高存儲起來:

  1. @SuppressLint("NewApi")  
  2. @Override  
  3. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  4.     // 省去幾行代碼…………  
  5.   
  6.     /* 
  7.      * 如果父容器內有子元素 
  8.      */  
  9.     if (getChildCount() > 0) {  
  10.         // 聲明兩個一維數組存儲子元素寬高數據  
  11.         int[] childWidths = new int[getChildCount()];  
  12.         int[] childHeights = new int[getChildCount()];  
  13.   
  14.         /* 
  15.          * 那麼就遍歷子元素 
  16.          */  
  17.         for (int i = 0; i < getChildCount(); i++) {  
  18.             // 省省省……  
  19.   
  20.             /* 
  21.              * 如果該子元素沒有以“不佔用空間”的方式隱藏則表示其需要被測量計算 
  22.              */  
  23.             if (child.getVisibility() != View.GONE) {  
  24.   
  25.                 // 省去N行代碼……  
  26.   
  27.                 /* 
  28.                  * 考量外邊距計算子元素實際寬高並將數據存入數組 
  29.                  */  
  30.                 childWidths[i] = child.getMeasuredWidth() + mlp.leftMargin + mlp.rightMargin;  
  31.                 childHeights[i] = child.getMeasuredHeight() + mlp.topMargin + mlp.bottomMargin;  
  32.   
  33.                 // 省去一行代碼……  
  34.             }  
  35.         }  
  36.   
  37.         // 聲明臨時變量存儲行/列寬高  
  38.         int indexMultiWidth = 0, indexMultiHeight = 0;  
  39.   
  40.         // 省去無數行代碼……………………  
  41.     }  
  42.   
  43.     // 省去一行代碼……  
  44. }  
然後上面還聲明兩個臨時變量indexMultiWidth和indexMultiHeight用來分別存儲單行/列的寬高並將該行計算後的結果累加到父容器的期望值,這裏我們就看看橫向排列的邏輯:

  1. /* 
  2.  * 如果爲橫向排列 
  3.  */  
  4. if (mOrientation == ORIENTATION_HORIZONTAL) {  
  5.     /* 
  6.      * 如果子元素數量大於限定值則進行折行計算 
  7.      */  
  8.     if (getChildCount() > mMaxColumn) {  
  9.   
  10.         // 計算產生的行數  
  11.         int row = getChildCount() / mMaxColumn;  
  12.   
  13.         // 計算餘數  
  14.         int remainder = getChildCount() % mMaxColumn;  
  15.   
  16.         // 聲明臨時變量存儲子元素寬高數組下標值  
  17.         int index = 0;  
  18.   
  19.         /* 
  20.          * 遍歷數組計算父容器期望寬高值 
  21.          */  
  22.         for (int x = 0; x < row; x++) {  
  23.             for (int y = 0; y < mMaxColumn; y++) {  
  24.                 // 單行寬度累加  
  25.                 indexMultiWidth += childWidths[index];  
  26.   
  27.                 // 單行高度取最大值  
  28.                 indexMultiHeight = Math.max(indexMultiHeight, childHeights[index++]);  
  29.             }  
  30.             // 每一行遍歷完後將該行寬度與上一行寬度比較取最大值  
  31.             parentDesireWidth = Math.max(parentDesireWidth, indexMultiWidth);  
  32.   
  33.             // 每一行遍歷完後累加各行高度  
  34.             parentDesireHeight += indexMultiHeight;  
  35.   
  36.             // 重置參數  
  37.             indexMultiWidth = indexMultiHeight = 0;  
  38.         }  
  39.   
  40.         /* 
  41.          * 如果有餘數表示有子元素未能佔據一行 
  42.          */  
  43.         if (remainder != 0) {  
  44.             /* 
  45.              * 遍歷剩下的這些子元素將其寬高計算到父容器期望值 
  46.              */  
  47.             for (int i = getChildCount() - remainder; i < getChildCount(); i++) {  
  48.                 indexMultiWidth += childWidths[i];  
  49.                 indexMultiHeight = Math.max(indexMultiHeight, childHeights[i]);  
  50.             }  
  51.             parentDesireWidth = Math.max(parentDesireWidth, indexMultiWidth);  
  52.             parentDesireHeight += indexMultiHeight;  
  53.             indexMultiWidth = indexMultiHeight = 0;  
  54.         }  
  55.     }  
  56.   
  57.     /* 
  58.      * 如果子元素數量還沒有限制值大那麼直接計算即可不須折行 
  59.      */  
  60.     else {  
  61.         for (int i = 0; i < getChildCount(); i++) {  
  62.             // 累加子元素的實際高度  
  63.             parentDesireHeight += childHeights[i];  
  64.   
  65.             // 獲取子元素中寬度最大值  
  66.             parentDesireWidth = Math.max(parentDesireWidth, childWidths[i]);  
  67.         }  
  68.     }  
  69. }  
計算我分了兩種情況,子元素數量如果小於我們的限定值,例如我們佈局下只有2個子元素,而我們的限定值爲3,這時候就沒必要計算折行,而另一種情況則是子元素數量大於我們的限定值,例如我們的佈局下有7個子元素而我們的限定值爲3,這時當我們橫向排列到第三個子元素後就得折行了,在新的一行開始排列,在這種情況下,我們先計算了能被整除的子元素數:例如7/3爲2餘1,也就意味着我們此時能排滿的只有兩行,而多出來的那一行只有一個子元素,分別計算兩種情況累加結果就OK了。縱向排列類似不說了,這裏我邏輯比較臃腫,但是可以讓大家很好理解,如果你Math好可以簡化很多邏輯,不說了,既然onMeasure方法改動了,那麼我們的onLayout方法也得跟上時代的步伐才行:

  1. /** 
  2.  *  
  3.  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  4.  * @since 2015/1/23 
  5.  *  
  6.  */  
  7. public class SquareLayout extends ViewGroup {  
  8.     private static final int ORIENTATION_HORIZONTAL = 0, ORIENTATION_VERTICAL = 1;// 排列方向的常量標識值  
  9.     private static final int DEFAULT_MAX_ROW = Integer.MAX_VALUE, DEFAULT_MAX_COLUMN = Integer.MAX_VALUE;// 最大行列默認值  
  10.   
  11.     private int mMaxRow = DEFAULT_MAX_ROW;// 最大行數  
  12.     private int mMaxColumn = DEFAULT_MAX_COLUMN;// 最大列數  
  13.   
  14.     private int mOrientation = ORIENTATION_HORIZONTAL;// 排列方向默認橫向  
  15.   
  16.     // 省去構造方法…………  
  17.   
  18.     // 省去上面已經給過的onMeasure方法…………  
  19.   
  20.     @Override  
  21.     protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  22.         /* 
  23.          * 如果父容器下有子元素 
  24.          */  
  25.         if (getChildCount() > 0) {  
  26.             // 聲明臨時變量存儲寬高倍增值  
  27.             int multi = 0;  
  28.   
  29.             // 指數倍增值  
  30.             int indexMulti = 1;  
  31.   
  32.             // 聲明臨時變量存儲行/列寬高  
  33.             int indexMultiWidth = 0, indexMultiHeight = 0;  
  34.   
  35.             // 聲明臨時變量存儲行/列臨時寬高  
  36.             int tempHeight = 0, tempWidth = 0;  
  37.   
  38.             /* 
  39.              * 遍歷子元素 
  40.              */  
  41.             for (int i = 0; i < getChildCount(); i++) {  
  42.                 // 獲取對應遍歷下標的子元素  
  43.                 View child = getChildAt(i);  
  44.   
  45.                 /* 
  46.                  * 如果該子元素沒有以“不佔用空間”的方式隱藏則表示其需要被測量計算 
  47.                  */  
  48.                 if (child.getVisibility() != View.GONE) {  
  49.                     // 獲取子元素佈局參數  
  50.                     MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams();  
  51.   
  52.                     // 獲取控件尺寸  
  53.                     int childActualSize = child.getMeasuredWidth();// child.getMeasuredHeight()  
  54.   
  55.                     /* 
  56.                      * 如果爲橫向排列 
  57.                      */  
  58.                     if (mOrientation == ORIENTATION_HORIZONTAL) {  
  59.                         /* 
  60.                          * 如果子元素數量比限定值大 
  61.                          */  
  62.                         if (getChildCount() > mMaxColumn) {  
  63.                             /* 
  64.                              * 根據當前子元素進行佈局 
  65.                              */  
  66.                             if (i < mMaxColumn * indexMulti) {  
  67.                                 child.layout(getPaddingLeft() + mlp.leftMargin + indexMultiWidth, getPaddingTop() + mlp.topMargin + indexMultiHeight,  
  68.                                         childActualSize + getPaddingLeft() + mlp.leftMargin + indexMultiWidth, childActualSize + getPaddingTop()  
  69.                                                 + mlp.topMargin + indexMultiHeight);  
  70.                                 indexMultiWidth += childActualSize + mlp.leftMargin + mlp.rightMargin;  
  71.                                 tempHeight = Math.max(tempHeight, childActualSize) + mlp.topMargin + mlp.bottomMargin;  
  72.   
  73.                                 /* 
  74.                                  * 如果下一次遍歷到的子元素下標值大於限定值 
  75.                                  */  
  76.                                 if (i + 1 >= mMaxColumn * indexMulti) {  
  77.                                     // 那麼累加高度到高度倍增值  
  78.                                     indexMultiHeight += tempHeight;  
  79.   
  80.                                     // 重置寬度倍增值  
  81.                                     indexMultiWidth = 0;  
  82.   
  83.                                     // 增加指數倍增值  
  84.                                     indexMulti++;  
  85.                                 }  
  86.                             }  
  87.                         } else {  
  88.                             // 確定子元素左上、右下座標  
  89.                             child.layout(getPaddingLeft() + mlp.leftMargin + multi, getPaddingTop() + mlp.topMargin, childActualSize  
  90.                                     + getPaddingLeft() + mlp.leftMargin + multi, childActualSize + getPaddingTop() + mlp.topMargin);  
  91.   
  92.                             // 累加倍增值  
  93.                             multi += childActualSize + mlp.leftMargin + mlp.rightMargin;  
  94.                         }  
  95.                     }  
  96.   
  97.                     /* 
  98.                      * 如果爲豎向排列 
  99.                      */  
  100.                     else if (mOrientation == ORIENTATION_VERTICAL) {  
  101.                         if (getChildCount() > mMaxRow) {  
  102.                             if (i < mMaxRow * indexMulti) {  
  103.                                 child.layout(getPaddingLeft() + mlp.leftMargin + indexMultiWidth, getPaddingTop() + mlp.topMargin + indexMultiHeight,  
  104.                                         childActualSize + getPaddingLeft() + mlp.leftMargin + indexMultiWidth, childActualSize + getPaddingTop()  
  105.                                                 + mlp.topMargin + indexMultiHeight);  
  106.                                 indexMultiHeight += childActualSize + mlp.topMargin + mlp.bottomMargin;  
  107.                                 tempWidth = Math.max(tempWidth, childActualSize) + mlp.leftMargin + mlp.rightMargin;  
  108.                                 if (i + 1 >= mMaxRow * indexMulti) {  
  109.                                     indexMultiWidth += tempWidth;  
  110.                                     indexMultiHeight = 0;  
  111.                                     indexMulti++;  
  112.                                 }  
  113.                             }  
  114.                         } else {  
  115.                             // 確定子元素左上、右下座標  
  116.                             child.layout(getPaddingLeft() + mlp.leftMargin, getPaddingTop() + mlp.topMargin + multi, childActualSize  
  117.                                     + getPaddingLeft() + mlp.leftMargin, childActualSize + getPaddingTop() + mlp.topMargin + multi);  
  118.   
  119.                             // 累加倍增值  
  120.                             multi += childActualSize + mlp.topMargin + mlp.bottomMargin;  
  121.                         }  
  122.                     }  
  123.                 }  
  124.             }  
  125.         }  
  126.     }  
  127.   
  128.     // 省去四個屌毛方法……  
  129. }  
onLayout方法就不具體說了,其實現要比onMeasure方法簡單,我們稍微更改下佈局文件儘可能地測試多種情況:

  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:layout_gravity="center"  
  5.     android:background="#ffffff" >  
  6.   
  7.     <com.aigestudio.customviewdemo.views.SquareLayout  
  8.         android:layout_width="wrap_content"  
  9.         android:layout_height="wrap_content"  
  10.         android:layout_gravity="center"  
  11.         android:layout_margin="5dp"  
  12.         android:background="#679135"  
  13.         android:paddingBottom="20dp"  
  14.         android:paddingLeft="5dp"  
  15.         android:paddingRight="7dp"  
  16.         android:paddingTop="12dp" >  
  17.   
  18.         <Button  
  19.             android:layout_width="wrap_content"  
  20.             android:layout_height="wrap_content"  
  21.             android:background="#125793"  
  22.             android:text="tomorrow"  
  23.             android:textSize="24sp"  
  24.             android:textStyle="bold"  
  25.             android:typeface="serif" />  
  26.   
  27.         <Button  
  28.             android:layout_width="50dp"  
  29.             android:layout_height="100dp"  
  30.             android:layout_marginBottom="5dp"  
  31.             android:layout_marginLeft="10dp"  
  32.             android:layout_marginRight="20dp"  
  33.             android:layout_marginTop="30dp"  
  34.             android:background="#495287"  
  35.             android:text="AigeStudio" />  
  36.   
  37.         <LinearLayout  
  38.             android:layout_width="wrap_content"  
  39.             android:layout_height="wrap_content"  
  40.             android:layout_marginBottom="50dp"  
  41.             android:layout_marginLeft="5dp"  
  42.             android:layout_marginRight="20dp"  
  43.             android:layout_marginTop="15dp"  
  44.             android:background="#976234" >  
  45.   
  46.             <ImageView  
  47.                 android:layout_width="wrap_content"  
  48.                 android:layout_height="wrap_content"  
  49.                 android:scaleType="centerCrop"  
  50.                 android:src="@drawable/lovestory_little" />  
  51.         </LinearLayout>  
  52.   
  53.         <Button  
  54.             android:layout_width="wrap_content"  
  55.             android:layout_height="wrap_content"  
  56.             android:background="#594342"  
  57.             android:text="AigeStudio" />  
  58.   
  59.         <Button  
  60.             android:layout_width="wrap_content"  
  61.             android:layout_height="wrap_content"  
  62.             android:background="#961315"  
  63.             android:text="Welcome AigeStudio" />  
  64.     </com.aigestudio.customviewdemo.views.SquareLayout>  
  65.   
  66. </LinearLayout>  
下面看看ADT中的直接顯示效果:


運行後的顯示效果:


換成縱向排列看看:


運行後的效果:


嘗試更改下縱向排列時的限制值:

  1. // 初始化最大行列數  
  2. mMaxRow = 2;  
  3. mMaxColumn =3;  
直接看運行效果:


表示暫時木有發現什麼大問題,OK,這兩個屬性值的實現就到這裏,雖然只有兩個屬性值  =  =   TMD實在是菊緊啊,可想而知LinearLayout等佈局這麼多屬性控制是有多蛋疼了麼,不過如我文章開頭所說,我們的這個自定義佈局實用意義不大,主要還是給大家演示瞭解下自定義佈局是有多麼蛋疼、啊不……是由多麼複雜,像系統自帶的那些佈局控件都是經過N多update版本纔有今天,即便如此,依然還有很多BUG,不過大多不會影響實際使用我們也可以很好地解決,所以,再次強調、控件的測量是一個極爲嚴謹縝密的過程,稍有不慎你的控件便到處都會是說不出的BUG~~~~~上一節我們爲了能讓我們的自定義佈局能對外邊距進行計算,我們定義了一個內部類LayoutParams繼承於MarginLayoutParams但是其中什麼也沒做,而這一節呢我們沒有定義這麼一個內部類而是直接返回MarginLayoutParams的實例,我們之所以能從佈局參數中獲取到外邊距的屬性值,比如:

  1. // 獲取子元素佈局參數  
  2. MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams();  
然後各種

  1. mlp.leftMargin  
  2. mlp.topMargin  
  3. mlp.rightMargin  
  4. mlp.bottomMargin  
是因爲在MarginLayoutParams中已經爲我們定義好了這些參數,具體代碼就不貼了,如果我們定義了自己的佈局,我們也可以去定義自己的佈局參數,比如我們在其中定義子元素在佈局中的對其方式:

  1. /** 
  2.  *  
  3.  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  4.  * @since 2015/1/23 
  5.  *  
  6.  */  
  7. public class SquareLayout extends ViewGroup {  
  8.     // 省去無數代碼…………  
  9.   
  10.     public static class LayoutParams extends MarginLayoutParams {  
  11.         public int mGravity;// 對齊方式  
  12.   
  13.         public LayoutParams(MarginLayoutParams source) {  
  14.             super(source);  
  15.         }  
  16.   
  17.         public LayoutParams(android.view.ViewGroup.LayoutParams source) {  
  18.             super(source);  
  19.         }  
  20.   
  21.         public LayoutParams(Context c, AttributeSet attrs) {  
  22.             super(c, attrs);  
  23.         }  
  24.   
  25.         public LayoutParams(int width, int height) {  
  26.             super(width, height);  
  27.         }  
  28.     }  
  29. }  
然後呢我們就要修改那四個屌毛方法返回我們自己定義的LayoutParams:

  1. /** 
  2.  *  
  3.  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  4.  * @since 2015/1/23 
  5.  *  
  6.  */  
  7. public class SquareLayout extends ViewGroup {  
  8.     // 省去無數代碼…………  
  9.   
  10.     @Override  
  11.     protected LayoutParams generateDefaultLayoutParams() {  
  12.         return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);  
  13.     }  
  14.   
  15.     @Override  
  16.     protected android.view.ViewGroup.LayoutParams generateLayoutParams(android.view.ViewGroup.LayoutParams p) {  
  17.         return new LayoutParams(p);  
  18.     }  
  19.   
  20.     @Override  
  21.     public android.view.ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {  
  22.         return new LayoutParams(getContext(), attrs);  
  23.     }  
  24.   
  25.     @Override  
  26.     protected boolean checkLayoutParams(android.view.ViewGroup.LayoutParams p) {  
  27.         return p instanceof LayoutParams;  
  28.     }  
  29.   
  30.     // 省去LayoutParams的定義…………  
  31. }  
然後你就可以通過其獲取這個對其方式的值:

  1. // 獲取子元素佈局參數  
  2. LayoutParams mlp = (LayoutParams) child.getLayoutParams();  
  3. if (mlp.mGravity == xxxxxxx) {  
  4.     ………………………………………………………………  
  5. }  
用法跟margin類似,那麼我們如何爲該變量賦值呢?方法多種多樣,可以寫死可以直接調用賦值,這裏我們來看另外的一種方式:通過xml在佈局文件中直接設置其屬性值,我們在使用xml進行佈局時經常會使用這樣的方式指定屬性值:

  1. android:layout_width="wrap_content"  
  2. android:layout_height="wrap_content"  
  3. android:background="#125793"  
  4. android:text="tomorrow"  
  5. android:textSize="24sp"  
  6. android:textStyle="bold"  
  7. android:typeface="serif"   
使用起來灰常方便,而這裏我們也可以自定義屬於自己的xml屬性,方法非常非常簡單,首先需要我們在declare-styleable標籤下定義我們的各類屬性:

  1. <!-- http://blog.csdn.net/aigestudio -->  
  2. <declare-styleable name="SquareLayout">  
  3.     <attr name="my_gravity" format="enum">  
  4.         <enum name="left" value="0" />  
  5.         <enum name="right" value="1" />  
  6.         <enum name="center" value="2" />  
  7.         <enum name="top" value="3" />  
  8.         <enum name="bottom" value="4" />  
  9.     </attr>  
  10. </declare-styleable>  
一般情況下,declare-styleable的定義存放在values/attr.xml文件中,屬性定義好了我們就該在佈局中使用這些屬性,使用方法也很簡單,比如我們在SquareLayout的Button中應用my_gravity屬性:

  1. <Button  
  2.     xmlns:aigestudio="http://schemas.android.com/apk/res/com.aigestudio.customviewdemo"  
  3.     aigestudio:my_gravity="left" />  
在使用自定義屬性前聲明我們包內的命名空間即可,你可以直接寫在佈局文件的根佈局下,命名空間的聲明有兩種寫法,上面是其一,其格式如下:

  1. xmlns:你想要的名字="http://schemas.android.com/apk/res/完整包名"  
第二種方式如果你是用的是Studio,IDE則會提示你使用該方式:

  1. xmlns:你想要的名字="http://schemas.android.com/apk/res-auto"  
都可以,最後就是從xml中獲取這些屬性了,我們可以直接簡單地通過帶有AttributeSet對象的構造方法來獲取:

  1. /** 
  2.  *  
  3.  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  4.  * @since 2015/1/23 
  5.  *  
  6.  */  
  7. public class SquareLayout extends ViewGroup {  
  8.     // 省去無數代碼…………  
  9.   
  10.     public static class LayoutParams extends MarginLayoutParams {  
  11.         public int mGravity;// 對齊方式  
  12.   
  13.         public LayoutParams(MarginLayoutParams source) {  
  14.             super(source);  
  15.         }  
  16.   
  17.         public LayoutParams(android.view.ViewGroup.LayoutParams source) {  
  18.             super(source);  
  19.         }  
  20.   
  21.         public LayoutParams(Context c, AttributeSet attrs) {  
  22.             super(c, attrs);  
  23.   
  24.             /* 
  25.              * 獲取xml對應屬性 
  26.              */  
  27.             TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.SquareLayout);  
  28.             mGravity = a.getInt(R.styleable.SquareLayout_my_gravity, 0);  
  29.         }  
  30.   
  31.         public LayoutParams(int width, int height) {  
  32.             super(width, height);  
  33.         }  
  34.     }  
  35. }  
通過Context的obtainStyledAttributes方法注入AttributeSet對象和我們資源文件中定義的declare-styleable屬性獲取一個TypedArray對象,我們通過這個TypedArray對象各種相應的方法來獲取參數值,本來呢我之前寫了很長的篇幅來給大家介紹這其中的過程,後來發現實在太繁瑣太多幹脆刪了重寫旨在教會大家如何用即可。Android支持如下十種不同類型的屬性定義:

  1. <!-- http://blog.csdn.net/aigestudio -->  
  2. <declare-styleable name="AttrView">  
  3.     <!-- 引用資源 -->  
  4.     <attr name="image" format="reference" />  
  5.     <!-- 顏色 -->  
  6.     <attr name="text_color" format="color" />  
  7.     <!-- 布爾值 -->  
  8.     <attr name="text_display" format="boolean" />  
  9.     <!-- 尺寸大小 -->  
  10.     <attr name="temp1" format="dimension" />  
  11.     <!-- 浮點值 -->  
  12.     <attr name="temp2" format="float" />  
  13.     <!-- 整型值 -->  
  14.     <attr name="temp3" format="integer" />  
  15.     <!-- 字符串 -->  
  16.     <attr name="text" format="string" />  
  17.     <!-- 百分比 -->  
  18.     <attr name="alpha" format="fraction" />  
  19.     <!-- 枚舉 -->  
  20.     <attr name="text_align" format="integer">  
  21.         <enum name="left" value="0" />  
  22.         <enum name="right" value="1" />  
  23.         <enum name="center" value="2" />  
  24.     </attr>  
  25.     <!-- 位運算 -->  
  26.     <attr name="text_optimize" format="integer">  
  27.         <flag name="anti" value="0x001" />  
  28.         <flag name="dither" value="0x002" />  
  29.         <flag name="linear" value="0x004" />  
  30.     </attr>  
  31. </declare-styleable>  
name都是我亂取的不要在意,主要看後面的format,這些類型都很好理解,它們在TypedArray中都有各種對應或重載的方法,比如獲取color的getColor方法,上面我們獲取int的getInt等等,這裏對大家來說比較新穎的是fraction百分比這個類型,其在TypedArray的對應方法如下:

  1. getFraction(int index, int base, int pbase, float defValue)  
第一個參數很好理解表示我們定義的屬性資源ID,最後一個參數呢也和前面的getInt類似,主要是這第二、三個參數,其作用是分開來的,當我們在xml中使用百分比屬性時有兩種寫法,一種是標準的10%而另一種是帶p的10%p:

  1. aigestudio:alpha="10%"  
  2. aigestudio:alpha="10%p"  
當屬性值爲10%的時候base參數起作用,我們此時獲取的參數值就等於(10% * base),而pbase參數則無效,同理當屬性值爲10%p時參數值就等於(10% * pbase)而base無效,Just it。還有兩個比較類似的類型:枚舉和位運算,這兩個類型也很好理解,枚舉嘛就是從衆多的選項中選一個,而位運算則可以選多個並通過“|”組合各種結果:

  1. aigestudio:text_optimize="anti|dither"  
這種寫法相信大家也很常見,比如layout_gravity屬性就可以以類似的方式多選,這種方式有一個好處就是我們不用在屬性聲明中定義太多的值,上面的text_optimize屬性只有三個對應值,但是在code中我們可以以位運算的方式組合這三個參數值:

  1. /* 
  2.  * 畫筆優化的標識位們 
  3.  */  
  4. private static final int OPTIMIZE_ANTI = 0x001, OPTIMIZE_DITHER = 0x002, OPTIMIZE_LINEAR = 0x004, OPTIMIZE_ANTI_DITHER = 0x003, OPTIMIZE_ANTI_LINEAR = 0x005, OPTIMIZE_DITHER_LINEAR = 0x006, OPTIMIZE_ALL = 0x007;  
通過三個參數值的位運算我們實質上就得到了7種不同的結果,Just it。xml屬性值的定義不難不多用幾次就會就不多說了,上面呢我們通過自定義的屬性mGravity來嘗試定義子元素相對於父容器的對其方式,而事實上Android提供給我們一個簡便的方法去計算這玩意,Android定義了Gravity類來實現我們對對其方式的計算,其中定義了大量的常量值定義不同對其方式,比如什麼左對齊、右對齊、水平居中亂七八糟的等等,也提供了多個方法來實現計算,使用方式呢也不難,比如上面的佈局參數我們換成如下方式:

  1. /** 
  2.  *  
  3.  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} 
  4.  * @since 2015/1/23 
  5.  *  
  6.  */  
  7. public class SquareLayout extends ViewGroup {  
  8.     // 省去無數代碼…………  
  9.   
  10.     public static class LayoutParams extends MarginLayoutParams {  
  11.         public int mGravity = Gravity.LEFT | Gravity.RIGHT;// 對齊方式  
  12.   
  13.         // 省去沒變的代碼…………  
  14.     }  
  15. }  
而在我們的xml屬性定義中則可以直接使用android:layout_gravity這樣的name而無需定義類型值:

  1. <declare-styleable name="SquareLayout">  
  2.     <attr name="android:layout_gravity" />  
  3. </declare-styleable>  
這樣則表示我們的屬性使用的Android自帶的標籤,之後我們只需根據佈局文件中layout_gravity屬性的值調用Gravity類下的方法去計算對齊方式則可,Gravity類下的方法很好用,爲什麼這麼說呢?因爲其可以說是無關佈局的,拿最簡單的一個來說:

  1. public static void apply(int gravity, int w, int h, Rect container, Rect outRect)  
第一個參數表示我們的對其方式值,第二三個參數呢則表示我們要對齊的元素,這裏通俗地說就是我們父容器下的子元素,而container參數表示的則是我們父容器的矩形區域,最後一個參數是接收計算後子元素位置區域的矩形對象,隨便new個傳進去就行,可見apply方式是根據矩形區域來計算對其方式的,所以說非常好用,我們只需在onLayout方法中確定出父容器的矩形區域就可以輕鬆地計算出子元素根據對其方式出現在父容器中的矩形區域,這一個過程留給大家自行嘗試,我就不多說了,TMD說的太多,又忘了上一節的那個問題了、肏!!!!好吧,下一節再說那個問題,哦!對了,還有一個擦邊球的東西忘了講,在Android很多的佈局控件中都會重寫這麼一個方法:

  1. @Override  
  2. public boolean shouldDelayChildPressedState() {  
  3.     return false;  
  4. }  
並且都會一致地返回false,其作用是告訴framework我們當前的佈局不是一個滾動的佈局,我們這裏的自定義佈局控件也重寫了該方法~~~好好了了,不講了,這節就到此爲止,接下來就是下一節,接踵而至in two days~~~~~~thx

源碼下載:傳送門

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