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)) { // 针对标签
        ......
    } 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

你可能感兴趣的:(Android进阶)