Hack3-创建自定义ViewGroup

当你在设计你的程序的时候,你可能会有一些很复杂的View要展示在不同的Activity当中。想象一下,你正在编写一个纸牌游戏,你为了想展示用户手中的牌,做出了如下图所示的样子,你会怎样设计它的布局呢?


你可能会说:我们使用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文件的效果如下图所示:



在这次的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指定垂直间距。你可以在下图中看到效果:



第一件我们需要做的事情就是在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


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值