Android浅谈LayoutParams

在平常开发中最常使用的两个属性是layout_width和layout_height,而且这两个属性在布局文件中是不可缺少的。查看源码知道上面的两个属性值其实是赋值给LayoutParams的,开发中最常用的一些View,如TextView、Button或者布局视图,所有基于View的控件都只有getWidth或者getHeight方法,但是没有setWidth或者setHeight方法。通过上面侧面反应出View其实是没有自己的大小的,而是说是由父亲ViewGroup限定的或者说是由ViewGroup分配的,所以布局文件中我们都是使用的"layout_"为前缀,而不是直接使用的width或者height。但是它的宽高属性是通过什么设置进去的呢?要解释这个就需要介绍LayoutParams,LayoutParams直接翻译过来就是布局参数的意思,下面我们看看LayoutParams是什么,它又做了哪些事情。

LayoutParams是什么

Google官方文档是这样介绍LayoutParams的:

LayoutParams are used by views to tell their parents how they want to be laid out...The base LayoutParams class just describes how big the view wants to be for both width and height...There are subclasses of LayoutParams for different subclasses of ViewGroup.

LayoutParams就是告诉它们的父亲如何布局它们...最基本的LayoutParams只有宽高属性...不同子类的ViewGroup的拥有LayoutParams的不同子类。

从上图可以看出来,所有ViewGroup控件的LayoutParams都直接或间接的继承自ViewGroup中的LayoutParams,LayoutParams是一个静态内部类,这从侧面也反应了LayoutParams与父控件是息息相关的。ViewGroup这个类中LayoutParams是一个基类,它很简单只提供了宽高属性。

在LinearLayout中的LayoutParams我们发现它继承自MarginLayoutParams,MarginLayoutParams又是什么鬼?

MarginLayoutParams

MarginLayoutParams看到名称我们也可以知道个大概了,ViewGroup中LayoutParams提供了宽高属性,这里再提供一个外边距属性,然后将来所有的继承自ViewGroup的子类也可以添加相应的属性到自己的LayoutParams中,原理就是这样的,所以LayoutParams根据不同的ViewGroup会有很多个。

由上图可以看到,除了提供了常用的leftMargin、rightMargin、topMargin和bottomMargin之外,还提供了两个私有属性startMargin和endMargin,分别提供了相应的setter和getter方法。startMargin和endMargin主要是为了提供对RTL设计的支持。

一般情况下,View开始部分就是左边,但是有的语言目前为止还是按照从右往左的顺序来书写的,例如阿拉伯语。在Android 4.2系统之后,Google在Android中引入了RTL布局,更好的支持了从右往左文字布局的显示。为了更好的兼容RTL布局,google推荐使用MarginStart和MarginEnd来替代MarginLeft和MarginRight,这样应用可以在正常的屏幕和从右往左显示文字的屏幕上都保持一致的用户体验。

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 {
		leftMargin = a.getDimensionPixelSize(
				R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
				UNDEFINED_MARGIN);
		//...
	}
	
	...
}

从上述构造方法中可以知道如果我们在布局文件中设置了layout_margin,这时候四个边界都会设置上外边距,而且此时会覆盖设置的layout_marginLeft或者layout_marginRight属性。

一些常用布局LayoutParams

开发中最常用的三种布局LinearLayout、FrameLayout和RelativeLayout,下面看一下这三种布局中LayoutParams的实现方式,这三种布局的LayoutParams都是直接继承自ViewGroup中的MarginLayoutParams。

LinearLayout

LinearLayout中加入了两个属性gravity和weight,所以线性布局中支持layout_gravity和layout_weight。LinearLayout的LayoutParams还是相对比较简单的,部分源码如下:

public static class LayoutParams extends ViewGroup.MarginLayoutParams {
	
	public float weight;
	public int gravity = -1;

	public LayoutParams(Context c, AttributeSet attrs) {
		super(c, attrs);
		TypedArray a =c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);

		weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);
		gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);

		a.recycle();
	}

	public LayoutParams(int width, int height) {
		super(width, height);
		weight = 0;
	}

	public LayoutParams(int width, int height, float weight) {
		super(width, height);
		this.weight = weight;
	}

	public LayoutParams(ViewGroup.LayoutParams p) {
		super(p);
	}

	public LayoutParams(ViewGroup.MarginLayoutParams source) {
		super(source);
	}

	public LayoutParams(LayoutParams source) {
		super(source);
		this.weight = source.weight;
		this.gravity = source.gravity;
	}

}

FrameLayout

FrameLayout中LayoutParams比LinearLayout的要简单一些,只有一个gravity属性,代码就不再贴出来了。

RelativeLayout

从截图上面大概看不出来RelativeLayout的LayoutParams有多么复杂,事实上它的LayoutParams可以说是最复杂的一个了,在使用过程中也知道RelativeLayout支持的属性最多了,如layout_toLeftOf、layout_above、layout_alignLeft等等。在RelativeLayout中它的
属性都被放在了一个rules的数组中了。部分源代码如下:

public static class LayoutParams extends ViewGroup.MarginLayoutParams {
        
	public LayoutParams(Context c, AttributeSet attrs) {
		super(c, attrs);
		//...
		TypedArray a = c.obtainStyledAttributes(attrs,com.android.internal.R.styleable.RelativeLayout_Layout);
		final int[] rules = mRules;
		
		final int N = a.getIndexCount();
		for (int i = 0; i < N; i++) {
			int attr = a.getIndex(i);
			switch (attr) {
				case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignWithParentIfMissing:
					alignWithParent = a.getBoolean(attr, false);
					break;
				case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toLeftOf:
					rules[LEFT_OF] = a.getResourceId(attr, 0);
					break;
				case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toRightOf:
					rules[RIGHT_OF] = a.getResourceId(attr, 0);
					break;
				case com.android.internal.R.styleable.RelativeLayout_Layout_layout_above:
					rules[ABOVE] = a.getResourceId(attr, 0);
					break;
				
				//...	
			}
		}
		//...
	}

