如果本文幫助到你,本人不勝榮幸,如果浪費了你的時間,本人深感抱歉。
希望用最簡單的大白話來幫助那些像我一樣的人。如果有什么錯誤,請一定指出,以免誤導大家、也誤導我。
本文來自:http://www.lxweimin.com/users/320f9e8f7fc9/latest_articles
感謝您的關注。
Android中所有界面都是由 View 和 ViewGroup 組成,
View是 所有基本組件的基類,以View結尾的。
ViewGroup是 拼裝這些組件的容器,以Layout結尾的。
我們這次用的是 ViewGroup
繪制布局由兩個遍歷的過程組成:測量過程 和 布局過程
onMeasure() 完成測量過程:測量所有視圖的寬度和高度
onLayout() 完成布局過程:利用上步計算出的測量信息,布局所有子視圖
我覺得能來搜ViewGroup的應該都基本的知道,這個是用來干嘛的。先來寫下個人的整體理解。
先上總結:
- 在attr創建屬性,在dimens設置默認屬性
- 在CascadeLayout構造器里面獲取設置的默認屬性
- 在 onMeasure 計算每個子視圖的位置和寬度 ————測量過程
3.1 創建 LayoutParams,來儲存每個視圖的 尺寸信息
3.2 添加 LayoutParams 需要的一些方法。- 將算好的子視圖的信息 在onLayout中設置給布局。 ————布局過程
- 然后在布局中就可以使用了。
然后我們來看看效果圖:
<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. 然后在布局中就可以使用了。
現在就可以使用了,跟我們剛開始的時候那個布局一樣。
為子布局添加屬性。
因為上面的大體邏輯已經寫完,添加子布局的屬性也比較簡單。
還是先上總結:
- 在attrs.xml 文件,添加子布局的新屬性
- 在CascadeLayout的內部類LayoutParams 中接受這個屬性
- 在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,歡迎批評指正。