提示:本文的源码均取自Android 7.0
前言
在平时的开发过程中,我们一般是通过XML文件去定义布局,所以对于LayoutParams的使用可能相对较少。但是在需要动态改变View的布局参数(比如宽度、位置)时,就必须要借助这个重要的类了。本文将结合具体源码详细讲解LayoutParams的相关知识。
基础知识
LayoutParams是什么
LayoutParams翻译过来就是布局参数,子View通过LayoutParams告诉父容器(ViewGroup)应该如何放置自己。从这个定义中也可以看出来LayoutParams与ViewGroup是息息相关的,因此脱离ViewGroup谈LayoutParams是没有意义的。
事实上,每个ViewGroup的子类都有自己对应的LayoutParams类,典型的如LinearLayout.LayoutParams和FrameLayout.LayoutParams等,可以看出来LayoutParams都是对应ViewGroup子类的内部类。
最基础的LayoutParams是定义在ViewGroup中的静态内部类,封装着View的宽度和高度信息,对应着XML中的layout_width
和layout_height
属性。主要源码如下:
public static class LayoutParams {
public static final int FILL_PARENT = -1;
public static final int MATCH_PARENT = -1;
public static final int WRAP_CONTENT = -2;
public int width;
public int height;
......
/**
* XML文件中设置的以layout_开头的属性将在这个方法中解析
*/
public LayoutParams(Context c, AttributeSet attrs) {
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
// 解析width和height属性
setBaseAttributes(a,
R.styleable.ViewGroup_Layout_layout_width,
R.styleable.ViewGroup_Layout_layout_height);
a.recycle();
}
/**
* 使用传入的width和height构建LayoutParams
*/
public LayoutParams(int width, int height) {
this.width = width;
this.height = height;
}
/**
* 通过传入的LayoutParams构建新的LayoutParams
*/
public LayoutParams(LayoutParams source) {
this.width = source.width;
this.height = source.height;
}
......
}
弄清楚了LayoutParams的意义,就可以解释为什么在XML中View的某些属性是以layout_
开头的了。因为这些属性并不直接属于View,而是属于这些View的LayoutParams,这样的命名方式也就显得很贴切了。
MarginLayoutParams
在ViewGroup中还定义一个LayoutParams的子类——MarginLayoutParams。从名字就可以猜出来,MarginLayoutParams是和外间距有关的。事实也确实如此,和LayoutParams相比,MarginLayoutParams只是增加了对上下左右外间距的支持。实际上大部分LayoutParams的实现类都是继承自MarginLayoutParams,因为基本所有的父容器都是支持子View设置外间距的。MarginLayoutParams的主要源码如下:
public static class MarginLayoutParams extends ViewGroup.LayoutParams {
/**
* The left margin in pixels of the child. Margin values should be positive.
* Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value
* to this field.
*/
public int leftMargin;
public int topMargin;
public int rightMargin;
public int bottomMargin;
/**
* 解析XML中以layout_开头的属性
*/
public MarginLayoutParams(Context c, AttributeSet attrs) {
super();
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
setBaseAttributes(a,
R.styleable.ViewGroup_MarginLayout_layout_width,
R.styleable.ViewGroup_MarginLayout_layout_height);
int margin = a.getDimensionPixelSize(
com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
if (margin >= 0) {
leftMargin = margin;
topMargin = margin;
rightMargin= margin;
bottomMargin = margin;
} else {
int horizontalMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginHorizontal, -1);
int verticalMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginVertical, -1);
if (horizontalMargin >= 0) {
leftMargin = horizontalMargin;
rightMargin = horizontalMargin;
} else {
leftMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
UNDEFINED_MARGIN);
rightMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginRight,
UNDEFINED_MARGIN);
}
.........
a.recycle();
}
}
从源码中也可以看到,MarginLayoutParams主要就是增加了上下左右4种外间距。在构造方法中,先是获取了margin属性;如果该值不合法,就获取horizontalMargin;如果该值不合法,再去获取leftMargin和rightMargin属性(verticalMargin、topMargin和bottomMargin同理)。我们可以据此总结出这几种属性的优先级:
margin > horizontalMargin和verticalMargin > leftMargin和RightMargin、topMargin和bottomMargin
优先级更高的属性会覆盖掉优先级较低的属性。此外,还要注意一下这几种属性上的注释:
Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value
也就是说,如果我们更改了MarginLayoutParams中这几种属性的值,就应该调用View的setLayoutParams
方法重新设置更改后的MarginLayoutParams,这样我们所做的更改才会生效(其实主要是因为在setLayoutParams方法中调用了requestLayout
方法)。
LayoutParams与View如何建立联系
说了这么多LayoutParams的作用,这里再简单谈一下LayoutParams是何时被创建出来的,又是怎样和View建立联系。归纳起来,View的使用方式无非有两种:在XML中定义View和在Java代码中直接生成View对应的实例对象,因此我们也分这两个方向进行探索。
在Java代码中实例化View
在代码中实例化View后,如果调用setLayoutParams
方法为View设置指定的LayoutParams,那么LayoutParams就已经和View建立起联系了。针对不同的ViewGroup子类,我们要选择合适的LayoutParams。
实例化View后,一般还会调用addView
方法将View对象添加到指定的ViewGroup中。可以想到,在ViewGroup中肯定也会为还没有LayoutParams的子View设置合适的LayoutParams,下文将通过分析代码说明这一过程。ViewGroup实现了以下五种addView方法的重载版本:
/**
* 重载方法1:添加一个子View
* 如果这个子View还没有LayoutParams,就为子View设置当前ViewGroup默认的LayoutParams
*/
public void addView(View child) {
addView(child, -1);
}
/**
* 重载方法2:在指定位置添加一个子View
* 如果这个子View还没有LayoutParams,就为子View设置当前ViewGroup默认的LayoutParams
* @param index View将在ViewGroup中被添加的位置(-1代表添加到末尾)
*/
public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();// 生成当前ViewGroup默认的LayoutParams
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
/**
* 重载方法3:添加一个子View
* 使用当前ViewGroup默认的LayoutParams,并以传入参数作为LayoutParams的width和height
*/
public void addView(View child, int width, int height) {
final LayoutParams params = generateDefaultLayoutParams(); // 生成当前ViewGroup默认的LayoutParams
params.width = width;
params.height = height;
addView(child, -1, params);
}
/**
* 重载方法4:添加一个子View,并使用传入的LayoutParams
*/
@Override
public void addView(View child, LayoutParams params) {
addView(child, -1, params);
}
/**
* 重载方法4:在指定位置添加一个子View,并使用传入的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");
}
// addViewInner() will call child.requestLayout() when setting the new LayoutParams
// therefore, we call requestLayout() on ourselves before, so that the child's request
// will be blocked at our level
requestLayout();
invalidate(true);
addViewInner(child, index, params, false);
}
以上代码已经添加了必要的注释,这里就不再赘述了。总之,只要子View没有LayoutParams,ViewGroup就会为其设置默认的LayoutParams。默认的LayoutParams对象通过generateDefaultLayoutParams
方法生成,ViewGroup中的代码实现如下:
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
实际上,addView的前四个重载方法最终都会调用第五个重载版本,即addView(View child, int index, LayoutParams params)。在这个方法中调用了requestLayout
和invalidate
方法,引起视图重新布局(onMeasure->onLayout->onDraw)和重绘。这也很好理解,既然我们添加了新的View,那么原有的视图结构自然会发生变化。同时,在这个方法中还调用了addViewInner
方法,关键代码如下:
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
.....
if (mTransition != null) {
mTransition.addChild(this, child);
}
if (!checkLayoutParams(params)) { // ① 检查传入的LayoutParams是否合法
params = generateLayoutParams(params); // 如果传入的LayoutParams不合法,将进行转化操作
}
if (preventRequestLayout) { // ② 是否需要阻止重新执行布局流程
child.mLayoutParams = params; // 这不会引起子View重新布局(onMeasure->onLayout->onDraw)
} else {
child.setLayoutParams(params); // 这会引起子View重新布局(onMeasure->onLayout->onDraw)
}
if (index < 0) {
index = mChildrenCount;
}
addInArray(child, index);
// tell our children
if (preventRequestLayout) {
child.assignParent(this);
} else {
child.mParent = this;
}
.....
}
可以看到,在代码①的位置先判断传入的LayoutParams是否合法,ViewGroup中这个方法只是简单判断了传入的LayoutParams是否为空:
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p != null;
}
如果LayoutParams不合法,将使用generateLayoutParams
方法对其进行转化,ViewGroup中这个方法仅仅将传入的LayoutParams原样返回:
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return p;
}
最后,在代码②的位置为子View设置LayoutParams。这里分为了两种情况:如果不希望引起子View重新布局(onMeasure->onLayout->onDraw)就直接为子View的LayoutParams变量赋值;否则调用子View的setLayoutParams方法传入LayoutParams。
到这一步,LayoutParams和View的联系就建立起来了。
在XML中定义View
在XML中定义的View首先会被解析为对应的实例化对象,这项工作将通过LayoutInflater
的inflate
方法完成。inflater方法有多个重载版本,最终将会调用inflate(XmlPullParser parser,ViewGroup root, boolean attachToRoot),关键代码如下:
/**
* 解析XML文件中的View
* @param parser 解析器
* @param root 父容器(可能为null)
* @param attachToRoot View是否需要附加到父容器中
*/
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
......
View result = root;
......
final String name = parser.getName();
if (TAG_MERGE.equals(name)) { // 针对<merge>标签
......
} else { // 针对普通标签
// ① 通过XML生成对应的View对象
// Temp指的是XML文件中的根View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
// ② 通过XML中的布局参数生成对应的LayoutParams
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// ③ 如果不需要将View附加到父容器中,就直接为View设置LayoutParams
temp.setLayoutParams(params);
}
}
rInflateChildren(parser, temp, attrs, true); // 解析View中包含的子View(如果存在的话)
// ④ 如果父容器不为null,且需要将View附加到父容器中,就使用addView方法
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
......
return result;
}
可以看到,如果父容器(ViewGroup)不为空,在代码②的位置将通过父容器的generateLayoutParams方法生成LayoutParams,这也间接说明了LayoutParams是与ViewGroup息息相关的,脱离ViewGroup谈LayoutParams是没有意义的。
在代码③的位置,如果attachToRoot
参数为false,代表不需要将View添加到父容器中,那就直接为View设置LayoutParams;否则在代码④的位置通过addView(temp, params)将View添加到父容器中。到了这一步,后续逻辑就和在Java代码中实例化View是一样的了。
其实最典型的例子就是在Activity中调用setContentView
方法,系统会通过LayoutInflater将整个XML文件解析为View Tree,从根布局开始为每个View和ViewGroup设置相应的LayoutParams。
自定义LayoutParams
如果我们需要自定义ViewGroup的话,一般也会自定义LayoutParams,这样可以提供一些个性化的布局参数。为了支持设置外间距,自定义的LayoutParams一般会选择继承ViewGroup.MarginLayoutParams。此外,还需要在XML文件中定义declare-styleable
资源属性,一般会创建一个名为attrs.xml
文件放置这些属性。这里假设我们要实现一个名为SimpleViewGroup的自定义ViewGroup,示例代码如下:
<resources>
<declare-styleable name="SimpleViewGroup_Layout">
<!-- 自定义的属性 -->
<attr name="layout_simple_attr" format="integer"/>
<!-- 使用系统预置的属性 -->
<attr name="android:layout_gravity"/>
</declare-styleable>
</resources>
这里将declare-styleable
的name设置为SimpleViewGroup_Layout,也就是自定义ViewGroup的名称加上_Layout
。这里一共定义了两个属性,第一个属性使用了自定义的名称,需要提供name
和format
参数,format用于限制自定义属性的类型;第二个属性使用了系统预置的属性,比如这里的android:layout_gravity
,好处是可以让用户使用熟悉的属性(在系统提供的属性语义合适时可以考虑这种方式)。不过要注意,这种情况下就不要为它定义format参数了,因为系统已经设置好了。
之后,需要在自定义的LayoutParams中解析这些属性,下面是一个简单的示例:
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
public int simpleAttr;
public int gravity;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
// 解析布局属性
TypedArray typedArray = c.obtainStyledAttributes(attrs, R.styleable.SimpleViewGroup_Layout);
simpleAttr = typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_layout_simple_attr, 0);
gravity=typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_android_layout_gravity, -1);
typedArray.recycle();//释放资源
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(MarginLayoutParams source) {
super(source);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
最后,我们还需要重写ViewGroup中几个与LayoutParams相关的方法,示例代码如下:
// 检查LayoutParams是否合法
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof SimpleViewGroup.LayoutParams;
}
// 生成默认的LayoutParams
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new SimpleViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
// 对传入的LayoutParams进行转化
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new SimpleViewGroup.LayoutParams(p);
}
// 对传入的LayoutParams进行转化
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new SimpleViewGroup.LayoutParams(getContext(), attrs);
}
这些方法的作用已经在前文介绍过了,同时代码中也添加了注释,这里就不再赘述了。
LayoutParams常见的子类
在为View设置LayoutParams的时候需要根据它的父容器选择对应的LayoutParams,否则结果可能与预期不一致,这里简单罗列一些常见的LayoutParams子类:
- ViewGroup.MarginLayoutParams
- FrameLayout.LayoutParams
- LinearLayout.LayoutParams
- RelativeLayout.LayoutParams
- RecyclerView.LayoutParams
- GridLayoutManager.LayoutParams
- StaggeredGridLayoutManager.LayoutParams
- ViewPager.LayoutParams
- WindowManager.LayoutParams