Android LayoutParams详解「建议收藏」

大家好,又见面了,我是你们的朋友全栈君。

提示:本文的源码均取自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_widthlayout_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)。在这个方法中调用了requestLayoutinvalidate方法,引起视图重新布局(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首先会被解析为对应的实例化对象,这项工作将通过LayoutInflaterinflate方法完成。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。这里一共定义了两个属性,第一个属性使用了自定义的名称,需要提供nameformat参数,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

参考资料

https://blog.csdn.net/yisizhu/article/details/51582622

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/190888.html原文链接:https://javaforall.cn

未经允许不得转载:木盒主机 » Android LayoutParams详解「建议收藏」

赞 (0)

相关推荐

    暂无内容!