Android自定義View深度解析(四、自定義ViewGroup打造自己的佈局容器)

版權聲明:本文爲openXu原創文章【openXu的博客】,未經博主允許不得以任何形式轉載

  通過前面幾篇博客,我們能夠自定義出一些比較簡單的自定義控件,但是這在實際應用中是遠遠不夠的,爲了實現一些比較牛X的效果,比如側滑菜單、滑動卡片等等,我們還需要了解自定義ViewGroup。官方文檔中對ViewGroup這樣描述的:

ViewGroup是一種可以包含其他視圖的特殊視圖,他是各種佈局和所有容器的基類,這些類也定義了ViewGroup.LayoutParams類作爲類的佈局參數。

  之前,我們只是學習過自定義View,其實自定義ViewGroup和自定義View的步驟差不了多少,他們的的區別主要來自各自的作用不同,ViewGroup是容器,用來包含其他控件,而View是真正意義上看得見摸得着的,它需要將自己畫出來。ViewGroup需要重寫onMeasure()方法測量子控件的寬高和自己的寬高,然後實現onLayout()方法擺放子控件。而 View則是需要重寫onMeasure()根據測量模式和父控件給出的建議的寬高值計算自己的寬高,然後再父控件爲其指定的區域繪製自己的圖形。
  
根據以往經驗我們初步將自定義ViewGroup的步驟定爲下面幾步:

  1. 繼承ViewGroup,覆蓋構造方法
  2. 重寫onMeasure()方法測量子控件和自身寬高
  3. 實現onLayout()方法擺放子控件

1. 簡單實現水平排列效果

我們先自定義一個ViewGroup作爲佈局容器,實現一個從左往右水平排列(排滿換行)的效果:

/**
 * 自定義佈局管理器的示例。
 */
public class CustomLayout extends ViewGroup {
      private static final String TAG = "CustomLayout";

    public CustomLayout(Context context) {
        super(context);
    }

