現在我們需要做一個撲克牌排列的佈局,如下圖:
可能最容易想到的佈局方式就是使用relativelayout來完成,然後對其margin進行調整。但是這樣一來,佈局將顯得非常繁瑣。想想如果是一套撲克牌,54張呢?那得計算多少次啊!
這裏就引出了本篇文章的主題,自定義ViewGroup,其實是有自定義的ViewGroup完全可以實現上面的功能,且可以對各個子View(即每張撲克牌)進行統一管理。
在實現自定義ViewGroup之前,我們先要了解一下其原理:
繪製佈局由兩個遍歷組成,測量過程和佈局過程,測量過程由measure函數完成,該方法會從上而下的遍歷視圖樹,在遞歸遍歷的過程中,每個視圖都會向下傳遞尺寸和規格,當遍歷完成,每個視圖都保存了各自的尺寸;佈局過程則由layout函數完成,該方法也會至上而下遍歷,在遍歷過程中,每個父視圖通過測量過程的結果定位所有子視圖的位置信息。
在自定義ViewGroup過程中,這兩個過程分別在onMeasure和onLayout中完成。
下面來看代碼,代碼是最好的老師:
首先是佈局文件
//定義命名空間,後面是程序的包名
xmlns:daven="http://schemas.android.com/apk/res/com.example.hello"
<com.example.hello.CascadeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="20dp"
daven:horizontal_spacing="20dp"
daven:vertical_spacing="30dp">
<View
android:layout_width="100dp"
android:layout_height="130dp"
daven:layout_vertical_spacing="50dp"
android:background="@drawable/poker_39"/>
<View
android:layout_width="100dp"
android:layout_height="130dp"
android:background="@drawable/poker_40"/>
<View
android:layout_width="100dp"
android:layout_height="130dp"
android:background="@drawable/poker_48"/>
</com.example.hello.CascadeLayout>
自定義屬性,首先需要在attr.xml中什麼屬性:
<declare-styleable name="CascadeLayout">
<attr name="horizontal_spacing" format="dimension"/>
<attr name="vertical_spacing" format="dimension"/>
</declare-styleable>
<declare-styleable name="CascadeLayout_LayoutParams">
<attr name="layout_vertical_spacing" format="dimension"/>
</declare-styleable>
這些自定義屬性可以在自定義ViewGroup的構造函數中通過context.obtainStyledAttributes(attrs, R.styleable.CascadeLayout)來獲取。
然後下面就是完整的自定義ViewGroup的過程,這裏我們當然是要繼承ViewGroup來完成,我們將其命名爲CascadeLayout,實際上我們常用的佈局如relativelayout, linearlayout等都是繼承ViewGroup完成的。
package com.example.hello;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
public class CascadeLayout extends ViewGroup {
private int mHorizontalSpacing;
private int mVerticalSpacing;
public CascadeLayout(Context context) {
super(context);
}
public CascadeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CascadeLayout);
mHorizontalSpacing = a.getDimensionPixelSize(R.styleable.CascadeLayout_horizontal_spacing, 30);
mVerticalSpacing = a.getDimensionPixelSize(R.styleable.CascadeLayout_vertical_spacing, 30);
a.recycle();
}
public static class LayoutParams extends ViewGroup.LayoutParams{
int top;
int left;
public int verticalSpacing;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a = c.obtainStyledAttributes(attrs,R.styleable.CascadeLayout_LayoutParams);
verticalSpacing = a.getDimensionPixelSize(R.styleable.CascadeLayout_LayoutParams_layout_vertical_spacing,-1);
a.recycle();
}
public LayoutParams(int w, int h) {
super(w, h);
}
}
@Override
protected boolean checkLayoutParams(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 void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = getPaddingLeft();
int height = getPaddingTop();
int verticalSpacing;
final int count = getChildCount();
for( int i=0; i<count; i++){
verticalSpacing = mVerticalSpacing;
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
width = getPaddingLeft() + mHorizontalSpacing * i;
lp.top = width;
lp.left = height;
if( lp.verticalSpacing >= 0){
verticalSpacing = lp.verticalSpacing;
}
width += child.getMeasuredWidth();
height += verticalSpacing;
}
width += getPaddingRight();
height += getChildAt(getChildCount() - 1).getMeasuredHeight()+ getPaddingBottom();
setMeasuredDimension(resolveSize(width, widthMeasureSpec),
resolveSize(height, heightMeasureSpec));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int count = getChildCount();
for ( int i = 0; i<count; i++){
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
child.layout(lp.top, lp.left,
lp.top + child.getMeasuredWidth(),
lp.left + child.getMeasuredHeight());
}
}
}
需要注意的是,如果要使得自定義的LayoutParams,需要重寫方法checkLayoutParams、generateDefaultLayoutParams以及generateLayoutParams,不過基本上寫法都一樣。
爲什麼要自定義ViewGroup?
1. 在不同的Activity中複用該視圖,更容易維護
2. 開發者可以使用自定義屬性來定製ViewGroup中子視圖的位置
3. 佈局文件更加簡明,更容易理解
其實在應用“雅虎每日新聞News Digest”中完全有使用到類似的控件,只不過人家把名字改了!
該應用真的效果很不錯,這個桌面wiget也是非常不錯的。大家看看佈局層次圖,這裏他取名字爲StackView,實際上還是ViewGroup,不過他的功能比上面的CascadeLayout更加強大。
該博文參考了50 Android Hacks!