自定义布局效果如图所示:
MainActivity的布局文件如下所示:
<FrameLayoutxmlns:cascade="http://schemas.android.com/apk/res/com.manning.androidhacks.hack003"
xmlns:android="http://schemas.android.com/apk/res/android"
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="50dp"
cascade:vertical_spacing="50dp" >
<View
android:layout_width="100dp"
android:layout_height="150dp"
cascade:layout_vertical_spacing ="10dp"
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>
通过布局文件可以看出,在自定义组建CascadeLayout中放了三个ImageView,每一个ImageView都设置了一个纯色的背景色。
既然CascadeLayout可以放置其它的视图组件,说明它必然是一个容器。该自定义容器有3个自定义的属性horizontal_spacing,vertical_spacing,layout_vertical_spacing。这3个属性定义在values/attrs.xml文件中:
<resources>
<declare-styleablename="CascadeLayout">
<attrname="horizontal_spacing" format="dimension" />
<attrname="vertical_spacing" format="dimension" />
</declare-styleable>
<declare-styleablename="CascadeLayout_LayoutParams">
<attrname="layout_vertical_spacing" format="dimension" />
</declare-styleable>
</resources>
这三个属性的作用是:
horizontal_spacing和vertical_spacing用来设定自定义容器,兄弟视图之间的水平和垂直间距。如果一个容器中的视图使用了layout_vertical_spacing属性,则它与它后面的兄弟元素的垂直间距大小为layout_vertical_spacing而不是vertical_spacing了。就如本例中,3个视图组件,1组件设置了layout_vertical_spacing为10dp,因此1组件和2组件间的垂直距离就是10dp,而2组件和3组件之间的间距是vertical_spacing为50dp。1和2,2和3的水平间距均为horizantol_spacing,50dp。
接下来看一下CascadeLayout的源代码:
public classCascadeLayout extends ViewGroup {
//用来存储兄弟视图间的水平间距
private int mHorizontalSpacing;
//用来存储兄弟视图间的垂直间距
private int mVerticalSpacing;
public CascadeLayout(Context context,AttributeSet attrs) {
super(context, attrs);
//通过TypedArray获取布局文件中为R.styleable.CascadeLayout中定义的属性所设定的数值
//CascadeLayout是attrs.xml中一组styleable的name
//因此返回的a相当于获得了name="CascadeLayout"组中声明的所有属性组成的一个“数组”
//利用“下标”可以访问到这个“数组”中的内容,下标分别是“R.styleable.CascadeLayout_horizontal_spacing”和
//“R.styleable.CascadeLayout_vertical_spacing”
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.CascadeLayout);
try {
//mHorizontalSpacing的值为布局文件中为horizontal_spacing指定的值并转为px单位
//如果没有设定R.styleable.CascadeLayout中定义的horizontal_spacing属性的值
//则mHorizontalSpacing设定为R.dimen.cascade_horizontal_spacing定义的值
mHorizontalSpacing =a.getDimensionPixelSize(
R.styleable.CascadeLayout_horizontal_spacing,
getResources().getDimensionPixelSize(
R.dimen.cascade_horizontal_spacing));
//mVerticalSpacing的值为布局文件中为vertical_spacing指定的值并转为px单位
//如果没有设定R.styleable.CascadeLayout中定义的vertical_spacing属性的值
//则mVerticalSpacing设定为R.dimen.cascade_vertical_spacing定义的值
mVerticalSpacing =a.getDimensionPixelSize(
R.styleable.CascadeLayout_vertical_spacing,getResources()
.getDimensionPixelSize(R.dimen.cascade_vertical_spacing));
} finally {
//TypedArray使用完毕后,一定要回收
a.recycle();
}
}
/**
* onMeasure方法中测量设定整个布局的宽和高
* 整体的宽为容器左内边距+子控件间的水平间距+最后一个子控件的宽度+容器右内边距
* 整体的高为容器上内边距+子控件间的垂直间距+最后一个子控件的高度+容器下内边距
*/
@Override
protected void onMeasure(intwidthMeasureSpec, int heightMeasureSpec) {
//自定义容器CascadeLayout的左内边距
int width = getPaddingLeft();
//自定义容器CascadeLayout的上内边距
int height = getPaddingTop();
int verticalSpacing;
final int count = getChildCount();
//这个循环中的代码对容器宽度和高度采用了不同的处理方式
//个人认为没有必要,尤其是width += child.getMeasuredWidth();这行代码
//只有在最后一次循环的时候才会起作用。其余的时候,上面代码设置的width的值都会在下一次循环开始的时候
//被 width = getPaddingLeft() + mHorizontalSpacing * i;重新覆盖掉
//循环结束时,宽度会在Left Padding的基础上增加每一个子控件间的水平间距和最后一个子控件的宽度
//高度会在Top Padding的基础上增加每一个子控件的高度(最后一个子控件的高度是在循环结束后的外面才添加的)
for (int i = 0; i < count; i++) {
//先将vertiSpacing局部变量的值设定为mVerticalSpacing
//随着代码的执行,如果这个子控件被指定了layout_vertical_spacing属性
//则vertiSpacing局部变量的值设定为layout_vertical_spacing属性值
verticalSpacing = mVerticalSpacing;
//依次去获得容器中的每一个子控件
View child = getChildAt(i);
//调用measureChild方法,对该子控件进行测量,不然后面拿不到getMeasuredWidth的值,会返回0
measureChild(child, widthMeasureSpec,heightMeasureSpec);
//获得容器中子控件上被作为容器的父控件设定的LayouParrams,此时默认获得的是ViewGroup.LayoutParams
//当容器的构造器调用的时候,LayoutParams就会被创建出来(通过generateLayoutParams)
//LayoutParams的创建先于onMeasure方法的调用
//这里将默认的ViewGroup.LayoutParams强制类型转化为CascadeLayout.LayoutParams
LayoutParams lp = (LayoutParams)child.getLayoutParams();
width = getPaddingLeft() +mHorizontalSpacing * i;
//获得子视图的LayoutParams对象并强制转换为CascadeLayout.LayoutParams类型后,进行设置
//设置好的值会在onLayout方法中被使用,每一个子控件就根据lp.x和lp.y的值进行摆放
lp.x = width;
lp.y = height;
//接下来调整width和height的值,以便设定下一个子视图的起始位置(左上角位置)
//如果在布局文件中设置了layout_vertical_spacing属性,
//那么CascadeLayout.LayoutParams的verticalSpacing属性值就不为0
//这里的源码如果给layout_vertical_spacing属性设置了负值,则该设置无效
if (lp.verticalSpacing >= 0) {
//此时将verticalSpacing属性的值设置为layout_vertical_spacing属性的值
verticalSpacing = lp.verticalSpacing;
}
//这里修改的width值会在下一次循环开始的时候被覆盖掉。所以,这个width值只有在循环的最后一次才能起作用
//换句话说,在循环的最后一次的时候,width的值加上了最后一个子视图的宽度值
width += child.getMeasuredWidth();
//加上子视图间的垂直间距
height += verticalSpacing;
}
//循环结束后,将宽度的值加上容器CascadeLayout的右内边距
width += getPaddingRight();
//循环结束后,将高度的值再加上容器中最后一个子控件的高度再加上容器CascadeLayout下内边距
height += getChildAt(getChildCount() -1).getMeasuredHeight()
+ getPaddingBottom();
//利用resolveSize来综合考量CascadeLayout中子容器需要的尺寸与CascadeLayout父容器允许的CascadeLayout的尺寸
//得到适当的值后,再进行setMeasuredDimension
setMeasuredDimension(resolveSize(width,widthMeasureSpec),
resolveSize(height,heightMeasureSpec));
}
@Override
protected void onLayout(boolean changed, intl, int t, int r, int b) {
final int count = getChildCount();
//依次把容器中所有的子控件摆放到应该的位置(左上角的坐标为(lp.x,lp.y))
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());
}
}
@Override
protected booleancheckLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
protected LayoutParams generateDefaultLayoutParams(){
return newLayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
}
@Override
public LayoutParamsgenerateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(),attrs);
}
@Override
protected LayoutParamsgenerateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p.width, p.height);
}
public static class LayoutParams extendsViewGroup.LayoutParams {
int x;
int y;
public int verticalSpacing;
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();
}
}
public LayoutParams(int w, int h) {
super(w, h);
}
}
}