顾名思义,LayoutParams是一个用于保存和布局有关的属性的类,layout_width和layout_height两个必不可少的属性就是由它管理的,可见它的重要性。本文将详细介绍LayoutParams类,以及如何为自己的ViewGroup实现LayoutParams。
LayoutParams是ViewGroup的一个静态内部类,所有其他ViewGroup自定义的LayoutParams都是直接或间接地从它继承而来。首先看看它的几个域:
public static class LayoutParams {
public static final int MATCH_PARENT = -1;
public static final int WRAP_CONTENT = -2;
public int width;
public int height;
}
想必不需要多解释。特别注意一下,MATCH_PARENT和WRAP_CONTENT两个标志位的值都是负的,这个特点下面会用到。
接下来看看它的几个构造器:
public LayoutParams(Context c, AttributeSet attrs) {
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
setBaseAttributes(a,
R.styleable.ViewGroup_Layout_layout_width,
R.styleable.ViewGroup_Layout_layout_height);
a.recycle();
}
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
width = a.getLayoutDimension(widthAttr, "layout_width");
height = a.getLayoutDimension(heightAttr, "layout_height");
}
利用TypedValue类在布局文件中提取出layout_width和layout_height两个属性,并保存在了width和height中。不了解TypedValue的话可以看自定义View之添加自定义属性。这里很重要的一点是,在保存时不会区分获得的是固定值(如100dp)还是标志位(如MATCH_PARENT)。为了在使用时能够区分,MATCH_PARENT和WRAP_CONTENT两个标志位的值都是负的。也就是说,如果width或者height的值≥0,那么就代表固定值,否则就代表一个标志位。这个特性在构建MeasureSpec时常常用到。
public LayoutParams(LayoutParams source) {
this.width = source.width;
this.height = source.height;
}
根据一个现有的LayoutParams来设置自己的width与height域。
public LayoutParams(int width, int height) {
this.width = width;
this.height = height;
}
根据给定的width与height设置相应的域,在动态创建view时常常会用到。
相比于原生的ViewGroup.LayoutParams,自定义的LayoutParams往往会选择继承自ViewGroup.MarginLayoutParams,这个类在父类的基础之上提供了对margin属性的支持。margin属性是子view的位置相对于父view边界或是其他子view的偏移量,有leftMargin、topMargin、rightMargin、bottomMargin四种。另外,还有一个总的margin属性。在布局文件中,如果设置了margin属性,另外四个属性就会失效,实际的值会全部取和margin相等。
MarginLayoutParams的构造器无非是在LayoutParams的基础上增加了关于提取与设置各个margin属性的代码,这里就不重复贴出来了。
在向一个父view中添加新的子view时,偷懒的人往往会直接这么写:
addView(myView);
单从字面意思上看,这句代码仅仅提供了要添加的view的引用,完全没有任何关于这个view应当有多大,放在什么位置的信息。实际上,这些信息父view在背后帮你补上了。下面通过addView()方法的实现,来看看LayoutParams是如何发挥作用的。
首先是最简单的版本:
public void addView(View child) {
addView(child, -1);
}
没什么内容。继续看下去:
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
//获取子view的LayoutParams
LayoutParams params = child.getLayoutParams();
if (params == null) {
//如果子view没有设置LayoutParams,就为它生成一个默认的
params = generateDefaultLayoutParams();
//默认生成的LayoutParams不能为null
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
这里可以清楚地看到,父view会判断将要添加的子view有没有自带一个LayoutParams。如果没有的话,会调用generateDefaultLayoutParams()为它提供一个默认的。ViewGroup默认会提供一个width与height均为WRAP_CONTENT的LayoutParams。如果ViewGroup的子类实现了自己的LayoutParams,一般需要覆盖该方法。
public void addView(View child, int index, LayoutParams params) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
//重新进行measure与layout
requestLayout();
//重绘界面
invalidate(true);
addViewInner(child, index, params, false);
}
添加新的子view之后自然需要重新执行measure、layout与draw。重点在addViewInner()方法:
private void addViewInner(View child, int index, LayoutParams params,boolean preventRequestLayout) {
//省略大量代码...
//确认子View的LayoutParams是否合法
if (!checkLayoutParams(params)) {
//如果不合法,则提取这个params中有用的信息,并生成一个合法的LayoutParams
params = generateLayoutParams(params);
}
//省略大量代码...
}
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p != null;
}
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return p;
}
checkLayoutParams()与generateLayoutParams()方法一般也是需要重写的。可以看出,ViewGroup在确保子view的LayoutParams合法这一方面下了很大功夫。这也充分体现了LayoutParams的重要性。
最后补充一个方法:
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
这个方法在ViewGroup中没有直接调用到,它的作用是用xml中加载的属性生成一个LayoutParams对象。如果你想要给自己的LayoutParams添加自定义属性,就必须同时覆盖这个方法。例:
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MyLayout.LayoutParams(getContext(),attrs);
}
关于如何为自己的ViewGroup实现LayoutParams,前面部分已经讲的差不多了,只差一步——为LayoutParams提供自定义属性。这部分其实和为view提供自定义属性的方法是一样的:首先在attrs.xml中声明declare-styleable,然后在里面设置属性的名称与数据类型,最后在LayoutParams的构造器中提取。可以参考自定义View之添加自定义属性 。
最后将步骤重新整理一遍:
(1)attrs.xml中声明declare-styleable,并设置属性的名称与数据类型;
(2)创建LayoutParams的子类,并在其构造器中提取自定义属性;
(3)覆盖ViewGroup的generateDefaultLayoutParams()方法为子view提供默认LayoutParams实现
(4)覆盖ViewGroup的checkLayoutParams()验证子view的LayoutParams是否合法。
(5)覆盖两个重载版本的generateLayoutParams()方法。