天天看點

Android自定義View深度解析(四、自定義ViewGroup打造自己的布局容器)1. 簡單實作水準排列效果2. 自定義LayoutParams3. 支援layout_margin屬性

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

文章目錄

  • 1. 簡單實作水準排列效果
  • 2. 自定義LayoutParams
    • ①. 大緻明确布局容器的需求,初步定義布局屬性
    • ②. 繼承LayoutParams,定義布局參數類
    • ③. 重寫generateLayoutParams()
    • ④. 在布局檔案中使用布局屬性
    • ⑤. 在onMeasure()和onLayout()中使用布局參數
  • 3. 支援layout_margin屬性

  通過前面幾篇部落格,我們能夠自定義出一些比較簡單的自定義控件,但是這在實際應用中是遠遠不夠的,為了實作一些比較牛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>
           

運作效果:

Android自定義View深度解析(四、自定義ViewGroup打造自己的布局容器)1. 簡單實作水準排列效果2. 自定義LayoutParams3. 支援layout_margin屬性

運作成功,是不是略有成就感?這個布局就是簡單版的

LinearLayout

設定

android:orientation ="horizontal"

的效果,比他還牛X一點,還能自動換行(哈哈)。接下來我們要實作一個功能,隻需要在布局檔案中指定布局屬性,就能控制子控件在什麼位置(類似相對布局

RelativeLayout

)。

2. 自定義LayoutParams

回想一下我們平時使用

RelativeLayout

的時候,在布局檔案中使用

android:layout_alignParentRight="true"

android:layout_centerInParent="true"

等各種屬性,就能控制子控件顯示在父控件的上下左右、居中等效果。 在上一篇講onMeasure的部落格中,我們有了解過

ViewGroup.LayoutParams

類,ViewGroup中有兩個内部類

ViewGroup.LayoutParams

ViewGroup.MarginLayoutParams

MarginLayoutParams

繼承自

LayoutParams

,這兩個内部類就是

ViewGroup

的布局參數類,比如我們在

LinearLayout

等布局中使用的

layout_width\layout_hight

等以“layout_ ”開頭的屬性都是布局屬性。

在View中有一個

mLayoutParams

的變量用來儲存這個View的所有布局屬性。

ViewGroup.LayoutParams

有兩個屬性

layout_width

layout_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_width

layout_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、設定):

Android自定義View深度解析(四、自定義ViewGroup打造自己的布局容器)1. 簡單實作水準排列效果2. 自定義LayoutParams3. 支援layout_margin屬性

 

Android自定義View深度解析(四、自定義ViewGroup打造自己的布局容器)1. 簡單實作水準排列效果2. 自定義LayoutParams3. 支援layout_margin屬性

 

Android自定義View深度解析(四、自定義ViewGroup打造自己的布局容器)1. 簡單實作水準排列效果2. 自定義LayoutParams3. 支援layout_margin屬性

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

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);
      }
      
}
           

運作效果:

Android自定義View深度解析(四、自定義ViewGroup打造自己的布局容器)1. 簡單實作水準排列效果2. 自定義LayoutParams3. 支援layout_margin屬性

好了,就寫到這裡,如果想嘗試設定其他屬性,比如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