Android中所有界面都是由 View 和 ViewGroup 组成,
View是 所有基本组件的基类,以View结尾的。
ViewGroup是 拼装这些组件的容器,以Layout结尾的。

我们这次用的是 ViewGroup
绘制布局由两个遍历的过程组成:测量过程布局过程
onMeasure() 完成测量过程:测量所有视图的宽度和高度
onLayout() 完成布局过程:利用上步计算出的测量信息,布局所有子视图

我觉得能来搜ViewGroup的应该都基本的知道,这个是用来干嘛的。先来写下个人的整体理解。

先上总结:

  1. 在attr创建属性,在dimens设置默认属性
  2. 在CascadeLayout构造器里面获取设置的默认属性
  3. 在 onMeasure 计算每个子视图的位置和宽度 ————测量过程
    3.1 创建 LayoutParams,来储存每个视图的 尺寸信息
    3.2 添加 LayoutParams 需要的一些方法。
  4. 将算好的子视图的信息 在onLayout中设置给布局。 ————布局过程
  5. 然后在布局中就可以使用了。

然后我们来看看效果图:

Paste_Image.png

  <com.lyl.viewgroup.view.CascadeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:horizontal_spacing="40dp"
    app:vertical_spacing="40dp" >

    <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" />

    <View
        android:layout_width="100dp"
        android:layout_height="150dp"
        android:background="#FF0000" />
</com.lyl.viewgroup.view.CascadeLayout>

只要在代码中使用了我们的父布局,在子布局中不用做任何设置,就会按照指定的规则排列。

具体步骤就是上面总结,看起来挺麻烦的,其实是非常简单。跟着看下去,不想写代码直接复制就可以,都是很简单的代码。走一遍流程之后,就知道自定义 ViewGroup是个怎么会事了,如果有更好的想法,可以再具体实践的。

这里说一下那个app:也就是 自定义属性的前缀。
这个app 为什么会是app 呢。
因为我们用到了自定义的属性,需要在xml引入我们需要在最顶部加入这个:

 xmlns:app="http://schemas.android.com/apk/res/com.lyl.viewgroup"

而这个app就是由后面的赋值的,当然也可以改成其他的你想要的。

不想看代码的,可以直接跳到最后看源码,源码也非常简单。注释也比较详细。


然后开始写代码吧。

1. 在attr创建属性,在dimens设置默认属性

首先在res/values的目录下创建一个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>

attrs.xml文件定义属性的作用是为了在这里使用。看那两个app:的属性,就是为了设置每个子布局距离 左边 和 上面 的距离:

   <com.lyl.viewgroup.view.CascadeLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      app:horizontal_spacing="40dp"
      app:vertical_spacing="40dp" >

然后在res/values的目录下创建一个dimens.xml文件,这里的作用是为了设置每个子布局距离左边和上面的默认距离,也就是上面两个属性没有设置的时候,这个会起作用。

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

2. 在CascadeLayout构造器里面获取设置的默认属性

这一步肯定是要先创建一个CascadeLayout类的,继承ViewGroup,实现方法。然后在构造器里面就可以获取设置的属性了,代码如下,有相应注释:

public class CascadeLayout extends ViewGroup {

// 子布局位于 左边 和 上面 的距离
private int mHorizontalSpacing;
private int mVerticalSpacing;

/**
 * 当通过XML文件创建该视图会调用这个方法
 */
public CascadeLayout(Context context, AttributeSet attrs) {
	super(context, attrs);

	// 从自定义属性中获取值,从attrs中
	TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CascadeLayout);

	try {
		// 获取用户指定的大小,就是我们刚开始指定的那个40dp
		// 如果没有指定,就使用默认值
		mHorizontalSpacing = ta.getDimensionPixelSize(R.styleable.CascadeLayout_horizontal_spacing, getResources()
				.getDimensionPixelSize(R.dimen.cascade_horizontal_spacing));
		mVerticalSpacing = ta.getDimensionPixelSize(R.styleable.CascadeLayout_vertical_spacing, getResources()
				.getDimensionPixelSize(R.dimen.cascade_vertical_spacing));

	} finally {
		ta.recycle();
	}
}

3. 在 onMeasure 计算每个子视图的位置和宽度 ————测量过程

在编写onMeasure()方法之前,我们要先创建LayoutParams类,这个类用来保存每个子视图的x,y轴的位置。一般都是把这个类直接定义为内部类。代码如下:

3.1 创建 LayoutParams,来储存每个视图的 尺寸信息

/** 保存每个子视图的x、y轴的位置 **/
public static class LayoutParams extends ViewGroup.LayoutParams {
	int x;
	int y;

	public LayoutParams(Context c, AttributeSet attrs) {
		super(c, attrs);
	}

