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的那些事。
本文地址:https://blog.csdn.net/weixin_44965650/article/details/107705820