当你在设计你的程序的时候,你可能会有一些很复杂的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
转载请注明原地址,谢谢!
http://blog.csdn.net/kost_/article/details/13296541
代码下载地址:
http://download.csdn.net/detail/u011418185/6466965