1. 程式人生 > >自定義控制元件其實很簡單2/3

自定義控制元件其實很簡單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屬性豈不是成了擺設?會不會呢?留給各位去想。好吧、暫時就先定義這倆屬性,光這倆已經夠折騰的了,來來來建立我們的佈局:

/** *  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} * @since 2015/1/23 *  */public class SquareLayout extends ViewGroup
private int mMaxRow;// 最大行數 private int mMaxColumn;// 最大列數 private int mOrientation;// 排列方向 public SquareLayout(Context context, AttributeSet attrs) {  super(context, attrs); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { }}
上一節我們曾說過,要讓我們父容器下子元素的margins外邊距能夠被正確計算,我們必需重寫父容器的三個相關方法並返回一個MarginLayoutParams的子類:
/** *  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} * @since 2015/1/23 *  */public class SquareLayout extends ViewGroup // 省去各種蛋疼的成員變數………… // 省去構造方法………… // 省去onLayout方法………… @Override protected LayoutParams generateDefaultLayoutParams() {  return new MarginLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); } @Override protected android.view.ViewGroup.LayoutParams generateLayoutParams(android.view.ViewGroup.LayoutParams p) {  return new MarginLayoutParams(p); } @Override public android.view.ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {  return new MarginLayoutParams(getContext(), attrs); }}
這裡我直接返回一個MarginLayoutParams的例項物件,因為我不需要在LayoutParams處理自己的邏輯,單純地計算margins就沒必要去實現一個自定義的MarginLayoutParams子類了,除此之外,你還可以重寫checkLayoutParams方法去驗證當前所使用的LayoutParams物件是否MarginLayoutParams的一個例項:
/** *  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} * @since 2015/1/23 *  */public class SquareLayout extends ViewGroup // 省去各種蛋疼的成員變數………… // 省去構造方法………… // 省去onLayout方法………… // 省去三個屌毛方法…… @Override protected boolean checkLayoutParams(android.view.ViewGroup.LayoutParams p) {  return p instanceof MarginLayoutParams; }}
然後呢我們就要開始對控制元件進行測量了,首先重寫onMeasure方法是肯定的,那麼我們就先在onMeasure中先把測量的邏輯處理了先,不過我們還是按部就班一步一步來,先把排列方式搞定:
/** *  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} * @since 2015/1/23 *  */public class SquareLayout extends ViewGroup private static final int ORIENTATION_HORIZONTAL = 0, ORIENTATION_VERTICAL = 1;// 排列方向的常量標識值 private static final int DEFAULT_MAX_ROW = Integer.MAX_VALUE, DEFAULT_MAX_COLUMN = Integer.MAX_VALUE;// 最大行列預設值 private int mMaxRow = DEFAULT_MAX_ROW;// 最大行數 private int mMaxColumn = DEFAULT_MAX_COLUMN;// 最大列數 private int mOrientation = ORIENTATION_HORIZONTAL;// 排列方向預設橫向 // 省去構造方法………… @SuppressLint("NewApi"@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  /*   * 宣告臨時變數儲存父容器的期望值   * 該值應該等於父容器的內邊距加上所有子元素的測量寬高和外邊距   */  int parentDesireWidth = 0;  int parentDesireHeight = 0;  // 宣告臨時變數儲存子元素的測量狀態  int childMeasureState = 0;  /*   * 如果父容器內有子元素   */  if (getChildCount() > 0) {   /*    * 那麼就遍歷子元素    */   for (int i = 0; i < getChildCount(); i++) {    // 獲取對應遍歷下標的子元素    View child = getChildAt(i);    /*     * 如果該子元素沒有以“不佔用空間”的方式隱藏則表示其需要被測量計算     */    if (child.getVisibility() != View.GONE) {     // 測量子元素並考量其外邊距     measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);     // 比較子元素測量寬高並比較取其較大值     int childMeasureSize = Math.max(child.getMeasuredWidth(), child.getMeasuredHeight());     // 重新封裝子元素測量規格     int childMeasureSpec = MeasureSpec.makeMeasureSpec(childMeasureSize, MeasureSpec.EXACTLY);     // 重新測量子元素     child.measure(childMeasureSpec, childMeasureSpec);     // 獲取子元素佈局引數     MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams();     /*      * 考量外邊距計運算元元素實際寬高      */     int childActualWidth = child.getMeasuredWidth() + mlp.leftMargin + mlp.rightMargin;     int childActualHeight = child.getMeasuredHeight() + mlp.topMargin + mlp.bottomMargin;     /*      * 如果為橫向排列      */     if (mOrientation == ORIENTATION_HORIZONTAL) {      // 累加子元素的實際寬度      parentDesireWidth += childActualWidth;      // 獲取子元素中高度最大值      parentDesireHeight = Math.max(parentDesireHeight, childActualHeight);     }     /*      * 如果為豎向排列      */     else if (mOrientation == ORIENTATION_VERTICAL) {      // 累加子元素的實際高度      parentDesireHeight += childActualHeight;      // 獲取子元素中寬度最大值      parentDesireWidth = Math.max(parentDesireWidth, childActualWidth);     }     // 合併子元素的測量狀態     childMeasureState = combineMeasuredStates(childMeasureState, child.getMeasuredState());    }   }   /*    * 考量父容器內邊距將其累加到期望值    */   parentDesireWidth += getPaddingLeft() + getPaddingRight();   parentDesireHeight += getPaddingTop() + getPaddingBottom();   /*    * 嘗試比較父容器期望值與Android建議的最小值大小並取較大值    */   parentDesireWidth = Math.max(parentDesireWidth, getSuggestedMinimumWidth());   parentDesireHeight = Math.max(parentDesireHeight, getSuggestedMinimumHeight());  }  // 確定父容器的測量寬高  setMeasuredDimension(resolveSizeAndState(parentDesireWidth, widthMeasureSpec, childMeasureState),    resolveSizeAndState(parentDesireHeight, heightMeasureSpec, childMeasureState << MEASURED_HEIGHT_STATE_SHIFT)); } // 省去onLayout方法………… // 省去四個屌毛方法……}
上面程式碼註釋很清楚,具體的我就不扯了,小窗我的童鞋有一部分問過我上一節中我在確定測量尺寸時候使用的resolveSize方法作用(以下程式碼源自上一節的CustomLayout):
/** *  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} * @since 2015/1/15 *  */public class CustomLayout extends ViewGroup // 省去N多程式碼 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  // 省省省………………  // 設定最終測量值  setMeasuredDimension(resolveSize(parentDesireWidth, widthMeasureSpec), resolveSize(parentDesireHeight, heightMeasureSpec)); } // 省去N+1多程式碼}
那麼這個resolveSize方法其實是View提供給我們解算尺寸大小的一個工具方法,其具體實現在API 11後交由另一個方法resolveSizeAndState也就是我們這一節例子所用到的去處理:
public static int resolveSize(int size, int measureSpec) {    return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;}
而這個resolveSizeAndState方法具體實現其實跟我們上一節開頭解算Bitmap尺寸的邏輯類似:
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {    int result = size;    int specMode = MeasureSpec.getMode(measureSpec);    int specSize =  MeasureSpec.getSize(measureSpec);    switch (specMode) {    case MeasureSpec.UNSPECIFIED:        result = size;        break;    case MeasureSpec.AT_MOST:        if (specSize < size) {            result = specSize | MEASURED_STATE_TOO_SMALL;        } else {            result = size;        }        break;    case MeasureSpec.EXACTLY:        result = specSize;        break;    }    return result | (childMeasuredState&MEASURED_STATE_MASK);}
是不是很類似呢?如果沒看過我上一節的內容,可以回頭去閱讀一下自定義控制元件其實很簡單7/12,與我們不同的是這個方法多了一個childMeasuredState引數,而上面例子我們在具體測量時也引入了一個childMeasureState臨時變數的計算,那麼這個值的作用是什麼呢?有何意義呢?說到這裡不得不提API 11後引入的幾個標識位:

