天天看點

Hack3-建立自定義ViewGroup3.1 了解Android如何繪制Views3.2 建立級聯布局(CascadeLayout)3.3 為children添加自定義屬性3.4 總結3.5 相關連結

當你在設計你的程式的時候,你可能會有一些很複雜的View要展示在不同的Activity當中。想象一下,你正在編寫一個紙牌遊戲,你為了想展示使用者手中的牌,做出了如下圖所示的樣子,你會怎樣設計它的布局呢?

Hack3-建立自定義ViewGroup3.1 了解Android如何繪制Views3.2 建立級聯布局(CascadeLayout)3.3 為children添加自定義屬性3.4 總結3.5 相關連結

你可能會說:我們使用margin屬性便足可以達到這樣的效果。此話不假,你的确可以用RelativeLayout,然後将它的children添加上margin屬性達到上圖所示的效果。xml檔案如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent" >
<View
	android:layout_width="100dp"
	android:layout_height="150dp"
	android:background="#FF0000" />
<View
	android:layout_width="100dp"
	android:layout_height="150dp"
	android:layout_marginLeft="30dp"
	android:layout_marginTop="20dp"
	android:background="#00FF00" />
<View
	android:layout_width="100dp"
	android:layout_height="150dp"
	android:layout_marginLeft="60dp"
	android:layout_marginTop="40dp"
	android:background="#0000FF" />
</RelativeLayout>
           

這個XML檔案的效果如下圖所示:

Hack3-建立自定義ViewGroup3.1 了解Android如何繪制Views3.2 建立級聯布局(CascadeLayout)3.3 為children添加自定義屬性3.4 總結3.5 相關連結

在這次的Hack中,我們将會使用另外一種辦法建立相同的布局——使用自定義ViewGroup。使用自定義ViewGroup相比向XML檔案中手動添加margin的優勢如下:

  • 在不同的activity中更易于維護
  • 你可以使用自定義的屬性來讓ViewGroup中的children的位置更加靈活可變
  • XML檔案的可讀性與精确性将會更好
  • 如果你需要改動margin,你不用不情願的手動修改每一個child的margin

下面就讓我們來看看Android如何繪制生成View的吧。

3.1 了解Android如何繪制Views

為了建立一個自定義的ViewGroup,你必須明白Android是如何繪制Views的。這裡不會深入的進行講解,但是你需要了解下面這個從Android文檔中寫的段落,它解釋了如何繪制一個layout:

繪制Layout是一個雙行程(two-pass)的過程,一個Measure(測量)的過程,一個Layout(布局)的過程。Measure的這個過程是measure(int,int)來實作的,是一個自頂向下周遊的View樹的過程。在這個遞歸過程中每個View将它的尺寸向下傳遞,在measure過程的結束的時候,每一個view都存儲了自己的尺寸的大小。第二個過程是layout(int,int,int,int)來實作的,它也是自頂向下。在這次過程當中,每一個parent負責用上一個過程計算出來的size來為自己的children來确定各自的位置。

為了明白這個概念,我們來分析一下繪制ViewGroup的方法。第一步就是确定他的width和height,我們在onMeasure()_函數中實作它。在這個函數當中,ViewGroup将會周遊它的children來确定它自己的大小。我們在onLayout()函數中來做最後的事情,在這個onLayout()方法中,ViewGroup将會利用在onMeasure()中搜集的資訊來定位放置它的children。

3.2 建立級聯布局(CascadeLayout)

在這節當中,我們将會為自定義的ViewGroup進行編碼。我們将會實作與第二張圖相同的效果。我們将新建立的ViewGroup叫做CascadeLayout。XML的代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:cascade="http://schemas.android.com/apk/res/com.manning.androidhacks.hack003"<!-- 自定義的namespace,即可使用自定義屬性-->
android:layout_width="fill_parent"
android:layout_height="fill_parent">

<com.manning.androidhacks.hack003.view.CascadeLayout<!--使用完整的名字-->
	android:layout_width="fill_parent"
	android:layout_height="fill_parent"
	cascade:horizontal_spacing="30dp"<!--自定義屬性-->
	cascade:vertical_spacing="20dp" >
<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" />
</com.manning.androidhacks.hack003.view.CascadeLayout>
</FrameLayout>
           

現在你應該知道你應該建構什麼東西了,接下來就開始吧。第一件我們做的事情就是定義這些自定義的屬性。為了做到這點,我們需要在res/layout建立一個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>
           

由于使用者可能不會定義水準和數值的間距(spacing),我們就有必要指定一個預設的值。我們将這些預設的值,放在res/values的dimens.xml中。内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
	<dimen name="cascade_horizontal_spacing">10dp</dimen>
	<dimen name="cascade_vertical_spacing">10dp</dimen>
</resources>
           

在明白Android如何繪制View之後,你就可能會想你需要寫一個繼承(extens)ViewGroup的類,叫做CascadeLayout,然後在裡面重寫(override)onMeasure()和onLayout()方法。由于代碼有一些長,是以我們分部分來讨論:構造函數、onMeasure(),onLayout()方法。接下來是構造函數的代碼:

public class CascadeLayout extends ViewGroup {
	private int mHorizontalSpacing;
	private int mVerticalSpacing;
	//當view執行個體從xml被建立的時候,就會調用構造函數
	Constructor public CascadeLayout(Context context, AttributeSet attrs) {
		super(context, attrs);
		TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.CascadeLayout);
		//mHorizontalSpacing和mVerticalSpacing從自定義屬性中讀取
		//若沒有指定,則使用預設值
		try {
			mHorizontalSpacing = a.getDimensionPixelSize(
				R.styleable.CascadeLayout_horizontal_spacing,
				getResources().getDimensionPixelSize(R.dimen.cascade_horizontal_spacing));
			mVerticalSpacing = a.getDimensionPixelSize(
				R.styleable.CascadeLayout_vertical_spacing,
				getResources().getDimensionPixelSize(R.dimen.cascade_vertical_spacing));
		} finally {
		a.recycle();
		}
}
           