    public CustomLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    /**
     * 要求所有的孩子測量自己的大小,然後根據這些孩子的大小完成自己的尺寸測量
     */
    @SuppressLint("NewApi") @Override
    protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) {
        // 計算出所有的childView的寬和高 
        measureChildren(widthMeasureSpec, heightMeasureSpec); 
        //測量並保存layout的寬高(使用getDefaultSize時,wrap_content和match_perent都是填充屏幕)
        //稍後會重新寫這個方法,能達到wrap_content的效果
        setMeasuredDimension( getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

    /**
     * 爲所有的子控件擺放位置.
     */
    @Override
    protected void onLayout( boolean changed, int left, int top, int right, int bottom) {
        final int count = getChildCount();
        int childMeasureWidth = 0;
        int childMeasureHeight = 0;
        int layoutWidth = 0;    // 容器已經佔據的寬度
        int layoutHeight = 0;   // 容器已經佔據的寬度
        int maxChildHeight = 0; //一行中子控件最高的高度,用於決定下一行高度應該在目前基礎上累加多少
        for(int i = 0; i<count; i++){
            View child = getChildAt(i);
             //注意此處不能使用getWidth和getHeight,這兩個方法必須在onLayout執行完,才能正確獲取寬高
            childMeasureWidth = child.getMeasuredWidth(); 
            childMeasureHeight = child.getMeasuredHeight(); 
            if(layoutWidth<getWidth()){
                   //如果一行沒有排滿,繼續往右排列
                  left = layoutWidth;
                  right = left+childMeasureWidth;
                  top = layoutHeight;
                  bottom = top+childMeasureHeight;
            } else{
                   //排滿後換行
                  layoutWidth = 0;
                  layoutHeight += maxChildHeight;
                  maxChildHeight = 0;
                  
                  left = layoutWidth;
                  right = left+childMeasureWidth;
                  top = layoutHeight;
                  bottom = top+childMeasureHeight;
            }

            layoutWidth += childMeasureWidth;  //寬度累加
             if(childMeasureHeight>maxChildHeight){
                  maxChildHeight = childMeasureHeight;
            }
                  
             //確定子控件的位置,四個參數分別代表(左上右下)點的座標值
            child.layout(left, top, right, bottom);
        }
    }
}

佈局文件:

<?xml version="1.0" encoding= "utf-8"?>
<com.openxu.costomlayout.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width= "wrap_content"
    android:layout_height= "wrap_content" >
    <Button
        android:layout_width= "wrap_content"
        android:layout_height= "wrap_content"
        android:background= "#FF8247"
        android:textColor= "#ffffff"
        android:textSize="20dip"
        android:padding= "20dip"
        android:text="按鈕1" />
    <Button
        android:layout_width= "wrap_content"
        android:layout_height= "wrap_content"
        android:background= "#8B0A50"
        android:textColor= "#ffffff"
        android:textSize="20dip"
        android:padding= "10dip"
        android:text="按鈕2222222222222" />
    <Button
        android:layout_width= "wrap_content"
        android:layout_height= "wrap_content"
        android:background= "#7CFC00"
        android:textColor= "#ffffff"
        android:textSize="20dip"
        android:padding= "15dip"
        android:text="按鈕333333" />
    <Button
        android:layout_width= "wrap_content"
        android:layout_height= "wrap_content"
        android:background= "#1E90FF"
        android:textColor= "#ffffff"
        android:textSize="10dip"
        android:padding= "10dip"
        android:text="按鈕4" />
    <Button
        android:layout_width= "wrap_content"
        android:layout_height= "wrap_content"
        android:background= "#191970"
        android:textColor= "#ffffff"
        android:textSize="20dip"
        android:padding= "15dip"
        android:text="按鈕5" />
    <Button
        android:layout_width= "wrap_content"
        android:layout_height= "wrap_content"
        android:background= "#7A67EE"
        android:textColor= "#ffffff"
        android:textSize="20dip"
        android:padding= "20dip"
        android:text="按鈕6" />

</com.openxu.costomlayout.CustomLayout>

運行效果:

這裏寫圖片描述

運行成功,是不是略有成就感?這個佈局就是簡單版的LinearLayout設置android:orientation ="horizontal"的效果,比他還牛X一點,還能自動換行(哈哈)。接下來我們要實現一個功能,只需要在佈局文件中指定佈局屬性,就能控制子控件在什麼位置(類似相對佈局RelativeLayout)。

2. 自定義LayoutParams

回想一下我們平時使用RelativeLayout的時候,在佈局文件中使用android:layout_alignParentRight="true"android:layout_centerInParent="true"等各種屬性,就能控制子控件顯示在父控件的上下左右、居中等效果。 在上一篇講onMeasure的博客中,我們有了解過ViewGroup.LayoutParams類,ViewGroup中有兩個內部類ViewGroup.LayoutParamsViewGroup.MarginLayoutParamsMarginLayoutParams繼承自LayoutParams,這兩個內部類就是ViewGroup的佈局參數類,比如我們在LinearLayout等佈局中使用的layout_width\layout_hight等以“layout_ ”開頭的屬性都是佈局屬性。

在View中有一個mLayoutParams的變量用來保存這個View的所有佈局屬性。ViewGroup.LayoutParams有兩個屬性layout_widthlayout_height,因爲所有的容器都需要設置子控件的寬高,所以這個LayoutParams是所有佈局參數的基類,如果需要擴展其他屬性,都應該繼承自它。比如RelativeLayout中就提供了它自己的佈局參數類RelativeLayout.LayoutParams,並擴展了很多佈局參數,我們平時在RelativeLayout中使用的佈局屬性都來自它 :

<declare-styleable name= "RelativeLayout_Layout">
        <attr name ="layout_toLeftOf" format= "reference" />
        <attr name ="layout_toRightOf" format= "reference" />
        <attr name ="layout_above" format="reference" />
        <attr name ="layout_below" format="reference" />
        <attr name ="layout_alignBaseline" format= "reference" />
        <attr name ="layout_alignLeft" format= "reference" />
        <attr name ="layout_alignTop" format= "reference" />
        <attr name ="layout_alignRight" format= "reference" />
        <attr name ="layout_alignBottom" format= "reference" />
        <attr name ="layout_alignParentLeft" format= "boolean" />
        <attr name ="layout_alignParentTop" format= "boolean" />
        <attr name ="layout_alignParentRight" format= "boolean" />
        <attr name ="layout_alignParentBottom" format= "boolean" />
        <attr name ="layout_centerInParent" format= "boolean" />
        <attr name ="layout_centerVertical" format= "boolean" />
        <attr name ="layout_alignWithParentIfMissing" format= "boolean" />
        <attr name ="layout_toStartOf" format= "reference" />
        <attr name ="layout_toEndOf" format="reference" />
        <attr name ="layout_alignStart" format= "reference" />
        <attr name ="layout_alignEnd" format= "reference" />
        <attr name ="layout_alignParentStart" format= "boolean" />
        <attr name ="layout_alignParentEnd" format= "boolean" />
    </declare-styleable >

看了上面的介紹,我們大概知道怎麼爲我們的佈局容器定義自己的佈局屬性了吧,就不繞彎子了,按照下面的步驟做:

①. 大致明確佈局容器的需求,初步定義佈局屬性

在定義屬性之前要弄清楚,我們自定義的佈局容器需要滿足那些需求,需要哪些屬性,比如,我們現在要實現像相對佈局一樣,爲子控件設置一個位置屬性layout_position="",來控制子控件在佈局中顯示的位置。暫定位置有五種:左上、左下、右上、右下、居中。有了需求,我們就在attr.xml定義自己的佈局屬性(和之前講的自定義屬性一樣的操作,不太瞭解的可以翻閱《Android自定義View(二、深入解析自定義屬性)》

<?xml version="1.0" encoding= "utf-8"?>
<resources> 
    <declare-styleable name ="CustomLayout">
    <attr name ="layout_position">
        <enum name ="center" value="0" />
        <enum name ="left" value="1" />
        <enum name ="right" value="2" />
        <enum name ="bottom" value="3" />
        <enum name ="rightAndBottom" value="4" />
    </attr >
    </declare-styleable>
</resources>

left就代表是左上(按常理默認就是左上方開始,就不用寫leftTop了,簡潔一點),bottom左下,right 右上,rightAndBottom右下,center居中。屬性類型是枚舉,同時只能設置一個值。

②. 繼承LayoutParams,定義佈局參數類

我們可以選擇繼承ViewGroup.LayoutParams,這樣的話我們的佈局只是簡單的支持layout_widthlayout_height;也可以繼承MarginLayoutParams,就能使用layout_marginxxx屬性了。因爲後面我們還要用到margin屬性,所以這裏方便起見就直接繼承MarginLayoutParams了。

覆蓋構造方法,然後在有AttributeSet參數的構造方法中初始化參數值,這個構造方法纔是佈局文件被映射爲對象的時候被調用的。

public static class CustomLayoutParams extends MarginLayoutParams {
       public static final int POSITION_MIDDLE = 0; // 中間
       public static final int POSITION_LEFT = 1; // 左上方
       public static final int POSITION_RIGHT = 2; // 右上方
       public static final int POSITION_BOTTOM = 3; // 左下角
       public static final int POSITION_RIGHTANDBOTTOM = 4; // 右下角

       public int position = POSITION_LEFT;  // 默認我們的位置就是左上角

       public CustomLayoutParams(Context c, AttributeSet attrs) {
             super(c, attrs);
            TypedArray a = c.obtainStyledAttributes(attrs,R.styleable.CustomLayout );
             //獲取設置在子控件上的位置屬性
             position = a.getInt(R.styleable.CustomLayout_layout_position ,position );

            a.recycle();
      }

       public CustomLayoutParams( int width, int height) {
             super(width, height);
      }

       public CustomLayoutParams(ViewGroup.LayoutParams source) {
             super(source);
      }

}

③. 重寫generateLayoutParams()

ViewGroup中有下面幾個關於LayoutParams的方法,generateLayoutParams (AttributeSet attrs)是在佈局文件被填充爲對象的時候調用的,這個方法是下面幾個方法中最重要的,如果不重寫它,我麼佈局文件中設置的佈局參數都不能拿到。後面我也會專門寫一篇博客來介紹佈局文件被添加到activity窗口的過程,裏面會講到這個方法被調用的來龍去脈。其他幾個方法我們最好也能重寫一下,將裏面的LayoutParams換成我們自定義的CustomLayoutParams類,避免以後會遇到佈局參數類型轉換異常。

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new CustomLayoutParams(getContext(), attrs);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
    return new CustomLayoutParams (p);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
    return new CustomLayoutParams (LayoutParams.MATCH_PARENT , LayoutParams.MATCH_PARENT);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    return p instanceof CustomLayoutParams ;
}

④. 在佈局文件中使用佈局屬性

注意引入命名空間xmlns:openxu= "http://schemas.android.com/apk/res/包名"

<?xml version="1.0" encoding= "utf-8"?>
<com.openxu.costomlayout.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:openxu= "http://schemas.android.com/apk/res/com.openxu.costomlayout"
    android:background="#33000000"
    android:layout_width= "match_parent "
    android:layout_height= "match_parent" >

    <Button
        android:layout_width= "wrap_content"
        android:layout_height= "wrap_content"
        openxu:layout_position= "left"
        android:background= "#FF8247"
        android:textColor= "#ffffff"
         android:textSize="20dip"
        android:padding= "20dip"
        android:text= "按鈕1" />

    <Button
        android:layout_width= "wrap_content"
        android:layout_height= "wrap_content"
        openxu:layout_position= "right"
        android:background= "#8B0A50"
        android:textColor= "#ffffff"
        android:textSize= "18dip"
        android:padding= "10dip"
        android:text= "按鈕2222222222222" />

    <Button
        android:layout_width= "wrap_content"
        android:layout_height= "wrap_content"
        openxu:layout_position= "bottom"
        android:background= "#7CFC00"
        android:textColor= "#ffffff"
        android:textSize= "20dip"
        android:padding= "15dip"
        android:text= "按鈕333333" />

    <Button
        android:layout_width= "wrap_content"
        android:layout_height= "wrap_content"
        openxu:layout_position= "rightAndBottom"
        android:background= "#1E90FF"
        android:textColor= "#ffffff"
        android:textSize= "15dip"
        android:padding= "10dip"
        android:text= "按鈕4" />

    <Button
        android:layout_width= "wrap_content"
        android:layout_height= "wrap_content"
        openxu:layout_position= "center"
        android:background= "#191970"
        android:textColor= "#ffffff"
        android:textSize= "20dip"
        android:padding= "15dip"
        android:text= "按鈕5" />

</com.openxu.costomlayout.CustomLayout>

⑤. 在onMeasure()和onLayout()中使用佈局參數

經過上面幾步之後,我們運行程序,就能獲取子控件的佈局參數了,在onMeasure()方法和onLayout()方法中,我們按照自定義佈局容器的特殊需求,對寬度和位置坐特殊處理。這裏我們需要注意一下,如果佈局容器被設置爲包裹類容,我們只需要保證能將最大的子控件包裹住就ok,代碼註釋比較詳細,就不多說了。

 @Override
protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) { 
  //獲得此ViewGroup上級容器爲其推薦的寬和高,以及計算模式  
 int widthMode = MeasureSpec. getMode(widthMeasureSpec); 
 int heightMode = MeasureSpec. getMode(heightMeasureSpec); 
 int sizeWidth = MeasureSpec. getSize(widthMeasureSpec); 
 int sizeHeight = MeasureSpec. getSize(heightMeasureSpec); 
 int layoutWidth = 0;
 int layoutHeight = 0;
      // 計算出所有的childView的寬和高
     measureChildren(widthMeasureSpec, heightMeasureSpec);
     
      int cWidth = 0;
      int cHeight = 0;
      int count = getChildCount(); 
     
      if(widthMode == MeasureSpec. EXACTLY){
            //如果佈局容器的寬度模式是確定的(具體的size或者match_parent),直接使用父窗體建議的寬度
           layoutWidth = sizeWidth;
     } else{
            //如果是未指定或者wrap_content,我們都按照包裹內容做,寬度方向上只需要拿到所有子控件中寬度做大的作爲佈局寬度
            for ( int i = 0; i < count; i++)  { 
                  View child = getChildAt(i); 
              cWidth = child.getMeasuredWidth(); 
              //獲取子控件最大寬度
              layoutWidth = cWidth > layoutWidth ? cWidth : layoutWidth;
           }
     }
      //高度很寬度處理思想一樣
      if(heightMode == MeasureSpec. EXACTLY){
           layoutHeight = sizeHeight;
     } else{
            for ( int i = 0; i < count; i++)  { 
                  View child = getChildAt(i); 
                  cHeight = child.getMeasuredHeight();
                  layoutHeight = cHeight > layoutHeight ? cHeight : layoutHeight;
           }
     }
     
      // 測量並保存layout的寬高
     setMeasuredDimension(layoutWidth, layoutHeight);
}