這些標識位上面的程式碼中我們都有用到,而官方文件對其作用的說明也是模稜兩可,原始碼裡的運用也不明朗,比如說我們看其它幾個與其相關的幾個方法:

public final int getMeasuredWidth() {    return mMeasuredWidth & MEASURED_SIZE_MASK;}public final int getMeasuredHeight() {    return mMeasuredHeight & MEASURED_SIZE_MASK;}public final int getMeasuredState() {    return (mMeasuredWidth&MEASURED_STATE_MASK)            | ((mMeasuredHeight>>MEASURED_HEIGHT_STATE_SHIFT)                    & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));}
這裡大家注意getMeasuredWidth和getMeasuredHeight這兩個我們用來獲取控制元件測量寬高的方法,在其之中對其做了一個按位與的運算,然後才把這個測量值返回給我們,也就是說這個mMeasuredWidth和mMeasuredHeight裡面應該還封裝了些什麼對吧,那麼我們來看其賦值,其賦值是在setMeasuredDimension方法下進行的:
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {    // 省去無關程式碼……    mMeasuredWidth = measuredWidth;    mMeasuredHeight = measuredHeight; // 省去一行程式碼……}
也就是說當我們給控制元件設定最終測量尺寸時這個值就直接被賦予給了mMeasuredWidth和mMeasuredHeight這兩個成員變數……看到這裡很多朋友蛋疼了,那有啥區別和意義呢?我們嘗試來翻翻系統自帶控制元件關於它的處理,其中TextView沒有涉及到這個引數的應用,而ImageView裡則有:
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) // 省去海量程式碼………… widthSize = resolveSizeAndState(w, widthMeasureSpec, 0);    heightSize = resolveSizeAndState(h, heightMeasureSpec, 0); // 省去一點程式碼…………}
在ImageView的onMeasure方法中使用resolveSizeAndState再次對計算得出的寬高進行解算,而這裡的第三個引數直接傳的0,也就是不作任何處理~~~~~~~~蛋疼!真蛋疼,以前寡人也曾糾結過一段時間,後來在stackoverflow在找到兩個比較靠譜的答案:

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

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

