下面我们定义一个可以容纳4个view的Layout,分别位于四个角。这个实例主要是为了让大家理解如何自定义一个ViewGroup。
首先需要了解下ViewGroup的职责。
ViewGroup相当于一个放置View的容器,并且我们在写布局xml的时候,会告诉容器(凡是以layout为开头的属性,都是容器的类型),我们的宽度(layout_width)、高度(layout_height)、对齐方式(layout_gravity)等;当然还有margin等;于是乎,ViewGroup的职能为:给childView计算出建议的宽和高和测量模式;决定childView的位置;
为什么只是建议的宽和高,而不是直接确定呢,别忘了childView宽和高可以设置为wrap_content,这样只有childView才能计算出自己的宽和高。
ViewGroup和LayoutParams之间的关系
大家可以回忆一下,当在LinearLayout中写childView的时候,可以写layout_gravity,layout_weight属性;在RelativeLayout中的childView有layout_centerInParent属性,却没有layout_gravity,layout_weight,这是为什么呢?这是因为每个ViewGroup需要指定一个LayoutParams,用于确定支持childView支持哪些属性,比如LinearLayout指定LinearLayout.LayoutParams,RelativeLayout有RelativeLayout.LayoutParams等。如果大家去看LinearLayout的源码,会发现其内部定义了LinearLayout.LayoutParams,在此类中,你可以发现weight和gravity的身影。
1. 定义FourLayout类继承ViewGroup
public class FourLayout extends ViewGroup {
private Context mContext;
public FourLayout(Context context){
this(context,null);
}
public FourLayout(Context context , AttributeSet attrs){
this(context,attrs,0);
}
public FourLayout(Context context,AttributeSet attrs,int defStyle){
super(context,attrs,defStyle);
mContext = context;
}
……
}
这里只是简单的复写了三个构造方法,一般来说,这三个构造方法是我们自定义view经常复写的。
2. 指定相应的LayoutParams
因为每个ViewGroup需要指定一个LayoutParams,用于确定支持childView支持哪些属性,比如LinearLayout指定LinearLayout.LayoutParams等。因此,需要重写ViewGroup的LayoutParamsgenerateLayoutParams()方法,用于返回LayoutParams。
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
//返回MarginLayoutParams
return new MarginLayoutParams(getContext(),attrs);
}
这里我们返回MarginLayoutParams的实例,因为我们的ViewGroup只需要支持margin即可。这样就为我们的ViewGroup指定了其LayoutParams为MarginLayoutParams。
3. 重写onMeasure()
根据ViewGroup的职责,我们需要在onMeasure()中测量子View,并且根据子View的布局,我们要计算出我们自己的宽和高。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//测量各个子元素的宽高
measureChildren(widthMeasureSpec,heightMeasureSpec);
/**
* 下面是根据子元素测量后,测量自己的宽高
*/
int width = 0;
int height = 0;
MarginLayoutParams param = null;
//左 右的高度
int leftHeight = 0;
int rightHeight = 0;
//上 下 的宽度
int topWidth = 0;
int bottomWidth = 0;
for (int i = 0;i < getChildCount();i++){
//获取子View
View child = getChildAt(i);
param = (MarginLayoutParams) child.getLayoutParams();
//最上面两个
if (i == 0 || i == 1){
topWidth += param.leftMargin + child.getMeasuredWidth() + param.rightMargin;
}
//最左边两个
if (i == 0 || i == 2){
leftHeight += param.topMargin + child.getMeasuredHeight() + param.bottomMargin;
}
//最下面两个
if (i == 2 || i == 3){
bottomWidth += param.leftMargin + child.getMeasuredWidth() + param.rightMargin;
}
//最上面两个
if (i == 1 || i == 3){
rightHeight += param.topMargin + child.getMeasuredHeight() + param.bottomMargin;
}
}
//获取两者的最大值
width = Math.max(topWidth,bottomWidth);
height = Math.max(leftHeight,rightHeight);
if (widthMode == MeasureSpec.EXACTLY){
width = widthSize;
}
if (heightMode == MeasureSpec.EXACTLY){
height = heightSize;
}
setMeasuredDimension(width,height);
}
首先获取该ViewGroup父容器为其设置的计算模式和尺寸,大多情况下,只要不是wrap_content,父容器都能正确的计算其尺寸。所以我们自己需要计算如果设置为wrap_content时的宽和高,如何计算呢?那就是通过其childView的宽和高来进行计算。
通过ViewGroup的measureChildren方法为其所有的childView设置宽和高,此行执行完成后,childView的宽和高都已经正确的计算过了
根据childView的宽和高,以及margin,计算ViewGroup在wrap_content时的宽和高。
如果宽高属性值为wrap_content,则设置为上面计算的值,否则为其父容器传入的宽和高。
4. 重写onLayout()
对于继承ViewGroup的,要求必须实现onLayout()方法,因此这里是必须的。在这里我们必须指明各个子View是如何放置的,其实就是指明各个childView的left,top,right,bottom。然后最后调用各个childView的layout()方法来进行Layout,一旦layout后,各个childview就固定好了。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childWidth = 0;
int childHeight = 0;
MarginLayoutParams param = null;
/**
* 遍历所有子View,根据其宽高对其进行margin布局
*/
for (int i = 0;i < getChildCount();i++){
View child = getChildAt(i);
childWidth = child.getMeasuredWidth();
childHeight = child.getMeasuredHeight();
param = (MarginLayoutParams) child.getLayoutParams();
int left = 0;
int right = 0;
int top = 0;
int bootom = 0;
switch (i){
case 0:
left = param.leftMargin;
top = param.topMargin;
break;
case 1:
left = getMeasuredWidth() - childWidth - param.rightMargin;
top = param.topMargin;
break;
case 2:
left = param.leftMargin;
top = getMeasuredHeight() - childHeight - param.bottomMargin;
break;
case 3:
left = getMeasuredWidth() - childWidth - param.rightMargin;
top = getMeasuredHeight() - childHeight - param.bottomMargin;
break;
default:
break;
}
right = left + childWidth;
bootom = top + childHeight;
//调用child的layout完成子view的layout
child.layout(left,top,right,bootom);
}
}
这里的工作就是遍历所有的childView,根据childView的宽和高以及margin,然后分别将0,1,2,3位置的childView依次设置到左上、右上、左下、右下的位置。
如果是第一个View(index=0) :left = param.leftMargin,top = param.topMargin,right = childView.getWidth() + left,bootom =childView.getHeight() + top;
如果是第二个View(index=1) : left = getMeasuredWidth()- childWidth - param.rightMargin, top = param.topMargin,right =childView.getWidth() + left,bootom = childView.getHeight() + top;
其他依次类似。
计算出了left,top,right,bootom,之后,我们需要调用childView的layout()方法来进行layout过程。
这样就完成了,我们的ViewGroup代码的编写,下面我们进行测试,分别设置宽高为固定值,wrap_content,match_parent。
5. 使用自定义的FourLayout
直接在布局文件中引入自定义的ViewGroup
<com.customview.ui.FourLayout
android:layout_width="wrap_content"
android:layout_height="400dp"
android:background="#FFF"
>
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#F00"
/>
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#F0F"
android:layout_marginLeft="20dp"
android:layout_marginRight="10dp"/>
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#0FF"/>
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#FF0"
android:layout_marginLeft="25dp"
android:layout_marginRight="20dp"/>
</com.customview.ui.FourLayout>
效果如下图: