最基礎的自定義ViewGroup

如果本文幫助到你,本人不勝榮幸,如果浪費了你的時間,本人深感抱歉。
希望用最簡單的大白話來幫助那些像我一樣的人。如果有什么錯誤,請一定指出,以免誤導大家、也誤導我。
本文來自:http://www.lxweimin.com/users/320f9e8f7fc9/latest_articles
感謝您的關注。

Android中所有界面都是由 View 和 ViewGroup 組成,
View是 所有基本組件的基類,以View結尾的。
ViewGroup是 拼裝這些組件的容器,以Layout結尾的。

我們這次用的是 ViewGroup
繪制布局由兩個遍歷的過程組成:測量過程布局過程
onMeasure() 完成測量過程:測量所有視圖的寬度和高度
onLayout() 完成布局過程:利用上步計算出的測量信息,布局所有子視圖

我覺得能來搜ViewGroup的應該都基本的知道,這個是用來干嘛的。先來寫下個人的整體理解。

先上總結:

  1. 在attr創建屬性,在dimens設置默認屬性
  2. 在CascadeLayout構造器里面獲取設置的默認屬性
  3. 在 onMeasure 計算每個子視圖的位置和寬度 ————測量過程
    3.1 創建 LayoutParams,來儲存每個視圖的 尺寸信息
    3.2 添加 LayoutParams 需要的一些方法。
  4. 將算好的子視圖的信息 在onLayout中設置給布局。 ————布局過程
  5. 然后在布局中就可以使用了。

然后我們來看看效果圖:

Paste_Image.png
  <com.lyl.viewgroup.view.CascadeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:horizontal_spacing="40dp"
    app:vertical_spacing="40dp" >

    <View
        android:layout_width="100dp"
        android:layout_height="150dp"
        android:background="#FF0000" />

    <View
        android:layout_width="100dp"
        android:layout_height="150dp"
        android:background="#00FF00" />

    <View
        android:layout_width="100dp"
        android:layout_height="150dp"
        android:background="#0000FF" />

    <View
        android:layout_width="100dp"
        android:layout_height="150dp"
        android:background="#FF0000" />
</com.lyl.viewgroup.view.CascadeLayout>

只要在代碼中使用了我們的父布局,在子布局中不用做任何設置,就會按照指定的規則排列。

具體步驟就是上面總結,看起來挺麻煩的,其實是非常簡單。跟著看下去,不想寫代碼直接復制就可以,都是很簡單的代碼。走一遍流程之后,就知道自定義 ViewGroup是個怎么會事了,如果有更好的想法,可以再具體實踐的。

這里說一下那個app:也就是 自定義屬性的前綴。
這個app 為什么會是app 呢。
因為我們用到了自定義的屬性,需要在xml引入我們需要在最頂部加入這個:

 xmlns:app="http://schemas.android.com/apk/res/com.lyl.viewgroup"

而這個app就是由后面的賦值的,當然也可以改成其他的你想要的。

不想看代碼的,可以直接跳到最后看源碼,源碼也非常簡單。注釋也比較詳細。


然后開始寫代碼吧。

1. 在attr創建屬性,在dimens設置默認屬性

首先在res/values的目錄下創建一個attrs.xml文件。文件內容如下:

   <?xml version="1.0" encoding="utf-8"?>
   <resources>
       <declare-styleable name="CascadeLayout">
           <attr name="horizontal_spacing" format="dimension" />
           <attr name="vertical_spacing" format="dimension" />
       </declare-styleable>
   </resources>

attrs.xml文件定義屬性的作用是為了在這里使用??茨莾蓚€app:的屬性,就是為了設置每個子布局距離 左邊 和 上面 的距離:

   <com.lyl.viewgroup.view.CascadeLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      app:horizontal_spacing="40dp"
      app:vertical_spacing="40dp" >

然后在res/values的目錄下創建一個dimens.xml文件,這里的作用是為了設置每個子布局距離左邊和上面的默認距離,也就是上面兩個屬性沒有設置的時候,這個會起作用。

   <?xml version="1.0" encoding="utf-8"?>
   <resources>
       <dimen name="cascade_horizontal_spacing">20dp</dimen>
       <dimen name="cascade_vertical_spacing">20dp</dimen>
   </resources>
2. 在CascadeLayout構造器里面獲取設置的默認屬性

這一步肯定是要先創建一個CascadeLayout類的,繼承ViewGroup,實現方法。然后在構造器里面就可以獲取設置的屬性了,代碼如下,有相應注釋:

public class CascadeLayout extends ViewGroup {

// 子布局位于 左邊 和 上面 的距離
private int mHorizontalSpacing;
private int mVerticalSpacing;

/**
 * 當通過XML文件創建該視圖會調用這個方法
 */
public CascadeLayout(Context context, AttributeSet attrs) {
    super(context, attrs);

    // 從自定義屬性中獲取值,從attrs中
    TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CascadeLayout);

    try {
        // 獲取用戶指定的大小,就是我們剛開始指定的那個40dp
        // 如果沒有指定,就使用默認值
        mHorizontalSpacing = ta.getDimensionPixelSize(R.styleable.CascadeLayout_horizontal_spacing, getResources()
                .getDimensionPixelSize(R.dimen.cascade_horizontal_spacing));
        mVerticalSpacing = ta.getDimensionPixelSize(R.styleable.CascadeLayout_vertical_spacing, getResources()
                .getDimensionPixelSize(R.dimen.cascade_vertical_spacing));

    } finally {
        ta.recycle();
    }
}
3. 在 onMeasure 計算每個子視圖的位置和寬度 ————測量過程

