Android开发LayoutParams知识点总结

在Android开发中,LayoutParams使用的场景相对来说比较少,总结一下也就三大方面:一是自定义ViewGroup时要获取子元素的LayoutParams来完成测量和布局流程;二是动态的给ViewGroup添加一个子View;三是动态改变子元素布局参数来实现滑动效果。虽然说它的使用频率并不高,但是它对我们深入理解View的工作原理上具有重要的作用,本文将结合源码介绍LayoutParams的相关知识。

LayoutParams是什么,有什么用

LayoutParams翻译成汉语就是布局参数,每个控件都有自己的LayoutParams,而且子元素通过自己的LayoutParams来告诉父容器如何摆放自己。这里的摆放又包含了两层含义:一是确定子元素的大小(因为首先要知道大小才能进行摆放);二是确定子元素在父容器中的位置。因为任何一个控件都不是独立存在的,都要被添加到ViewGroup中才能显示在屏幕上,所以LayoutParams跟父容器是分不开的,因此脱离了父容器来讨论LayoutParams没有任何意义。

就其本质来说,LayoutParams的根本作用就是为View的3大流程服务的,关于3大流程,可以参考之前的一篇文章View的三大流程分析。回想一下View测量流程中的2个方法:

protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();
    //父容器根据子元素的布局参数和父容器自己的MeasureSpec计算子元素的MeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height);
	//根据计算好的MeasureSpec调用子元素的measure方法进入了子元素的测量流程
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    //父容器根据子元素的布局参数和父容器自己的MeasureSpec计算子元素的MeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin+ widthUsed, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin+ heightUsed, lp.height);
    //根据计算好的MeasureSpec调用子元素的measure方法进入了子元素的测量流程
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

可以看到,计算子元素MeasureSpec的第一步就是要获取其LayoutParams,之后将LayoutParams通过getChildMeasureSpec方法转换成具体的数值。在ViewGroup的布局流程中,如果考虑margin,同样也要先获取子元素的布局参数。

LayoutParams及其派生子类

上面已经说过了,摆放自己有两个含义,一是确定大小,二是确定位置。跟大小相关的布局参数有两个:android:layout_width和android:layout_height;跟位置相关的布局参数就比较多了:android:layout_margin、android:layout_gravity、android:layout_weight、android:layout_alignParentStart等等。当然了,使用android:layout_weight的前提是父容器必须是LinearLayout,使用android:layout_alignParentStart的前提是父容器必须是RelativeLayout。

根据以上的分析我们知道,子元素除了一些自己具有的参数android:layout_width、android:layout_height和android:layout_margin之外,根据父容器的不同还可以有一些特殊的参数,这样就意味着布局参数这个概念在Java代码中会有不同的类去实现。而这些不同的类都有一个共同的父类,就是ViewGroup.LayoutParams,它的重要代码如下:

	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;

		//这个构造方法用于根据布局文件中的android:layout_width和android:layout_height创建LayoutParams
        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();
        }

		//这个构造方法根据指定的宽高创建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是一个ViewGroup的一个静态内部类,它保存了View的宽高信息,其对应着xml文件中的android:layout_width和android:layout_height属性。因为一个控件的宽高其父容器相关,所以这个属性被命名为android:layout_width/height而不是android:width,前面已经提过了,脱离了父容器,这些属性也就没了意义。