/* * 如果為橫向排列 */if (mOrientation == ORIENTATION_HORIZONTAL) { // 累加子元素的實際寬度 parentDesireWidth += childActualWidth; // 獲取子元素中高度最大值 parentDesireHeight = Math.max(parentDesireHeight, childActualHeight);}/* * 如果為豎向排列 */else if (mOrientation == ORIENTATION_VERTICAL) { // 累加子元素的實際高度 parentDesireHeight += childActualHeight; // 獲取子元素中寬度最大值 parentDesireWidth = Math.max(parentDesireWidth, childActualWidth);}
如果為橫/豎向排列,那麼我們應該統計各個子元素的寬/高,而高/寬呢則不需要統計,我們取其最高/最寬的那個子元素的值即可,注意在上一節的處理中我們並沒有這樣去做哦!不知道大家發現沒~~~好了,onMeasure方法的邏輯就是這樣,如果你覺得好長,那麼恭喜你,這只是我們的第一步,爾後還有幾個引數的處理~~~~~這時候你如果執行會發現什麼都沒有,因為onMeasure方法的作用僅僅是測量的一步,按照官方的說法,Android對Viewgroup的測量由兩方面構成:一是對父容器和子元素大小尺寸的測量主要體現在onMeasure方法,二是對父容器的子元素在其區域內的定位主要體現在onLayout方法。也就是會說,即便我們完成了測量但沒告訴兒子們該出現在哪的話也不會有任何顯示效果,OK,現在我們來看看onLayout方法的邏輯處理:
/** *  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} * @since 2015/1/23 *  */public class SquareLayout extends ViewGroup private static final int ORIENTATION_HORIZONTAL = 0, ORIENTATION_VERTICAL = 1;// 排列方向的常量標識值 private static final int DEFAULT_MAX_ROW = Integer.MAX_VALUE, DEFAULT_MAX_COLUMN = Integer.MAX_VALUE;// 最大行列預設值 private int mMaxRow = DEFAULT_MAX_ROW;// 最大行數 private int mMaxColumn = DEFAULT_MAX_COLUMN;// 最大列數 private int mOrientation = ORIENTATION_HORIZONTAL;// 排列方向預設橫向 // 省去構造方法………… // 省去上面已經給過的onMeasure方法………… @Override protected void onLayout(boolean changed, int l, int t, int r, int b) {  /*   * 如果父容器下有子元素   */  if (getChildCount() > 0) {   // 宣告臨時變數儲存寬高倍增值   int multi = 0;   /*    * 遍歷子元素    */   for (int i = 0; i < getChildCount(); i++) {    // 獲取對應遍歷下標的子元素    View child = getChildAt(i);    /*     * 如果該子元素沒有以“不佔用空間”的方式隱藏則表示其需要被測量計算     */    if (child.getVisibility() != View.GONE) {     // 獲取子元素佈局引數     MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams();     // 獲取控制元件尺寸     int childActualSize = child.getMeasuredWidth();// child.getMeasuredHeight()     /*      * 如果為橫向排列      */     if (mOrientation == ORIENTATION_HORIZONTAL) {      // 確定子元素左上、右下座標      child.layout(getPaddingLeft() + mlp.leftMargin + multi, getPaddingTop() + mlp.topMargin, childActualSize + getPaddingLeft()        + mlp.leftMargin + multi, childActualSize + getPaddingTop() + mlp.topMargin);      // 累加倍增值      multi += childActualSize + mlp.leftMargin + mlp.rightMargin;     }     /*      * 如果為豎向排列      */     else if (mOrientation == ORIENTATION_VERTICAL) {      // 確定子元素左上、右下座標      child.layout(getPaddingLeft() + mlp.leftMargin, getPaddingTop() + mlp.topMargin + multi, childActualSize + getPaddingLeft()        + mlp.leftMargin, childActualSize + getPaddingTop() + mlp.topMargin + multi);      // 累加倍增值      multi += childActualSize + mlp.topMargin + mlp.bottomMargin;     }    }   }  } } // 省去四個屌毛方法……}
比起對onMeasure方法的邏輯處理,onLayout方法相對簡單,主要是在對子元素layout的地方需要我們一點計算思維,也不是很複雜,哥相信你能懂,畢竟註釋如此清楚,來我們嘗試用一下我們的佈局:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:layout_gravity="center"    android:background="#ffffff" >    <com.aigestudio.customviewdemo.views.SquareLayout        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:paddingLeft="5dp"        android:paddingTop="12dp"        android:layout_margin="5dp"        android:paddingRight="7dp"        android:paddingBottom="20dp"        android:layout_gravity="center"        android:background="#679135" >        <Button            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:background="#125793"            android:text="tomorrow"            android:textSize="24sp"            android:textStyle="bold"            android:typeface="serif" />        <Button            android:layout_width="50dp"            android:layout_height="100dp"            android:layout_marginBottom="5dp"            android:layout_marginLeft="10dp"            android:layout_marginRight="20dp"            android:layout_marginTop="30dp"            android:background="#495287"            android:text="AigeStudio" />        <ImageView            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_marginBottom="50dp"            android:layout_marginLeft="5dp"            android:layout_marginRight="20dp"            android:layout_marginTop="15dp"            android:background="#976234"            android:scaleType="centerCrop"            android:src="@drawable/lovestory_little" />        <Button            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:background="#594342"            android:text="AigeStudio" />        <Button            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:background="#961315"            android:text="AigeStudio" />    </com.aigestudio.customviewdemo.views.SquareLayout></LinearLayout>

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

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

private int mOrientation = ORIENTATION_VERTICAL;// 排列方向預設橫向
再來瞅瞅ADT的顯示效果:

在執行看看:

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

/** *  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} * @since 2015/1/23 *  */public class SquareLayout extends ViewGroup private static final int ORIENTATION_HORIZONTAL = 0, ORIENTATION_VERTICAL = 1;// 排列方向的常量標識值 private static final int DEFAULT_MAX_ROW = Integer.MAX_VALUE, DEFAULT_MAX_COLUMN = Integer.MAX_VALUE;// 最大行列預設值 private int mMaxRow = DEFAULT_MAX_ROW;// 最大行數 private int mMaxColumn = DEFAULT_MAX_COLUMN;// 最大列數 private int mOrientation = ORIENTATION_HORIZONTAL;// 排列方向預設橫向 public SquareLayout(Context context, AttributeSet attrs) {  super(context, attrs);  // 初始化最大行列數  mMaxRow = mMaxColumn = 2; } // 省去onMeasure方法………… // 省去onLayout方法………… // 省去四個屌毛方法……}
首先呢在構造方法內初始化我們的最大行列數,不然我們可不可能造出Integer.MAX_VALUE這麼多的子元素~~~~~
// 初始化最大行列數mMaxRow = mMaxColumn = 2;
我們的SquareLayout中有5個子元素,那麼這裡就暫定我們的最大行列均為2好了,首先來看看onMeasure方法的邏輯處理,變動較大我先貼程式碼好了:
/** *  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} * @since 2015/1/23 *  */public class SquareLayout extends ViewGroup private static final int ORIENTATION_HORIZONTAL = 0, ORIENTATION_VERTICAL = 1;// 排列方向的常量標識值 private static final int DEFAULT_MAX_ROW = Integer.MAX_VALUE, DEFAULT_MAX_COLUMN = Integer.MAX_VALUE;// 最大行列預設值 private int mMaxRow = DEFAULT_MAX_ROW;// 最大行數 private int mMaxColumn = DEFAULT_MAX_COLUMN;// 最大列數 private int mOrientation = ORIENTATION_HORIZONTAL;// 排列方向預設橫向 // 省去構造方法………… @SuppressLint("NewApi"@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  /*   * 宣告臨時變數儲存父容器的期望值   * 該值應該等於父容器的內邊距加上所有子元素的測量寬高和外邊距   */  int parentDesireWidth = 0;  int parentDesireHeight = 0;  // 宣告臨時變數儲存子元素的測量狀態  int childMeasureState = 0;  /*   * 如果父容器內有子元素   */  if (getChildCount() > 0) {   // 宣告兩個一維陣列儲存子元素寬高資料   int[] childWidths = new int[getChildCount()];   int[] childHeights = new int[getChildCount()];   /*    * 那麼就遍歷子元素    */   for (int i = 0; i < getChildCount(); i++) {    // 獲取對應遍歷下標的子元素    View child = getChildAt(i);    /*     * 如果該子元素沒有以“不佔用空間”的方式隱藏則表示其需要被測量計算     */    if (child.getVisibility() != View.GONE) {     // 測量子元素並考量其外邊距     measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);     // 比較子元素測量寬高並比較取其較大值     int childMeasureSize = Math.max(child.getMeasuredWidth(), child.getMeasuredHeight());     // 重新封裝子元素測量規格     int childMeasureSpec = MeasureSpec.makeMeasureSpec(childMeasureSize, MeasureSpec.EXACTLY);     // 重新測量子元素     child.measure(childMeasureSpec, childMeasureSpec);     // 獲取子元素佈局引數     MarginLayoutParams mlp = (MarginLayoutParams) child.getLayoutParams();     /*      * 考量外邊距計運算元元素實際寬高並將資料存入陣列      */     childWidths[i] = child.getMeasuredWidth() + mlp.leftMargin + mlp.rightMargin;     childHeights[i] = child.getMeasuredHeight() + mlp.topMargin + mlp.bottomMargin;     // 合併子元素的測量狀態     childMeasureState = combineMeasuredStates(childMeasureState, child.getMeasuredState());    }   }   // 宣告臨時變數儲存行/列寬高   int indexMultiWidth = 0, indexMultiHeight = 0;   /*    * 如果為橫向排列    */   if (mOrientation == ORIENTATION_HORIZONTAL) {    /*     * 如果子元素數量大於限定值則進行折行計算     */    if (getChildCount() > mMaxColumn) {     // 計算產生的行數     int row = getChildCount() / mMaxColumn;     // 計算餘數     int remainder = getChildCount() % mMaxColumn;     // 宣告臨時變數儲存子元素寬高陣列下標值     int index = 0;     /*      * 遍歷陣列計算父容器期望寬高值      */     for (int x = 0; x < row; x++) {      for (int y = 0; y < mMaxColumn; y++) {       // 單行寬度累加       indexMultiWidth += childWidths[index];       // 單行高度取最大值       indexMultiHeight = Math.max(indexMultiHeight, childHeights[index++]);      }      // 每一行遍歷完後將該行寬度與上一行寬度比較取最大值      parentDesireWidth = Math.max(parentDesireWidth, indexMultiWidth);      // 每一行遍歷完後累加各行高度      parentDesireHeight += indexMultiHeight;      // 重置引數      indexMultiWidth = indexMultiHeight = 0;     }     /*      * 如果有餘數表示有子元素未能佔據一行      */     if (remainder != 0) {      /*       * 遍歷剩下的這些子元素將其寬高計算到父容器期望值       */      for (int i = getChildCount() - remainder; i < getChildCount(); i++) {       indexMultiWidth += childWidths[i];       indexMultiHeight = Math.max(indexMultiHeight, childHeights[i]);      }      parentDesireWidth = Math.max(parentDesireWidth, indexMultiWidth);      parentDesireHeight += indexMultiHeight;      indexMultiWidth = indexMultiHeight = 0;     }    }    /*     * 如果子元素數量還沒有限制值大那麼直接計算即可不須折行     */    else {     for (int i = 0; i < getChildCount(); i++) {      // 累加子元素的實際高度      parentDesireHeight += childHeights[i];      // 獲取子元素中寬度最大值      parentDesireWidth = Math.max(parentDesireWidth, childWidths[i]);     }    }   }   /*    * 如果為豎向排列    */   else if (mOrientation == ORIENTATION_VERTICAL) {    if (getChildCount() > mMaxRow) {     int column = getChildCount() / mMaxRow;     int remainder = getChildCount() % mMaxRow;     int index = 0;     for (int x = 0; x < column; x++) {      for (int y = 0; y < mMaxRow; y++) {       indexMultiHeight += childHeights[index];       indexMultiWidth = Math.max(indexMultiWidth, childWidths[index++]);      }      parentDesireHeight = Math.max(parentDesireHeight, indexMultiHeight);      parentDesireWidth += indexMultiWidth;      indexMultiWidth = indexMultiHeight = 0;     }     if (remainder != 0) {      for (int i = getChildCount() - remainder; i < getChildCount(); i++) {       indexMultiHeight += childHeights[i];       indexMultiWidth = Math.max(indexMultiHeight, childWidths[i]);      }      parentDesireHeight = Math.max(parentDesireHeight, indexMultiHeight);      parentDesireWidth += indexMultiWidth;      indexMultiWidth = indexMultiHeight = 0;     }    } else {     for (int i = 0; i < getChildCount(); i++) {      // 累加子元素的實際寬度      parentDesireWidth += childWidths[i];      // 獲取子元素中高度最大值      parentDesireHeight = Math.max(parentDesireHeight, childHeights[i]);     }    }   }   /*    * 考量父容器內邊距將其累加到期望值    */   parentDesireWidth += getPaddingLeft() + getPaddingRight();   parentDesireHeight += getPaddingTop() + getPaddingBottom();   /*    * 嘗試比較父容器期望值與Android建議的最小值大小並取較大值    */   parentDesireWidth = Math.max(parentDesireWidth, getSuggestedMinimumWidth());   parentDesireHeight = Math.max(parentDesireHeight, getSuggestedMinimumHeight());  }  // 確定父容器的測量寬高  setMeasuredDimension(resolveSizeAndState(parentDesireWidth, widthMeasureSpec, childMeasureState),    resolveSizeAndState(parentDesireHeight, heightMeasureSpec, childMeasureState << MEASURED_HEIGHT_STATE_SHIFT)); } // 省去onLayout方法………… // 省去四個屌毛方法……}
邏輯計算變動較大,首先在遍歷子元素時我沒有直接對橫縱向排列進行計算而是先用兩個陣列將子元素的寬高儲存起來:
@SuppressLint("NewApi")@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) // 省去幾行程式碼………… /*  * 如果父容器內有子元素  */ if (getChildCount() > 0) {  // 宣告兩個一維陣列儲存子元素寬高資料  int[] childWidths = new int[getChildCount()];  int[] childHeights = new int[getChildCount()];  /*   * 那麼就遍歷子元素   */  for (int i = 0; i < getChildCount(); i++) {   // 省省省……   /*    * 如果該子元素沒有以“不佔用空間”的方式隱藏則表示其需要被測量計算    */   if (child.getVisibility() != View.GONE) {    // 省去N行程式碼……    /*     * 考量外邊距計運算元元素實際寬高並將資料存入陣列     */    childWidths[i] = child.getMeasuredWidth() + mlp.leftMargin + mlp.rightMargin;    childHeights[i] = child.getMeasuredHeight() + mlp.topMargin + mlp.bottomMargin;    // 省去一行程式碼……   }  }  // 宣告臨時變數儲存行/列寬高  int indexMultiWidth = 0, indexMultiHeight = 0;  // 省去無數行程式碼…………………… } // 省去一行程式碼……}
然後上面還宣告兩個臨時變數indexMultiWidth和indexMultiHeight用來分別儲存單行/列的寬高並將該行計算後的結果累加到父容器的期望值,這裡我們就看看橫向排列的邏輯:
/* * 如果為橫向排列 */if (mOrientation == ORIENTATION_HORIZONTAL) { /*  * 如果子元素數量大於限定值則進行折行計算  */ if (getChildCount() > mMaxColumn) {  // 計算產生的行數  int row = getChildCount() / mMaxColumn;  // 計算餘數  int remainder = getChildCount() % mMaxColumn;  // 宣告臨時變數儲存子元素寬高陣列下標值  int index = 0;  /*   * 遍歷陣列計算父容器期望寬高值   */  for (int x = 0; x < row; x++) {   for (int y = 0; y < mMaxColumn; y++) {    // 單行寬度累加    indexMultiWidth += childWidths[index];    // 單行高度取最大值    indexMultiHeight = Math.max(indexMultiHeight, childHeights[index++]);   }   // 每一行遍歷完後將該行寬度與上一行寬度比較取最大值   parentDesireWidth = Math.max(parentDesireWidth, indexMultiWidth);   // 每一行遍歷完後累加各行高度   parentDesireHeight += indexMultiHeight;   // 重置引數   indexMultiWidth = indexMultiHeight = 0;  }  /*   * 如果有餘數表示有子元素未能佔據一行   */  if (remainder != 0) {   /*    * 遍歷剩下的這些子元素將其寬高計算到父容器期望值    */   for (int i = getChildCount() - remainder; i < getChildCount(); i++) {    indexMultiWidth += childWidths[i];    indexMultiHeight = Math.max(indexMultiHeight, childHeights[i]);   }   parentDesireWidth = Math.max(parentDesireWidth, indexMultiWidth);   parentDesireHeight += indexMultiHeight;   indexMultiWidth = indexMultiHeight = 0;  } } /*  * 如果子元素數量還沒有限制值大那麼直接計算即可不須折行  */ else {  for (int i = 0; i < getChildCount(); i++) {   // 累加子元素的實際高度   parentDesireHeight += childHeights[i];   // 獲取子元素中寬度最大值   parentDesireWidth = Math.max(parentDesireWidth, childWidths[i]);  } }}
計算我分了兩種情況,子元素數量如果小於我們的限定值,例如我們佈局下只有2個子元素,而我們的限定值為3,這時候就沒必要計算折行,而另一種情況則是子元素數量大於我們的限定值,例如我們的佈局下有7個子元素而我們的限定值為3,這時當我們橫向排列到第三個子元素後就得折行了,在新的一行開始排列,在這種情況下,我們先計算了能被整除的子元素數:例如7/3為2餘1,也就意味著我們此時能排滿的只有兩行,而多出來的那一行只有一個子元素,分別計算兩種情況累加結果就OK了。縱向排列類似不說了,這裡我邏輯比較臃腫,但是可以讓大家很好理解,如果你Math好可以簡化很多邏輯,不說了,既然onMeasure方法改動了,那麼我們的onLayout方法也得跟上時代的步伐才行:
/** *  * @author AigeStudio {@link http://blog.csdn.net/aigestudio} * @since 2015/1/23 *  */public class SquareLayout extends ViewGroup private static final int ORIENTATION_HORIZONTAL = 0, ORIENTATION_VERTICAL = 1;// 排列方向的常量標識值 private static final int DEFAULT_MAX_ROW = Integer.MAX_VALUE, DEFAULT_MAX_COLUMN = Integer.MAX_VALUE;// 最大行列預設值 private int mMaxRow = DEFAULT_MAX_ROW;// 最大行數 private int mMaxColumn = DEFAULT_MAX_COLUMN;// 最大列數 private int mOrientation = ORIENTATION_HORIZONTAL;// 排列方向預設橫向 // 省去構造方法………… // 省去上面已經給過的onMeasure方法………… @Override protected void onLayout(boolean changed, 
            
           

相關推薦

定義控制元件其實簡單2/3

                尊重原創轉載請註明:From AigeStudio(http://blog.csdn.net/aigestudio)Power by Aige 侵權必究!炮兵鎮樓又要開始雞凍人心的一刻了有木有!有木有雞凍! = = ……通過上一節的講解呢我們大致對Android測量控制元件有個

winform定義控制元件之ComboBox簡單重寫

由於專案需要,現有的ComboBox控制元件滿足不了需求,需要重寫做一些小小的改變。要求ComboBox每一項前增加圖片顯示,使邊框顏色修改,及禁用滑鼠滾輪修改當前選項。 定義ComboBox選擇項類 using System; using System.Collectio

duilib中將xml封裝為控制元件簡單示例(簡單定義控制元件,封裝幾個基本控制元件合為1個定義控制元件)

使用duilib的時候,難免會有這樣的需求: 某一塊Container(Layout)以及裡面的佈局需要重複用,不想每次都複製貼上這麼多,要不然xml太大了; 通過繼承來自定義一個控制元件,比如CButtonUIEx之類的,想讓他像button一樣在xml中被識別; xml裡面的東西

定義控制元件01---簡單view的實現

對於每一個應用來說幾乎都會有一個Topbar,並且基本都是類似的那麼假如應用有好多個頁面的話,就要寫好多遍,可以在Topbar整合為一個控制元件來使用,針對於這個的學習,總結如下: 1 atts自定義屬性的定義 res–values-atts.xml <?xml versi

定義控制元件三部曲檢視篇(四)——RecyclerView系列之一簡單使用

絕望的時候不要那麼絕望,高興的時候不要那麼高興,是你慢慢會學會的。 ——董卿 轉了一年多,又回來繼續做Android。果然還是看到程式碼最讓我興奮……但有些事,沒經歷過,總歸還是遺憾的。在VIVO的遊戲中心,有一個特別炫酷的功能: 這個功能就是使

第一行程式碼 3.4.2 建立定義控制元件 章節中初上手出項的下載完成後閃退問題和定義控制元件無反應問題

關於出項下載後閃退並且開啟app時也閃退的問題,主要是xml檔案出錯,一般情況按照書中的流程title.xml檔案是沒有錯誤的,主要原因在於 activity_main.xm了檔案中,直接說程式碼 <RelativeLayout xmlns:android="http://schema

視錯覺:從一個看似簡單定義控制元件說起

為什麼要寫今天這篇部落格那就說來話長了,那是在一個大雪紛飛的冬天……然後……。好了,不扯淡了,直接進入今天的主題吧,這篇部落格是關於iOS自定義元件的東西。一些UI效果看起來似乎是這個樣子,其實本質不是這個樣子。在做一些UI效果時我們可以利用視錯覺的一些東西,讓使用者看到的是一個東西,其實你實現的又是一個東西

第一行程式碼 3.4.2 建立定義控制元件 章節中初上手出項的下載完成後閃退問題和定義控制元件無反應問題

關於出項下載後閃退並且開啟app時也閃退的問題,主要是xml檔案出錯,一般情況按照書中的流程title.xml檔案是沒有錯誤的,主要原因在於 activity_main.xm了檔案中,直接說程式碼 <RelativeLayout xmlns:android="http

Asp.net 2.0定義控制元件(點選HyperLink後執行事件)[網友問題: DataList裡HyperLink控制元件激發事件,在哪定義?]

 (一). 概述         HyperLink預設沒有Click事件,  重寫了一個HyperLink自定義控制元件.         實現原理:          預設Hyperlink是跳到點選請求的頁面, 本HyperLink自定義控制元件最終也是跳轉到請求的頁面, 但期間        執行

定義控制元件四:一個簡單定義控制元件例項

轉載請標明出處:http://blog.csdn.net/lmj623565791/article/details/24252901 很多的Android入門程式猿來說對於Android自定義View,可能都是比較恐懼的,但是這又是高手進階的必經之路,所有準備在

wheel定義控制元件,實現城市三級聯動,時間選擇的功能簡單使用

                對於Android初學者,最煩的就是學習自定義控制元件。原生的控制元件不是醜就是無法滿足需求,不得以我們只好自己重寫控制元件的方法,下面我就為大家介紹一下某大神自定義的wheel控制元件Android滾輪控制元件,基於ListView實現,可以自定義樣式。,此控制元件可以實現

Android學習之路------定義控制元件,圓形進度條的簡單實現

簡單介紹 主要是通過自定義一個view類,然後通過操作canvas和paint進行效果的實現 Step 1 新建一個attr.xml,這裡主要是為了自定義我們的控制元件屬性,attr開頭的語句表示控制元件的自定義屬性,在這裡為了實現圓形進度條,定義了一

使用者定義控制元件的使用(2)

<%...@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" Inherits="_Default"%><%...@ Register Src="myuc.ascx" TagName="myuc" T

AjaxPanel定義控制元件實現頁面無重新整理資料互動(做了個示例程式, 效果確實比較Cool, 用法非常簡單! )(示例程式碼下載)

(一) . 簡要           AjaxPanel, 一個自定義控制元件, 只要在頁面中將AjaxPanel作為父控制元件, 則它內部的控制元件在執行時無重新整理.            做了個程式試了一下果然比較Cool !  下面介紹一下具體配置, 配置也比較簡單.

Android定義控制元件例項(2)——AndroidTableView,支援行列合併

package com.wjk.androidtableview; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.util.Pair; import com.wjk.tab

定義控制元件2)---ColorMatrixColorFilter色彩矩陣顏色過濾器

可以通過Paint中大量的setter方法來為畫筆設定屬性 setColorFilter(ColorFilter filter) 設定顏色過濾,什麼意思呢?就像拿個篩子把顏色“濾”一遍獲取我們想要的色彩結果,這個方法需要我們傳入一個ColorFilter引數同樣也會返回一個

Android定義控制元件2定義帶下劃線的文字或按鈕、組合使用可切換tab

package com.custom.controls.button; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Color; import android.u

Qt編寫定義控制元件2-進度條標尺

前言 進度條標尺控制元件的應用場景一般是需要手動拉動進度,上面有標尺可以看到當前進度,類似於qslider控制元件,其實就是qsl

[WPF定義控制元件庫]簡單的表單佈局控制元件

1. WPF佈局一個表單 <Grid Width="400" HorizontalAlignment="Center" VerticalAlignment="Center"> <Grid.RowDefinitions&g

Qt定義控制元件之儀表盤1--簡單的貼圖儀表盤

0、前言   學程式首先要輸出hell world,學電子要先來個流水燈。學Qt,那就必須先來個自定義控制元件,若有人問我從哪個下手,我推薦儀表盤,可簡可繁,從低配到高配齊全,可入門也可進階。 1、儀表盤解析 以常見的、傳統的儀表盤為例,分解儀表盤的元素,主要有邊框、刻度、數字、指標(或數