在編寫onMeasure()方法之前,我們要先創建LayoutParams類,這個類用來保存每個子視圖的x,y軸的位置。一般都是把這個類直接定義為內部類。代碼如下:

3.1 創建 LayoutParams,來儲存每個視圖的 尺寸信息
/** 保存每個子視圖的x、y軸的位置 **/
public static class LayoutParams extends ViewGroup.LayoutParams {
    int x;
    int y;

    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
    }

    public LayoutParams(int width, int height) {
        super(width, height);
    }
}
3.2 添加 LayoutParams 需要的一些方法。

要使用LayoutParams,還需要在我們的CascadeLayout類中重寫以下方法。這些方法在不同的Viewgroup通常都是相同的,可參考 LinearLayout 源碼。

@Override
protected boolean checkLayoutParams(android.view.ViewGroup.LayoutParams p) {
    return p instanceof LayoutParams;
}

@Override
protected LayoutParams generateDefaultLayoutParams() {
    return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new LayoutParams(getContext(), attrs);
}

@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
    return new LayoutParams(p.width, p.height);
}
3.3 編寫onMeasure()方法。這是這個類的核心代碼

注釋的很清楚。

/**
 * 測量所有視圖的寬度和高度
 */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // !!!在編寫onMeasure方法之前,先創建自定義LayoutParams類,保存每個子視圖的x、y軸的位置。

    // 使用寬和高 計算布局的最終大小,以及子視圖的x、y軸的位置
    int width = getPaddingLeft();
    int height = getPaddingTop();

    // 1. 獲取子視圖的數量
    final int count = getChildCount();
    for (int i = 0; i < count; i++) {

        View child = getChildAt(i);

        // 2. 讓 每個子視圖 測量自己
        measureChild(child, widthMeasureSpec, heightMeasureSpec);

        LayoutParams lp = (LayoutParams) child.getLayoutParams();

        // 3. 在LayoutParams中保存每個子視圖的x、y坐標,以便在onLayout()中布局
        lp.x = width;
        width += mHorizontalSpacing;

        lp.y = height;
        height += mVerticalSpacing;
    }

    // 4. 計算整體的寬、高
    width += getChildAt(getChildCount() - 1).getMeasuredWidth() + getPaddingRight();
    height += getChildAt(getChildCount() - 1).getMeasuredHeight() + getPaddingBottom();

    // 5. 使用計算的 寬、高 設置整個布局的測量尺寸
    setMeasuredDimension(resolveSize(width, widthMeasureSpec), resolveSize(height, heightMeasureSpec));
}
4. 將算好的子視圖的信息 在onLayout中設置給布局。 ————布局過程
/**
 * 利用上步計算出的測量信息,布局所有子視圖
 */
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int count = getChildCount();
    for (int i = 0; i < count; i++) {
        View child = getChildAt(i);
        LayoutParams lp = (LayoutParams) child.getLayoutParams();
        child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y + child.getMeasuredHeight());
    }
}

到這里就可以結束了。

5. 然后在布局中就可以使用了。

現在就可以使用了,跟我們剛開始的時候那個布局一樣。


為子布局添加屬性。

因為上面的大體邏輯已經寫完,添加子布局的屬性也比較簡單。
還是先上總結:

  1. 在attrs.xml 文件,添加子布局的新屬性
  2. 在CascadeLayout的內部類LayoutParams 中接受這個屬性
  3. 在onMeasure()使用這個屬性

來寫代碼

1. 在attrs.xml 文件,添加子布局的新屬性
<declare-styleable name="CascadeLayout_LayoutParams">
    <attr name="layout_horizontal_spacing" format="dimension" />
    <attr name="layout_vertical_spacing" format="dimension" />
</declare-styleable>
2. 在CascadeLayout的內部類LayoutParams 中接受這個屬性

修改之后LayoutParams 類的代碼如下,同樣注釋寫的很清楚:

/** 保存每個子視圖的x、y軸的位置 **/
public static class LayoutParams extends ViewGroup.LayoutParams {
    int x;
    int y;
    // 子布局距離 左邊 和 上面的距離
    public int verticalSpacing;
    public int horizontalSpacing;

    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);

        // 1. 從自定義屬性中獲取值
        TypedArray ta = c.obtainStyledAttributes(attrs, R.styleable.CascadeLayout_LayoutParams);

        // 2.將獲取的值拿到,以便onMeasure()使用
        try {
            horizontalSpacing = ta.getDimensionPixelSize(
                    R.styleable.CascadeLayout_LayoutParams_layout_horizontal_spacing, -1);
            verticalSpacing = ta.getDimensionPixelSize(
                    R.styleable.CascadeLayout_LayoutParams_layout_vertical_spacing, -1);
        } finally {
            ta.recycle();
        }
    }

    public LayoutParams(int width, int height) {
        super(width, height);
    }
}
3. 在onMeasure()使用這個屬性

在onMeasure()中修改了 子布局 x、y的位置代碼。其余的沒有變

    if (lp.horizontalSpacing >= 0) {
        lp.x = width + lp.horizontalSpacing;
        width += mHorizontalSpacing + lp.horizontalSpacing;
    } else {
        lp.x = width;
        width += mHorizontalSpacing;
    }

    if (lp.verticalSpacing >= 0) {
        lp.y = height + lp.verticalSpacing;
        height += mVerticalSpacing + lp.verticalSpacing;
    } else {
        lp.y = height;
        height += mVerticalSpacing;
    }

然后就可以在子視圖中添加屬性了:

app:layout_horizontal_spacing="40dp"
app:layout_vertical_spacing="40dp"

至此,就結束了。


最后項目源碼:
https://github.com/Wing-Li/PracticeDemos
歡迎Star,歡迎批評指正。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容