当然了,仅有宽高两个属性并不能确定控件在父容器中的位置,所以就产生了一个新的类MarginLayoutParams,它在LayoutParams的基础上增加了margin属性。MarginLayoutParams的主要代码如下:

	public static class MarginLayoutParams extends ViewGroup.LayoutParams {
       
        public int leftMargin;     
        public int topMargin;
        public int rightMargin;
        public int bottomMargin;

        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);
                    if (leftMargin == UNDEFINED_MARGIN) {
                        mMarginFlags |= LEFT_MARGIN_UNDEFINED_MASK;
                        leftMargin = DEFAULT_MARGIN_RESOLVED;
                    }
                    rightMargin = a.getDimensionPixelSize(
                            R.styleable.ViewGroup_MarginLayout_layout_marginRight,
                            UNDEFINED_MARGIN);
                    if (rightMargin == UNDEFINED_MARGIN) {
                        mMarginFlags |= RIGHT_MARGIN_UNDEFINED_MASK;
                        rightMargin = DEFAULT_MARGIN_RESOLVED;
                    }
                }

                startMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginStart,
                        DEFAULT_MARGIN_RELATIVE);
                endMargin = a.getDimensionPixelSize(
                        R.styleable.ViewGroup_MarginLayout_layout_marginEnd,
                        DEFAULT_MARGIN_RELATIVE);

                if (verticalMargin >= 0) {
                    topMargin = verticalMargin;
                    bottomMargin = verticalMargin;
                } else {
                    topMargin = a.getDimensionPixelSize(
                            R.styleable.ViewGroup_MarginLayout_layout_marginTop,
                            DEFAULT_MARGIN_RESOLVED);
                    bottomMargin = a.getDimensionPixelSize(
                            R.styleable.ViewGroup_MarginLayout_layout_marginBottom,
                            DEFAULT_MARGIN_RESOLVED);
                }      
            }

            ……

            a.recycle();
        }
  
    }

代码有点长,但还是很好理解的。因为MarginLayoutParams是继承自LayoutParams的,所以首先还是设置宽和高,之后就是设置margin了。设置margin的过程遵循着由上到下的过程,这一点从代码中也可以看出来。如果在xml中指定了android:layout_margin属性,那么上下左右边距都会被赋值成这个margin值;如果没设置android:layout_margin而是设置了android:layout_marginVertical或者android:layout_marginHorizontal属性,那么就将上下边距或左右边距指定为对应的值;如果xml文件中没有上述3个属性,则会依次设置android:layout_marginLeft/Right/Bottom/Top这4个属性。

由这个过程我们可以得出结论:上述几个属性的优先级为:margin > horizontalMargin和verticalMargin > leftMargin和RightMargin、topMargin和bottomMargin,优先级高的属性会覆盖掉低的。还要注意一点的是,如果我们动态的改变这些边距值,那么还要通过控件的setLayoutParams方法对其进行更新

MarginLayoutParams之后就是一些具体容器特有的布局参数类了,比如说LinearLayout.LayoutParams、RelativeLayout.LayoutParams、FrameLayout.LayoutParams等等,它们都是继承自ViewGroup.MarginLayoutParams,这里我们拿出LinearLayout.LayoutParams看看:

	public static class LayoutParams extends ViewGroup.MarginLayoutParams {
	
		@ViewDebug.ExportedProperty(category = "layout")
        @InspectableProperty(name = "layout_weight")
        public float weight;
        /**
         * Gravity for the view associated with these LayoutParams.
         *
         * @see android.view.Gravity
         */
        @ViewDebug.ExportedProperty(category = "layout", mapping = {
            @ViewDebug.IntToString(from =  -1,                       to = "NONE"),
            @ViewDebug.IntToString(from = Gravity.NO_GRAVITY,        to = "NONE"),
            @ViewDebug.IntToString(from = Gravity.TOP,               to = "TOP"),
            @ViewDebug.IntToString(from = Gravity.BOTTOM,            to = "BOTTOM"),
            @ViewDebug.IntToString(from = Gravity.LEFT,              to = "LEFT"),
            @ViewDebug.IntToString(from = Gravity.RIGHT,             to = "RIGHT"),
            @ViewDebug.IntToString(from = Gravity.START,             to = "START"),
            @ViewDebug.IntToString(from = Gravity.END,               to = "END"),
            @ViewDebug.IntToString(from = Gravity.CENTER_VERTICAL,   to = "CENTER_VERTICAL"),
            @ViewDebug.IntToString(from = Gravity.FILL_VERTICAL,     to = "FILL_VERTICAL"),
            @ViewDebug.IntToString(from = Gravity.CENTER_HORIZONTAL, to = "CENTER_HORIZONTAL"),
            @ViewDebug.IntToString(from = Gravity.FILL_HORIZONTAL,   to = "FILL_HORIZONTAL"),
            @ViewDebug.IntToString(from = Gravity.CENTER,            to = "CENTER"),
            @ViewDebug.IntToString(from = Gravity.FILL,              to = "FILL")
        })
        @InspectableProperty(
                name = "layout_gravity",
                valueType = InspectableProperty.ValueType.GRAVITY)
        public int gravity = -1;

        /**
         * {@inheritDoc}
         */
        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
            ……
        }
    }