@Override
protected void onLayout( boolean changed, int left, int top, int right,
            int bottom) {
      final int count = getChildCount();
      int childMeasureWidth = 0;
      int childMeasureHeight = 0;
     CustomLayoutParams params = null;
      for ( int i = 0; i < count; i++) {
           View child = getChildAt(i);
            // 注意此處不能使用getWidth和getHeight,這兩個方法必須在onLayout執行完,才能正確獲取寬高
           childMeasureWidth = child.getMeasuredWidth();
           childMeasureHeight = child.getMeasuredHeight();

           params = (CustomLayoutParams) child.getLayoutParams(); 
     switch (params. position) {
            case CustomLayoutParams. POSITION_MIDDLE:    // 中間
                 left = (getWidth()-childMeasureWidth)/2;
                 top = (getHeight()-childMeasureHeight)/2;
                  break;
            case CustomLayoutParams. POSITION_LEFT:      // 左上方
                 left = 0;
                 top = 0;
                  break;
            case CustomLayoutParams. POSITION_RIGHT:     // 右上方
                 left = getWidth()-childMeasureWidth;
                 top = 0;
                  break;
            case CustomLayoutParams. POSITION_BOTTOM:    // 左下角
                 left = 0;
                 top = getHeight()-childMeasureHeight;
                  break;
            case CustomLayoutParams. POSITION_RIGHTANDBOTTOM:// 右下角
                 left = getWidth()-childMeasureWidth;
                 top = getHeight()-childMeasureHeight;
                  break;
            default:
                  break;
           }
    
            // 確定子控件的位置,四個參數分別代表(左上右下)點的座標值
           child.layout(left, top, left+childMeasureWidth, top+childMeasureHeight);
     }
}

