如需建如下的布局,我们应该如何创建这样的布局呢?
虽然使用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>
但我们可以通过创建自定义的ViewGroup来实现上述功能。该方法相对于在XML文件中手工指定margin值有如下优点:
1、在不同Activity中复用该视图时,更易维护。
2、开发者可以使用自定义属性来定制ViewGroup中子视图的位置。
3、布局文件更简明,更容易理解。
4、如果需要修改margin,不必重新手动计算每个子视图的margin。
理解Android绘制视图的方式
Android如何绘制一个布局:
”绘制布局由两个遍历过程组成:测量过程和布局过程。测量过程由measure(int, int)方法完成,该方法从上到下遍历视图树。在递归遍历过程中,每个视图都会向下层传递尺寸和规格。当measure方法遍历结束,每个视图都保存了各自的尺寸信息。第二个过程由layout(int, int, int, int)方法完成,该方法也是从上到下遍历视图树,在遍历过程中,每个父视图通过测量过程的结果定位所有子视图的位置信息“。
为了理解这个概念,下面分析ViewGroup的绘制过程。第一步是测量ViewGroup的宽度和高度。在onMeasure()方法中完成这步操作。在该方法中,ViewGroup通过遍历所有子视图计算出它的大小。最后一步操作,在onLayout()方法中完成,在该方法中,ViewGroup利用上一步计算出的测量信息,布局所有子视图。
创建自定义CascadeLayout
首先要定义CascadeLayout的定制属性,需要在res/values目录下创建一个属性文件attrs.xml,该文件的内容如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--为CascadeLayout视图添加自定义属性。
declare-styleable中的name属性值必为自定义控件的类名(CascadeLayout),不然在布局文件中将无法使自定义控件的定制属性-->
<declare-styleable name="CascadeLayout">
<attr name="horizontal_spacing" format="dimension"/>
<attr name="vertical_spacing" format="dimension"/>
</declare-styleable>
<!--为CascadeLayout的子视图添加自定义属性。添加为特定子视图重写(override)垂直间距的方法,name属性值必为父控件类名_父控件的LayoutParams内部类(CascadeLayout_LayoutParams)-->
<declare-styleable name="CascadeLayout_LayoutParams">
<!--因为属性名的前缀是layout_,没有包含一个视图属性,因此该属性会被添加到LayoutParams的属性表中-->
<attr name="layout_vertical_spacing" format="dimension"/>
</declare-styleable>
</resources>
同时还需要指定水平间距和垂直间距的默认值,以便在未指定这些值时使用。把这些默认值保存在dimens.xml文件中,该文件同样位于res/values文件夹下。dimens.xml文件如下:
<resources>
<dimen name="cascade_horizontal_spacing">10dp</dimen>
<dimen name="cascade_vertical_spacing">10dp</dimen>
</resources>
创建CascadeLayout类
package com.example.huangfei.demo;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
/**
* Created by huangfeihong on 2015/10/25.
*/
public class CascadeLayout extends ViewGroup {
private int mHorizontalSpacing;//水平间距
private int mVerticalSpacing;//垂直间距
/**
* 当通过XML文件创建该视图的实例时会调用该构造函数
*/
public CascadeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CascadeLayout);
try {
/**
* mHorizontalSpacing和mVerticalSpacing由自定义属性中获取,如果其值未指定,就使用默认值
*/
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();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//使用宽和高计算布局的最终大小以及子视图的x与y轴位置
int width = getPaddingLeft();
int height = getPaddingTop();
int verticalSpacing;//子视图垂直间距
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;
//在LayoutParams中保存每个子视图的x和y坐标
lp.x = width;
lp.y = height;
if(lp.verticalSpacing >= 0){
verticalSpacing = lp.verticalSpacing;
}
width += child.getMeasuredWidth();
height += verticalSpacing;
}
width += getPaddingRight();
height += getChildAt(count - 1).getMeasuredHeight() + getPaddingBottom();
//使用计算所得的宽和高设置整个布局的测量尺寸
setMeasuredDimension(resolveSize(width, widthMeasureSpec), resolveSize(height, heightMeasureSpec));
}
/**
* 该方法以onMeasure()计算出的值为参数循环调用子View的layout()方法
*/
@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());
}
}
/**
* 要使自新定义的CascadeLayout.LayoutParams类,必须重写以下四个方法。
* 这些方法的代码在不同ViewGroup之间往往是相同的。
*/
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p.width, p.height);
}
/**
* 该类用于保存每个子视图的x、y轴位置
*/
public static class LayoutParams extends ViewGroup.LayoutParams {
int x;
int y;
int verticalSpacing;//子视图自定义垂直间距
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.CascadeLayout_LayoutParams);
try {
verticalSpacing = a.getDimensionPixelSize(
R.styleable.CascadeLayout_LayoutParams_layout_vertical_spacing, -1);
} finally {
a.recycle();
}
}
public LayoutParams(int w, int h) {
super(w, h);
}
}
}
创建Activity的布局文件
<?xml version="1.0" encoding="utf-8"?>
<!--在XML中使用自定义属性时指定自定义命名空间-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:cascade="http://schemas.android.com/apk/res-auto"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<!--通过cascade命名空间,就可以使用其自定义属性-->
<com.example.huangfei.demo.CascadeLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
cascade:horizontal_spacing="50dp"
cascade:vertical_spacing="20dp">
<!--父视图添加自定义属性-->
<View
android:layout_width="100dp"
android:layout_height="150dp"
android:background="#FF0000"
cascade:layout_vertical_spacing="90dp"/>
<!--子视图添加自定义属性-->
<View
android:layout_width="100dp"
android:layout_height="150dp"
android:background="#00FF00"
cascade:layout_vertical_spacing="50dp"/>
<View
android:layout_width="100dp"
android:layout_height="150dp"
android:background="#0000FF"/>
</com.example.huangfei.demo.CascadeLayout>
</FrameLayout>