可以看到,里面定义了LinearLayout中一些自己特有的属性android:layout_gravity和android:layout_weight。对于其他容器的LayoutParams,里面也定义了它们特有的属性。

布局参数的创建和跟View联系的建立

明白了布局参数LayoutParams的意义和作用之后,那么它是怎么和一个具体的控件建立关联的呢?View给我们提供了一个setLayoutParams方法,该方法执行完毕之后在测量和布局流程中我们就可以通过getLayoutParams获取到这个布局参数了。

那么LayoutParams是怎样被创建出来的呢?这要分为两种情况:定义在xml文件中控件布局参数的创建动态创建控件时布局参数的创建

动态创建View时布局参数的创建过程

先来说说比较简单的动态创建View时布局参数是如何被创建的。由于控件是我们动态创建的,所以它没有父容器,我们想要让它显示在屏幕上就要让它成为一个容器的子元素,ViewGroup提供了addView方法来实现这一功能,该方法有几个重载:

//重载1:直接给ViewGroup添加一个子元素,默认添加到ViewGroup的末尾
public void addView(View child) {
    addView(child, -1);
}

//重载2:将View添加到指定位置index
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中,默认添加到ViewGroup的末尾
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添加到ViewGroup中,默认添加到ViewGroup的末尾
@Override
public void addView(View child, LayoutParams params) {
    addView(child, -1, params);
}

//重载5:根据传入的布局参数将View添加到ViewGroup的指定位置index
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);
}

对于前两个重载方法由于没有指定LayoutParams,所以它们在内部会调用generateDefaultLayoutParams方法来为它创建一个LayoutParams。当然这也有个前提,就是传进来的View本身没有LayoutParams。在源码中我们也可以看到,如果child.getLayoutParams为空,那么才会通过generateDefaultLayoutParams创建默认的布局参数。

对于第3个重载方法,在源码中可以看到首先通过generateDefaultLayoutParams创建默认的布局参数,之后再将其宽高设置为指定值。

对于后两个重载方法,由于在调用的时候就已经指定了布局参数,所以在内部调用addViewInner将View添加到ViewGroup中即可。在源码中可以发现,前4个方法最终都会调用第5个重载方法。

说了这么多,我们来看看如果调用时没指定布局参数而且View本身也不带布局参数时,ViewGroup是怎么为子元素创建默认的布局参数的,我们点开ViewGroup的generateDefaultLayoutParams方法:

	protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

非常简单,ViewGroup默认为其子元素创建的布局参数在宽高方向上都是wrap_content的。但是很可惜,这个方法几乎不会被用到,因为对于一个具体的容器,都重写了这个方法。这里我们点开FrameLayout的generateDefaultLayoutParams方法看一下:

	@Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

FrameLayou默认创建的布局参数宽高都是match_parent的,对于其他的容器,创建的原则也有所不同。