	public LayoutParams(int width, int height) {
		super(width, height);
	}
}

3.2 添加 LayoutParams 需要的一些方法。

要使用LayoutParams,还需要在我们的CascadeLayout类中重写以下方法。这些方法在不同的Viewgroup通常都是相同的,可参考 LinearLayout 源码。

@Override
protected boolean checkLayoutParams(android.view.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 LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
	return new LayoutParams(p.width, p.height);
}

3.3 编写onMeasure()方法。这是这个类的核心代码

注释的很清楚。

/**
 * 测量所有视图的宽度和高度
 */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	// !!!在编写onMeasure方法之前,先创建自定义LayoutParams类,保存每个子视图的x、y轴的位置。

	// 使用宽和高 计算布局的最终大小,以及子视图的x、y轴的位置
	int width = getPaddingLeft();
	int height = getPaddingTop();

	// 1. 获取子视图的数量
	final int count = getChildCount();
	for (int i = 0; i < count; i++) {

		View child = getChildAt(i);

		// 2. 让 每个子视图 测量自己
		measureChild(child, widthMeasureSpec, heightMeasureSpec);

		LayoutParams lp = (LayoutParams) child.getLayoutParams();

		// 3. 在LayoutParams中保存每个子视图的x、y坐标,以便在onLayout()中布局
		lp.x = width;
		width += mHorizontalSpacing;

		lp.y = height;
		height += mVerticalSpacing;
	}

	// 4. 计算整体的宽、高
	width += getChildAt(getChildCount() - 1).getMeasuredWidth() + getPaddingRight();
	height += getChildAt(getChildCount() - 1).getMeasuredHeight() + getPaddingBottom();

	// 5. 使用计算的 宽、高 设置整个布局的测量尺寸
	setMeasuredDimension(resolveSize(width, widthMeasureSpec), resolveSize(height, heightMeasureSpec));
}

4. 将算好的子视图的信息 在onLayout中设置给布局。 ————布局过程

/**
 * 利用上步计算出的测量信息,布局所有子视图
 */
@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());
	}
}

到这里就可以结束了。

5. 然后在布局中就可以使用了。

现在就可以使用了,跟我们刚开始的时候那个布局一样。


为子布局添加属性。

因为上面的大体逻辑已经写完,添加子布局的属性也比较简单。
还是先上总结:

  1. 在attrs.xml 文件,添加子布局的新属性
  2. 在CascadeLayout的内部类LayoutParams 中接受这个属性
  3. 在onMeasure()使用这个属性

来写代码

1. 在attrs.xml 文件,添加子布局的新属性

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

2. 在CascadeLayout的内部类LayoutParams 中接受这个属性

修改之后LayoutParams 类的代码如下,同样注释写的很清楚:

/** 保存每个子视图的x、y轴的位置 **/
public static class LayoutParams extends ViewGroup.LayoutParams {
	int x;
	int y;
	// 子布局距离 左边 和 上面的距离
	public int verticalSpacing;
	public int horizontalSpacing;

	public LayoutParams(Context c, AttributeSet attrs) {
		super(c, attrs);

		// 1. 从自定义属性中获取值
		TypedArray ta = c.obtainStyledAttributes(attrs, R.styleable.CascadeLayout_LayoutParams);

		// 2.将获取的值拿到,以便onMeasure()使用
		try {
			horizontalSpacing = ta.getDimensionPixelSize(
					R.styleable.CascadeLayout_LayoutParams_layout_horizontal_spacing, -1);
			verticalSpacing = ta.getDimensionPixelSize(
					R.styleable.CascadeLayout_LayoutParams_layout_vertical_spacing, -1);
		} finally {
			ta.recycle();
		}
	}

	public LayoutParams(int width, int height) {
		super(width, height);
	}
}

3. 在onMeasure()使用这个属性

在onMeasure()中修改了 子布局 x、y的位置代码。其余的没有变

	if (lp.horizontalSpacing >= 0) {
		lp.x = width + lp.horizontalSpacing;
		width += mHorizontalSpacing + lp.horizontalSpacing;
	} else {
		lp.x = width;
		width += mHorizontalSpacing;
	}

	if (lp.verticalSpacing >= 0) {
		lp.y = height + lp.verticalSpacing;
		height += mVerticalSpacing + lp.verticalSpacing;
	} else {
		lp.y = height;
		height += mVerticalSpacing;
	}

然后就可以在子视图中添加属性了:

app:layout_horizontal_spacing="40dp"
app:layout_vertical_spacing="40dp"

至此,就结束了。


最后项目源码:
https://github.com/Wing-Li/PracticeDemos
欢迎Star,欢迎批评指正。