假设现在要创建一个如下图所示的布局,你会怎么做?
一种简单做法是利用margin 属性,代码如下:
<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>
另一种做法是;将这三个view放进一个自定义的ViewGroup中,好处是:
1.可重用性,方便在其它的activity中使用
2.增加布局文件的可阅读性
3.如果需要改变margin,不再需要手动去调整每个view的margin
4.可以利用自定义属性去自定义ViewGroup中的每个View
在自定义ViewGroup之前,需要知道Android是如何绘制视图(views)的
官方文档:绘制一个布局分两个过程:先测量后绘图。测量过程是通过函数measure(int, int)实现,对视图树自顶向下,依次计算,每一个view都会将自己的测量结果往下传递,最后所有的view都会记录自己的测量结果。然后就是通过layout(int, int, int,int)来绘图了,也是自顶向下绘制。
接下来说说怎么绘制ViewGroup:
1.处理ViewGroup的width和height.
处理width及height的操作在onMeasure()方法中进行,在此方法内,ViewGroup会根据它的子View来计算自身所占用的布局空间。
2.布局到页面上
这点操作在onLayout()方法中进行,在此方法中,ViewGroup会根据从onMeasure()中得到的信息将其每一个子View绘制出来。
下面是具体的代码步骤:
首先在XML布局文件中添加CascadeLayout:
<FrameLayout <!--自定义命名空间,以便在下文中使用自定义的属性--> xmlns: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 ="30dp" <!--因为前面添加了cascade命名空间,所以此处可以使用自定义属性--> cascade:vertical_spacing ="20dp" > <View android:layout_width ="100dp" android:layout_height ="150dp" cascade:layout_vertical_spacing ="90dp"<!--为子View添加的自定义属性,将在本文第三部分用到--> 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/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>
然后,当我们在创建CascadeLayout且没有为其指定horizontal_spacing与vertical_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>
最后,我们需要创建一个名为CascadeLayout的Java类,它继承了ViewGroup并重写了onMeasure()与OnLayout()方法。
1.CascadeLayout的构造函数
public CascadeLayout (Context context, AttributeSet attrs) {
super( context, attrs);
TypedArray a = context .obtainStyledAttributes (attrs ,
R. styleable. CascadeLayout );
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 ();
}
2.构建自定义的LayoutParams类
LayoutParams类将作为CascadeLayout的内部类存在,它将存储每个子View的x,y坐标。定义如下:
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 );
}
}
3.重写onMeasure()方法
onMeasure()方法将是CascadeLayout类中最关键的部分,这个方法不仅计算整个ViewGroup所占用的布局空间,还将计算出每个子View所占用的布局空间。
@Override protected void onMeasure (int widthMeasureSpec , int heightMeasureSpec ) { int width = 0; int height = getPaddingTop (); 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; lp .x = width; lp .y = height; width += child .getMeasuredWidth (); height += mVerticalSpacing ; } width += getPaddingRight (); height += getChildAt (getChildCount () - 1). getMeasuredHeight () + getPaddingBottom (); setMeasuredDimension ( resolveSize( width, widthMeasureSpec ), resolveSize( height, heightMeasureSpec )); }
4.最后一步,重写onLayout()方法
代码很简单,就是让每个子View都调用layout()方法。
@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 ());
}
}
至此,就利用自定义的ViewGroup创建了一个和图2一样效果的布局页面。
三、为子View添加自定义属性
既然费了这么大劲,怎么可能就和之前几行XML代码效果一样?
下面,我们就来为CascadeLayout中的子View添加自定义属性:
首先,在之前创建的attrs.xml中添加如下代码:
<declare-styleable name="CascadeLayout_LayoutParams"> <attr name="layout_vertical_spacing" format="dimension" /> </declare-styleable>
因为这个新添加的属性是以 layout_ 开头的,所以它会被添加到LayoutParams中去。
我们可以在之前自定义的内部类LayoutParams中的构造函数中读取到这个属性,将第一个构造函数改为:
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 ();
}
}
既然添加了新的自定义属性,就必须在onMeasure()方法中对其加以处理:
@Override
protected void onMeasure (int widthMeasureSpec , int heightMeasureSpec ) {
int width = getPaddingLeft ();
int height = getPaddingTop ();
int verticalSpacing ;
final 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;
lp .x = width;
lp .y = height;
if (lp .verticalSpacing >= 0 ) {
verticalSpacing = lp .verticalSpacing ;
}
width += child .getMeasuredWidth ();
height += verticalSpacing ;
}
width += getPaddingRight ();
height += getChildAt (getChildCount () - 1). getMeasuredHeight ()
+ getPaddingBottom ();
setMeasuredDimension ( resolveSize( width, widthMeasureSpec ),
resolveSize( height, heightMeasureSpec ));
}
最后附上完整的CascadeLayout代码:
package com.manning.androidhacks.hack003.view;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import com.manning.androidhacks.hack003.R;
public class CascadeLayout extends ViewGroup {
private int mHorizontalSpacing;
private int mVerticalSpacing;
public CascadeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.CascadeLayout);
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();
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = getPaddingLeft();
int height = getPaddingTop();
int verticalSpacing;
final 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;
lp.x = width;
lp.y = height;
if (lp.verticalSpacing >= 0) {
verticalSpacing = lp.verticalSpacing;
}
width += child.getMeasuredWidth();
height += verticalSpacing;
}
width += getPaddingRight();
height += getChildAt(getChildCount() - 1).getMeasuredHeight()
+ getPaddingBottom();
setMeasuredDimension(resolveSize(width, widthMeasureSpec),
resolveSize(height, heightMeasureSpec));
}
@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());
}
}
@Override
protected boolean checkLayoutParams(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);
}
public static class LayoutParams extends ViewGroup.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);
}
}
}