運行效果:

下面幾個效果分別對應佈局容器寬高設置不同的屬性的情況(設置match_parent 、設置200dip、設置):

這裏寫圖片描述 這裏寫圖片描述 這裏寫圖片描述

從運行結果看,我們自定義的佈局容器在各種寬高設置下都能很好的測量大小和擺放子控件。現在我們讓他支持margin屬性

3. 支持layout_margin屬性

如果我們自定義的佈局參數類繼承自MarginLayoutParams,就自動支持了layout_margin屬性了,我們需要做的就是直接在佈局文件中使用layout_margin屬性,然後再onMeasure()onLayout()中使用margin屬性值測量和擺放子控件。需要注意的是我們測量子控件的時候應該調用measureChildWithMargin()方法。

佈局文件:

<?xml version="1.0" encoding= "utf-8"?>
<com.openxu.costomlayout.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:openxu= "http://schemas.android.com/apk/res/com.openxu.costomlayout"
    android:background="#33000000"
    android:layout_width= "match_parent"
    android:layout_height= "match_parent" >
    <Button
        android:layout_width= "wrap_content"
        android:layout_height= "wrap_content"
        openxu:layout_position= "left"
        android:layout_marginLeft = "20dip"
        android:background= "#FF8247"
        android:textColor= "#ffffff"
        android:textSize="20dip"
        android:padding= "20dip"
        android:text="按鈕1" />
    <Button
        android:layout_width= "wrap_content"
        android:layout_height= "wrap_content"
        android:layout_marginTop = "30dip"
        openxu:layout_position= "right"
        android:background= "#8B0A50"
        android:textColor= "#ffffff"
        android:textSize="18dip"
        android:padding= "10dip"
        android:text="按鈕2222222222222" />
    <Button
        android:layout_width= "wrap_content"
        android:layout_height= "wrap_content"
        android:layout_marginLeft = "30dip"
        android:layout_marginBottom = "10dip"
        openxu:layout_position= "bottom"
        android:background= "#7CFC00"
        android:textColor= "#ffffff"
        android:textSize="20dip"
        android:padding= "15dip"
        android:text="按鈕333333" />
    <Button
        android:layout_width= "wrap_content"
        android:layout_height= "wrap_content"
        openxu:layout_position= "rightAndBottom"
        android:layout_marginBottom = "30dip"
        android:background= "#1E90FF"
        android:textColor= "#ffffff"
        android:textSize="15dip"
        android:padding= "10dip"
        android:text="按鈕4" />
    <Button
        android:layout_width= "wrap_content"
        android:layout_height= "wrap_content"
        openxu:layout_position= "center"
        android:layout_marginBottom = "30dip"
        android:layout_marginRight = "30dip"
        android:background= "#191970"
        android:textColor= "#ffffff"
        android:textSize="20dip"
        android:padding= "15dip"
        android:text="按鈕5" />

