View布局参数LayoutParams
为了计算布局实现动效和LayoutParams密切接触有一段时间,LayoutParams彻底玩明白了。
1、LayoutParams
LayoutParams直译为布局参数,是定义在ViewGroup中的静态内部类。
1.1、LayoutParams用途
LayoutParams类的注释清楚的写明此类的用途:LayoutParams是子View用来告诉它们的父布局如何放置自己。
/** * LayoutParams are used by views to tell their parents how they want to be * laid out. See * {@link android.R.styleable#ViewGroup_Layout ViewGroup Layout Attributes} * for a list of all child view attributes that this class supports. */
1.2、继承关系
定义在ViewGroup中的LayoutParams类是各种子LayoutParams的基类,只定义View的两个基本属性:宽度和高度。
/** * There are subclasses of LayoutParams for different subclasses of * ViewGroup. For example, AbsoluteLayout has its own subclass of * LayoutParams which adds an X and Y value.</p> * * <div class="special reference"> * <h3>Developer Guides</h3> * <p>For more information about creating user interface layouts, read the * <a href="{@docRoot}guide/topics/ui/declaring-layout.html">XML Layouts</a> developer * guide.</p></div> * * @attr ref android.R.styleable#ViewGroup_Layout_layout_height * @attr ref android.R.styleable#ViewGroup_Layout_layout_width */
继承抽象类ViewGroup的容器实现类非常多,比如常见的LinearLayout、RelativeLayout、FrameLayout等等。
有些容器在继承ViewGroup的同时,还会继承ViewGroup.LayoutParams实现自己的LayoutParams。或者直接使用父类的LayoutParams。
2、LayoutParams类详解
通过前面简单的了解,定义在ViewGroup中的LayoutParams基类只有宽高两个基本信息。
2.1、宽 & 高
ViewGroup中的LayoutParams基类只定义了View的两个基本属性:width和height,控件的宽和高。
/** * Information about how wide the view wants to be. Can be one of the * constants FILL_PARENT (replaced by MATCH_PARENT * in API Level 8) or WRAP_CONTENT, or an exact size. */ public int width; /** * Information about how tall the view wants to be. Can be one of the * constants FILL_PARENT (replaced by MATCH_PARENT * in API Level 8) or WRAP_CONTENT, or an exact size. */ public int height; /** * 用于动画布局。 */ public LayoutAnimationController.AnimationParameters layoutAnimationParameters;
关于ViewGroup.LayoutParams中对于View宽高的说明如下,值只能是以下三种情况之一:①MATCH_PARENT、②WRAP_CONTENT、③具体的值。
/** * <p> * The base LayoutParams class just describes how big the view wants to be * for both width and height. For each dimension, it can specify one of: * <ul> * <li>FILL_PARENT (renamed MATCH_PARENT in API Level 8 and higher), which * means that the view wants to be as big as its parent (minus padding) * <li> WRAP_CONTENT, which means that the view wants to be just big enough * to enclose its content (plus padding) * <li> an exact number * </ul> */
有没有很熟悉?这也是XML布局中最常用的两个属性,以layout_前缀加上布局参数的属性。看到以layout_前缀开头的属性,就应该知道这个属性来自LayoutParams。
<TextView android:layout_width="match_parent" android:layout_height="18dp" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" />
2.2、MATCH_PARENT和WRAP_CONTENT
ViewGroup.LayoutParams中定义了两个描述MATCH_PARENT和WRAP_CONTENT的常量。对于已废弃的FILL_PARENT直接忽略,用MATCH_PARENT替代。
/** * Special value for the height or width requested by a View. * FILL_PARENT means that the view wants to be as big as its parent, * minus the parent's padding, if any. This value is deprecated * starting in API Level 8 and replaced by {@link #MATCH_PARENT}. */ @SuppressWarnings({"UnusedDeclaration"}) @Deprecated public static final int FILL_PARENT = -1; /** * Special value for the height or width requested by a View. * MATCH_PARENT means that the view wants to be as big as its parent, * minus the parent's padding, if any. Introduced in API Level 8. */ public static final int MATCH_PARENT = -1; /** * Special value for the height or width requested by a View. * WRAP_CONTENT means that the view wants to be just large enough to fit * its own internal content, taking its own padding into account. */ public static final int WRAP_CONTENT = -2;
2.3、LayoutParams的三个构造方法
ViewGroup.LayoutParams有三个公有构造方法,用来构造ViewGroup.LayoutParams实例。LayoutParams子类也要实现这三个父类构造方法。
/** * Creates a new set of layout parameters. The values are extracted from * the supplied attributes set and context. The XML attributes mapped * to this set of layout parameters are: * * <ul> * <li><code>layout_width</code>: the width, either an exact value, * {@link #WRAP_CONTENT}, or {@link #FILL_PARENT} (replaced by * {@link #MATCH_PARENT} in API Level 8)</li> * <li><code>layout_height</code>: the height, either an exact value, * {@link #WRAP_CONTENT}, or {@link #FILL_PARENT} (replaced by * {@link #MATCH_PARENT} in API Level 8)</li> * </ul> * * @param c the application environment * @param attrs the set of attributes from which to extract the layout * parameters' values */ 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(); }
用已知的宽、高构造ViewGroup.LayoutParams对象。LayoutParams子类中也有类似的构造方法。
/** * Creates a new set of layout parameters with the specified width * and height. * * @param width the width, either {@link #WRAP_CONTENT}, * {@link #FILL_PARENT} (replaced by {@link #MATCH_PARENT} in * API Level 8), or a fixed size in pixels * @param height the height, either {@link #WRAP_CONTENT}, * {@link #FILL_PARENT} (replaced by {@link #MATCH_PARENT} in * API Level 8), or a fixed size in pixels */ public LayoutParams(int width, int height) { this.width = width; this.height = height; }
用已有的LayoutParams作为参数构造新的LayoutParams对象,俗称复制构造函数。
/** * Copy constructor. Clones the width and height values of the source. * * @param source The layout params to copy from. */ public LayoutParams(LayoutParams source) { this.width = source.width; this.height = source.height; }
还有一个无参构造方法,框架内部使用,应用开发者无法使用,了解一下即可。
/** * Used internally by MarginLayoutParams. * @hide */ @UnsupportedAppUsage LayoutParams() { }
3、派生LayoutParams
不能直接使用ViewGroup.LayoutParams类,往往使用其子类。在第1.2节中就已经提到过,在众多继承ViewGroup的容器类中,也有不少派生LayoutParams类。
3.1、MarginLayoutParams
绝大部分派生LayoutParams并不是直接继承自ViewGroup.LayoutParams,而是继承自ViewGroup.MarginLayoutParams。给原本只有width和height的布局参数,增加了四个额外的参数:left、top、right、bottom,表示View上下左右四个方向的空白边距。
public static class MarginLayoutParams extends ViewGroup.LayoutParams { public int leftMargin; public int topMargin; public int rightMargin; public int bottomMargin; private int startMargin = DEFAULT_MARGIN_RELATIVE; private int endMargin = DEFAULT_MARGIN_RELATIVE; ... ... }
这也是除了2.1节中提到的两个属性之外,在XML布局中常用到的几个layout_属性:
<TextView android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="30dp" android:layout_marginHorizontal="20dp" android:layout_marginVertical="20dp" android:layout_marginLeft="10dp" android:layout_marginTop="10dp" android:layout_marginRight="10dp" android:layout_marginBottom="10dp" android:layout_marginStart="10dp" android:layout_marginEnd="10dp" />
后来为了更好的支持RTL,带有强烈固定方向含义的left和right被start和end取代。建议不要用含有left和start的属性,而应该用含有start和end的属性,可以更好的支持RTL。
最后,来看一下ViewGroup.MarginLayoutParams的构造函数,从这里面可以更加清晰了解到前面布局中设置的几个margin属性设置流程:
①如果设置了layout_margin,那么用这个作为四个方向的统一边距值,即便设置了其它四个属性也不会生效。
②如果设置了垂直layout_marginVertical,那么layout_marginTop和layout_marginBottom属性将不起作用。
③同样的,如果设置了水平layout_marginHorizontal,那么layout_marginLeft和layout_marginRight属性将不起作用。
④layout_marginStart和layout_marginEnd不受除layout_margin之外的属性影响。
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); //如果设置了layout_margin,那么用这个作为四个方向的统一边距值,即便设置了其它四个属性也不会生效。 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); } if (isMarginRelative()) { mMarginFlags |= NEED_RESOLUTION_MASK; } } ... }
3.2、自定义LayoutParams
开发者可以继承ViewGroup.LayoutParams抽象类或者ViewGroup.MarginLayoutParams类,实现自己的LayoutParams布局参数。
public class QuibblerView extends ViewGroup { ... public static class LayoutParams extends ViewGroup.MarginLayoutParams { int front; int behind; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.QuibblerView_Layout); front = a.getLayoutDimension(R.styleable.QuibblerView_Layout_layout_front, "layout_front"); behind = a.getLayoutDimension(R.styleable.QuibblerView_Layout_layout_behind, "layout_behind"); a.recycle(); } public LayoutParams(int width, int height) { super(width, height); } public LayoutParams(ViewGroup.LayoutParams source) { super(source); } } }
再或者省点事,继承现有容器的LayoutParams,只需新增属性方法实现功能。比如直接继承LinearLayout中的LinearLayout.LayoutParams,在很多继承自LinearLayout的容器中也常常看到这样的做法:直接使用父容器的LayoutParams,或者继承父容器的LayoutParams。
public static class LayoutParams extends LinearLayout.LayoutParams { public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); } public LayoutParams(int width, int height) { super(width, height); } public LayoutParams(ViewGroup.LayoutParams source) { super(source); } }
为了让自定义布局参数更方便些,再自定义一些xml属性。关于自定义</declare-styleable>属性详见《自定义View属性》一文。
<declare-styleable name="QuibblerView_Layout"> <attr name="layout_front" format="dimension" /> <attr name="layout_behind" format="dimension" /> </declare-styleable>
在LayoutParams构造方法中解析设置的xml属性:
public static class LayoutParams extends ViewGroup.MarginLayoutParams { int front; int behind; public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.QuibblerView_Layout); front = a.getLayoutDimension(R.styleable.QuibblerView_Layout_layout_front, "layout_front"); behind = a.getLayoutDimension(R.styleable.QuibblerView_Layout_layout_behind, "layout_behind"); a.recycle(); } ... }
4、使用LayoutParams
开发者无需自己构造LayoutParams设置给View,给View设置LayoutParams是该View所在父容器的责任,在随后文章中会详细讲到。
4.1、获取View的布局参数
通常是直接通过View的getLayoutParams()方法获取View中已有的布局参数:
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
每个View中都定义了一个ViewGroup.LayoutParams类型的属性成员mLayoutParams;
public ViewGroup.LayoutParams getLayoutParams() { return mLayoutParams; }
4.2、修改布局参数
然后重新设置一些布局参数,如宽高、margin等。对于ViewGroup.LayoutParams布局参数也只有宽高属性可以设置。
layoutParams.height = height ; layoutParams.width = width ;
如需设置额外的属性,需要将从View拿到的ViewGroup.LayoutParams类型转换成View所在父布局中LayoutParams的类型或其超类。比如,在LinearLayout容器中的View,其布局参数类型为LinearLayout.LayoutParams。可以进行如下操作:
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) view.getLayoutParams(); layoutParams.setMargins(left, top, right, bottom); layoutParams.bottomMargin = 20; layoutParams.gravity = Gravity.END;
4.3、设置View的布局参数
最后,将设置过的LayoutParams再重新设置给View。有些开发者会遇到不设置不生效的情况,或者不二次设置也能生效。
view.setLayoutParams(layoutParams);
先说第一种情况,在setLayoutParams(layoutParams)方法中,调用了requestLayout()刷新重新布局,使新的LayoutParams生效。关于requestLayout()详见View的requestLayout()流程一文。
public void setLayoutParams(ViewGroup.LayoutParams params) { if (params == null) { throw new NullPointerException("Layout parameters cannot be null"); } mLayoutParams = params; resolveLayoutParams(); if (mParent instanceof ViewGroup) { ((ViewGroup) mParent).onSetLayoutParams(this, params); } requestLayout(); }
为什么不调用setLayoutParams(layoutParams)方法,也能使修改后的LayoutParams生效?是因为LayoutParams是在布局阶段之前设置的,比如在onCreate()中进行操作,即便没有再次setLayoutParams(layoutParams),也会在后续的布局阶段生效。
相关资料:
reference/android/view/ViewGroup.LayoutParams
reference/android/view/ViewGroup.MarginLayoutParams