Android LayoutParams详解「建议收藏」

365账户受到限制怎么办 🗓 2026-01-03 14:04:19 ✍ admin 👁 8780 👍 576
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_width和layout_height属性。主要源码如下:

代码语言:javascript复制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的主要源码如下:

代码语言:javascript复制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方法的重载版本:

代码语言:javascript复制/** * 重载方法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中的代码实现如下:

代码语言:javascript复制protected LayoutParams generateDefaultLayoutParams() {

return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);

}实际上,addView的前四个重载方法最终都会调用第五个重载版本,即addView(View child, int index, LayoutParams params)。在这个方法中调用了requestLayout和invalidate方法,引起视图重新布局(onMeasure->onLayout->onDraw)和重绘。这也很好理解,既然我们添加了新的View,那么原有的视图结构自然会发生变化。同时,在这个方法中还调用了addViewInner方法,关键代码如下:

代码语言:javascript复制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是否为空:

代码语言:javascript复制protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {

return p != null;

}如果LayoutParams不合法,将使用generateLayoutParams方法对其进行转化,ViewGroup中这个方法仅仅将传入的LayoutParams原样返回:

代码语言:javascript复制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首先会被解析为对应的实例化对象,这项工作将通过LayoutInflater的inflate方法完成。inflater方法有多个重载版本,最终将会调用inflate(XmlPullParser parser,ViewGroup root, boolean attachToRoot),关键代码如下:

代码语言:javascript复制/** * 解析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,示例代码如下:

代码语言:javascript复制

这里将declare-styleable的name设置为SimpleViewGroup_Layout,也就是自定义ViewGroup的名称加上_Layout。这里一共定义了两个属性,第一个属性使用了自定义的名称,需要提供name和format参数,format用于限制自定义属性的类型;第二个属性使用了系统预置的属性,比如这里的android:layout_gravity,好处是可以让用户使用熟悉的属性(在系统提供的属性语义合适时可以考虑这种方式)。不过要注意,这种情况下就不要为它定义format参数了,因为系统已经设置好了。

之后,需要在自定义的LayoutParams中解析这些属性,下面是一个简单的示例:

代码语言:javascript复制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相关的方法,示例代码如下:

代码语言:javascript复制// 检查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.MarginLayoutParamsFrameLayout.LayoutParamsLinearLayout.LayoutParamsRelativeLayout.LayoutParamsRecyclerView.LayoutParamsGridLayoutManager.LayoutParamsStaggeredGridLayoutManager.LayoutParamsViewPager.LayoutParamsWindowManager.LayoutParams参考资料 https://blog.csdn.net/yisizhu/article/details/51582622

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

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

相关推荐

【深度】大选后,美国新总统将如何影响世界?
世界杯365软件

【深度】大选后,美国新总统将如何影响世界?

🗓 07-30 👁 7064
从化15个A级景区,你最爱“打卡”哪一个?
365bet投注在线

从化15个A级景区,你最爱“打卡”哪一个?

🗓 12-24 👁 4357