	public void addRule(int verb) {
		mRules[verb] = TRUE;
		mInitialRules[verb] = TRUE;
		mRulesChanged = true;
	}

	public void removeRule(int verb) {
		mRules[verb] = 0;
		mInitialRules[verb] = 0;
		mRulesChanged = true;
	}

	public int getRule(int verb) {
		return mRules[verb];
	}

	public int[] getRules() {
		return mRules;
	}
	
	//...
	
}

最常用的三个布局都有各自的LayoutParams,如果自定义一个继承自ViewGroup的布局也都会定义一个与该布局相匹配的LayoutParams。但是LayoutParams定义完成之后是如何赋值到View中的呢?

LayoutParams如何设置到View

先从一个简单的示例讲起吧,在LinearLayout中使用Java代码动态添加一个Button,代码如下:

LinearLayout layout=(LinearLayout) findViewById(R.id.layout);
		
Button btn=new Button(this);
btn.setText("按钮");
//方式一
layout.addView(btn);

LinearLayout.LayoutParams params=new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);

//方式二
btn.setLayoutParams(params);
layout.addView(btn);

//方式三
layout.addView(btn, params);

//方式四
layout.addView(btn, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);

上面四种方式添加View都是来自父类ViewGroup,如果对代码有较高的敏感性,就可以猜测到这几个重载方法应该都是调用同一个方法,对部分参数提供默认值就可以了。

public void addView(View child) {
	addView(child, -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();
		if (params == null) {
			throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
		}
	}
	addView(child, index, params);
}

public void addView(View child, LayoutParams params) {
	addView(child, -1, params);
}

public void addView(View child, int index, LayoutParams params) {
	if (DBG) {
		System.out.println(this + " addView");
	}

	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);
}

在代码中直接调用addView(View child)方法事实上调用的是addView(View child, int index),带有index的方法对LayoutParams进行了有效性判断,如果为空,则调用generateDefaultLayoutParams()方法,该方法在LinearLayout中被重写了,代码如下:

protected LayoutParams generateDefaultLayoutParams() {
	if (mOrientation == HORIZONTAL) {
		return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
	} else if (mOrientation == VERTICAL) {
		return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
	}
	return null;
}

所以在线性布局LinearLayout中如果是垂直方向的,我们添加进去的View都是会沾满整个一行,布局属性默认是layout_width="match_parent"。addView方法最总调用的都是addView(View child, int index, LayoutParams params),然而在该方法中又调用了addViewInner方法,部分代码如下:

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

	//...
	if (!checkLayoutParams(params)) {
		params = generateLayoutParams(params);
	}

	if (preventRequestLayout) {
		child.mLayoutParams = params;
	} else {
		child.setLayoutParams(params);
	}
	
	//...
}

在addView方法中对LayoutParams是否为空进行了判断,如果为空则调用generateDefaultLayoutParams(),在这里又进行了进一步有效性检查,checkLayoutParams(ViewGroup.LayoutParams p),不符合条件则调用generateLayoutParams(ViewGroup.LayoutParams p)生成一个LayoutParams对象。从addView的流程可以看出,如果我们使用LayoutParams出错系统会进行一定的容错处理,如果LayoutParams不为空就不会报错,但是有些时候需要注意一下,子View有一个方法叫做setLayoutParams(ViewGroup.LayoutParams params)方法,在这种情况下系统并不会进行有效性判断,setLayoutParams代码如下:

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();
}

一旦调用requestLayout()方法ViewGroup变回自动进行重绘,但是在onMeasure方法中有下面一行代码:

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

此时如果我们set进去的一个LayoutParams不是与ViewGroup相对应的params就会抛出一个类型转换异常,异常信息如下:

java.lang.ClassCastException: android.view.ViewGroup$LayoutParams cannot be cast to android.widget.LinearLayout$LayoutParams

相信有一部分开发者肯定遇到过这中类型的异常吧,至少我遇到过这种情况。

上面看到如果调用了requestLayout()方法,UI会执行重绘,UI重绘就会调用onMeasure(有时候在ViewGroup中该方法也不一定会被调用)和onLayout方法,无论是在onMeasure还是onLayout方法中,都会使用到相应的LayoutParams,也就是说前面所做的一切生成的一个LayoutParams就是为了在这里使用。

自定义LayoutParams

多数情况下我们自定义ViewGroup基本上都不需要直接继承自ViewGroup,只需要继承现有的几个布局View就可以了,但是有些特殊情况需要直接继承自ViewGroup,如果继承ViewGroup此时就需要我们自己定义一个符合该ViewGroup的LayoutParams。通过上文的理解如果自定义一个ViewGroup,我们要自定义一个LayoutParams基本需要两个步骤:

步骤一:

继承MarginLayoutParams,新增自己需要的属性:

public static class LayoutParams extends ViewGroup.MarginLayoutParams {

	//属性

	//构造方法
	
	//部分逻辑方法

}

步骤二:

重写ViewGroup中部分与LayoutParams相关方法:

protected LayoutParams generateDefaultLayoutParams() {
	//...
}

protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
	//...
}

protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
	//...
}

参考资料

自定义控件知识储备-LayoutParams的那些事

教你搞定Android自定义ViewGroup

Android.Views.ViewGroup Class

评论

您确定要删除吗?删除之后不可恢复