</com.openxu.costomlayout.CustomLayout>

onMeasure()和onLayout():

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
   // 獲得此ViewGroup上級容器爲其推薦的寬和高,以及計算模式   
  int widthMode = MeasureSpec. getMode(widthMeasureSpec); 
  int heightMode = MeasureSpec. getMode(heightMeasureSpec); 
  int sizeWidth = MeasureSpec. getSize(widthMeasureSpec); 
  int sizeHeight = MeasureSpec. getSize(heightMeasureSpec); 
  int layoutWidth = 0;
  int layoutHeight = 0;
       int cWidth = 0;
       int cHeight = 0;
       int count = getChildCount(); 

       // 計算出所有的childView的寬和高
       for( int i = 0; i < count; i++){
            View child = getChildAt(i); 
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
      }
      CustomLayoutParams params = null;
       if(widthMode == MeasureSpec. EXACTLY){
             //如果佈局容器的寬度模式時確定的(具體的size或者match_parent)
            layoutWidth = sizeWidth;
      } else{
             //如果是未指定或者wrap_content,我們都按照包裹內容做,寬度方向上只需要拿到所有子控件中寬度做大的作爲佈局寬度
             for ( int i = 0; i < count; i++)  { 
                   View child = getChildAt(i); 
               cWidth = child.getMeasuredWidth(); 
               params = (CustomLayoutParams) child.getLayoutParams(); 
               //獲取子控件寬度和左右邊距之和,作爲這個控件需要佔據的寬度
               int marginWidth = cWidth+params.leftMargin+params.rightMargin ;
               layoutWidth = marginWidth > layoutWidth ? marginWidth : layoutWidth;
            }
      }
       //高度很寬度處理思想一樣
       if(heightMode == MeasureSpec. EXACTLY){
            layoutHeight = sizeHeight;
      } else{
             for ( int i = 0; i < count; i++)  { 
                   View child = getChildAt(i); 
                   cHeight = child.getMeasuredHeight();
                   params = (CustomLayoutParams) child.getLayoutParams(); 
                   int marginHeight = cHeight+params.topMargin+params.bottomMargin ;
                   layoutHeight = marginHeight > layoutHeight ? marginHeight : layoutHeight;
            }
      }
      
       // 測量並保存layout的寬高
      setMeasuredDimension(layoutWidth, layoutHeight);
}