布局参数创建好了之后,在第5个重载方法中调用了requestLayout()和invalidate(true)方法来重新激活3大流程。这一点是必然的,因为既然添加了新的子元素,那么原有视图就会改变,因此3大流程就要重新走一遍。之后就调用了addViewInner方法将子元素放到父容器中了,我们点开它看一看:

	private void addViewInner(View child, int index, LayoutParams params, boolean preventRequestLayout) {

        ……
        
        //判断传入的child是否已经有父容器了
        if (child.getParent() != null) {
            throw new IllegalStateException("The specified child already has a parent. " +
                    "You must call removeView() on the child's parent first.");
        }
        
        //判断传入的布局参数是否为空
        if (!checkLayoutParams(params)) {
            params = generateLayoutParams(params);
        }

		//判断child需不需要重新布局
        if (preventRequestLayout) {
            child.mLayoutParams = params;  //需要
        } else {
            child.setLayoutParams(params);  //不需要
        }

        if (index < 0) {
            index = mChildrenCount;
        }

		//将child添加到父容器中
        addInArray(child, index);

        // 指定了child的父容器为this
        if (preventRequestLayout) {
            child.assignParent(this);
        } else {
            child.mParent = this;
        }
        
       ……
       
    }

代码中的注释把addInner方法的流程描述的很清楚了,这样一来动态创建的View布局参数创建过程就说完了,接下来看看我们编写xml文件来做app界面时,文件中控件的布局参数是如何被创建的。

xml文件中View的布局参数创建过程

Android开发之LayoutInflater及其源码分析Android开发Activity的setContentView源码分析这俩篇文章中已经说过了,Activity的setContentView方法中会调用LayoutInflater的inflate方法将xml中定义的各种控件解析成对象,最终将布局的根ViewGroup添加到mContentParent中来,其代码如下:

/**
 * 解析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;
}

代码中可以看到如果root不为空,则调用generateLayoutParams为子元素创建布局参数,而generateLayoutParams方法是根据xml文件中子元素的属性值来创建LayoutParams的,这一点根创建默认布局参数不同。

总的来说,就是Activity中调用setContentView方法,通过LayoutInflater将整个XML文件解析为View树,从根布局开始为每个View和ViewGroup设置相应的LayoutParams。

这样一来布局参数的创建过程也就说完了,有了以上的理论过程,就可以来讨论一下自定义布局参数了。

自定义布局参数

在自定义ViewGroup时如果想让它支持我们自定义的一些布局属性,就可以用自定义布局参数来实现。自定义的ViewGroup要继承自ViewGroup.MarginLayoutParams,否则这个自定义的ViewGroup将不会支持子元素的margin属性。此外因为这些属性是我们自定义的,那么就要在attr.xml文件中定义好它们,示例代码如下:

<resources>
	<!-- 自定义属性集 -->
    <declare-styleable name="MyViewGroup_Layout">
        <!-- 自定义的属性 -->
        <attr name="my_layout_xxx" format="integer"/>
        <!-- 使用系统预置的属性 -->
        <attr name="android:layout_gravity"/>
    </declare-styleable>
</resources>

之后就是在自定义的布局参数中解析这些属性:

public static class LayoutParams extends ViewGroup.MarginLayoutParams {
    public int my_layout_xxx;
    public int gravity;

    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
        // 解析布局属性
        TypedArray typedArray = c.obtainStyledAttributes(attrs, R.styleable.MyViewGroup_Layout);
        simpleAttr = typedArray.getInteger(R.styleable.MyViewGroup_Layout_my_layout_xxx, 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);
}

这几个方法的作用之前已经提过了。如果不重写的话,那么LayoutInflater在解析xml文件时为子元素创建的布局参数的类型就是ViewGroup.LayoutParams,这样使用如下代码强制转换时就会产生类型不匹配异常:

MyViewGroup.LayoutParams lp = (MyViewGroup.LayoutParams) child.getLayoutParams();

同理,就算不自定义布局参数,如果想让自定义的ViewGroup支持子元素的margin属性,也要重写上面4个方法并在其中返回MarginLayoutParams的实例,否则就会在下面的代码中产生类型不匹配异常:

MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

本文参考自:Android LayoutParams详解自定义控件知识储备-LayoutParams的那些事

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值