在寫onMeasure()方法之前,我們先來建立一些自定義的LayoutParams,這個class将會儲存各個child的x,y位置的值。我們将會将LayoutParams作為CascadeLayout的内部類。這個class的定義如下:

public static class LayoutParams extends ViewGroup.LayoutParams {
	int x;
	int y;
	public LayoutParams(Context context, AttributeSet attrs) {
		super(context, attrs);
	}
	public LayoutParams(int w, int h) {
		super(w, h);
	}
}
           

為了使用這個CascadeLayout.LayoutParams這個class,我們需要override一些CascadeLayout中附加的函數。要重寫的函數有:

checkLayoutParams();
generateDefaultLayoutParams();
generateLayoutParams(AttributeSet attrs);
generateLayoutParams(ViewGroup.LayoutParams p);
           

在ViewGroup中這些方法的代碼都是幾乎一樣的,如果你對這個内容很感興趣,你可以看看示例代碼。

下一步就是寫onMeasure()方法了,這是整個class的關鍵的地方,代碼如下:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	//使用width和height來計算layout最後的尺寸和每個child的x,y
	int width = 0;
	int height = getPaddingTop();//擷取toppadding
	final int count = getChildCount();
	for (int i = 0; i < count; i++) {
		View child = getChildAt(i);
		measureChild(child, widthMeasureSpec, heightMeasureSpec);
		LayoutParams lp = (LayoutParams) child.getLayoutParams();
		width = getPaddingLeft() + mHorizontalSpacing * i;//計算目前child的x坐标
		lp.x = width;//指派目前child的x坐标
		lp.y = height;//指派目前child的x坐标
		//累加child的寬度,作為parent的寬度(最後還要加上rightpadding)
		width += child.getMeasuredWidth();
		//累加child之間的間距,作為parent的高度(最後還要加上最後一個child的高度和bottompadding)
		height += mVerticalSpacing;
	}
	width += getPaddingRight();
	height += getChildAt(getChildCount() - 1).getMeasuredHeight() + getPaddingBottom();
	//最後确定寬度高度
	setMeasuredDimension(resolveSize(width, widthMeasureSpec),
							resolveSize(height, heightMeasureSpec));
}
           

最後一步就是重寫onLayout方法,讓我們看看代碼:

@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.x, lp.y,
			lp.x + child.getMeasuredWidth(),lp.y+ child.getMeasuredHeight());
	}
}
           

正如你所看到的,代碼非常的簡單。它調用了每個child的layout函數,并且使用了在onMeasure中計算出來的值。

3.3 為children添加自定義屬性

在這最後一節中,你将會學到如何給子View添加自定義的屬性。作為一個例子,我們也将會說明如何為特定的child指定垂直間距。你可以在下圖中看到效果:

Hack3-建立自定義ViewGroup3.1 了解Android如何繪制Views3.2 建立級聯布局(CascadeLayout)3.3 為children添加自定義屬性3.4 總結3.5 相關連結

第一件我們需要做的事情就是在attrs.xml中添加新屬性:

<declare-styleable name="CascadeLayout_LayoutParams">
	<attr name="layout_vertical_spacing" format="dimension" />
</declare-styleable>
           

由于屬性的名字是layout_打頭的,是以它被加進LayoutParams的屬性中。我們将會在LayoutParams的構造函數中讀取這個值,就像我們之前在CascadeLayout中做的一樣。代碼如下:

public LayoutParams(Context context, AttributeSet attrs) {
	super(context, attrs);
	TypedArray a = context.obtainStyledAttributes(attrs,
						R.styleable.CascadeLayout_LayoutParams);
	try {
		verticalSpacing = a.getDimensionPixelSize(
		R.styleable.CascadeLayout_LayoutParams_layout_vertical_spacing,-1);
	} finally {
		a.recycle();
	}
}
           

verticalSpacing是一個都可以通路的變量,我們将在CascadeLayout的onMeasure()函數中使用它,如果child的LayoutParams包含了verticalSpacing,我們就可以使用。代碼如下:

verticalSpacing = mVerticalSpacing;
	...
	LayoutParams lp = (LayoutParams) child.getLayoutParams();
	if (lp.verticalSpacing >= 0) {
		verticalSpacing = lp.verticalSpacing;
	}
	...
	width += child.getMeasuredWidth();
	height += verticalSpacing;
           

3.4 總結

使用自定義的View和ViewGroups是一個非常好的組織你的程式布局的方法。自定義元件将會為你提供更多自定義的行為。下一次你需要建立一個很複雜的布局的時候,你可以考慮一下是否使用ViewGroup,開始的時候可能工作量很比較大,但是結果卻是很值得的。

3.5 相關連結

http://developer.android.com/guide/topics/ui/how-android-draws.html

http://developer.android.com/reference/android/view/ViewGroup.html

http://developer.android.com/reference/android/view/ViewGroup.LayoutParams.html

MeasureSpec介紹及使用詳解

android 中 padding與margin的差別

Android中mesure過程詳解

轉載請注明原位址,謝謝!

http://blog.csdn.net/kost_/article/details/13296541

代碼下載下傳位址:

http://download.csdn.net/detail/u011418185/6466965