@Override
protected void onLayout( boolean changed, int left, int top, int right,
             int bottom) {
       final int count = getChildCount();
       int childMeasureWidth = 0;
       int childMeasureHeight = 0;
      CustomLayoutParams params = null;
       for ( int i = 0; i < count; i++) {
            View child = getChildAt(i);
             // 注意此處不能使用getWidth和getHeight,這兩個方法必須在onLayout執行完,才能正確獲取寬高
            childMeasureWidth = child.getMeasuredWidth();
            childMeasureHeight = child.getMeasuredHeight();
            params = (CustomLayoutParams) child.getLayoutParams(); 
      switch (params. position) {
             case CustomLayoutParams. POSITION_MIDDLE:    // 中間
                  left = (getWidth()-childMeasureWidth)/2 - params.rightMargin + params.leftMargin ;
                  top = (getHeight()-childMeasureHeight)/2 + params.topMargin - params.bottomMargin ;
                   break;
             case CustomLayoutParams. POSITION_LEFT:      // 左上方
                  left = 0 + params. leftMargin;
                  top = 0 + params. topMargin;
                   break;
             case CustomLayoutParams. POSITION_RIGHT:     // 右上方
                  left = getWidth()-childMeasureWidth - params.rightMargin;
                  top = 0 + params. topMargin;
                   break;
             case CustomLayoutParams. POSITION_BOTTOM:    // 左下角
                  left = 0 + params. leftMargin;
                  top = getHeight()-childMeasureHeight-params.bottomMargin ;
                   break;
             case CustomLayoutParams. POSITION_RIGHTANDBOTTOM:// 右下角
                  left = getWidth()-childMeasureWidth - params.rightMargin;
                  top = getHeight()-childMeasureHeight-params.bottomMargin ;
                   break;
             default:
                   break;
            }
     
             // 確定子控件的位置,四個參數分別代表(左上右下)點的座標值
            child.layout(left, top, left+childMeasureWidth, top+childMeasureHeight);
      }
      
}

運行效果:

這裏寫圖片描述

好了,就寫到這裏,如果想嘗試設置其他屬性,比如above、below等,感興趣的同學可以嘗試一下哦~。其實也沒什麼難的,無非就是如果佈局屬性定義的多,那麼在onMeasure和onLayout中考慮的問題就更多更復雜,自定義佈局容器就是根據自己的需求,讓容器滿足我們特殊的擺放要求。

總結一下今天學習的內容,這篇博客主要學習了兩個知識點:

自定義ViewGroup的步驟:

  1. 繼承ViewGroup,覆蓋構造方法
  2. 重寫onMeasure()方法測量子控件和自身寬高
  3. 實現onLayout()方法擺放子控件

爲佈局容器自定義佈局屬性:

  1. 大致明確佈局容器的需求,初步定義佈局屬性
  2. 繼承LayoutParams,定義佈局參數類
  3. 重寫獲取佈局參數的方法
  4. 在佈局文件中使用佈局屬性
  5. onMeasure()onLayout()中使用佈局參數

各位童鞋有什麼疑問或者建議歡迎留言,有你的支持讓我們共同進步,如果覺得寫的不錯,那就頂我一下吧·

源碼下載

https://github.com/openXu/View-CustomLayout

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