测量 View 就是测量一个矩形
透过另一个视角来观察,所有的 Widget,我们使用的小控件都是Widget。如果TextView和Buttton等
因此,自定义 View 的第一步,我们要在心里默念 – 我们现在要确定一个矩形了!
既然是矩形,那么它肯定有明确的宽高和位置坐标,宽高是在测量阶段得出。然后在布局阶段,确定好位置信息对矩形进行布局,之后的视觉效果就交给绘制流程了,我们是最好的画家。
布局绘画涉及两个过程:测量过程和布局过程。测量过程通过measure方法实现,是View树自顶向下的遍历,每个View在循环过程中将尺寸细节往下传递,当测量过程完成之后,所有的View都存储了自己的尺寸。第二个过程则是通过方法layout来实现的,也是自顶向下的。在这个过程中,每个父View负责通过计算好的尺寸放置它的子View。
好了,我们知道了测量的就是长和宽,我们的目的也就是长和宽。
View 设置尺寸的基本方法
接下来的过程,我将会用一系列比较细致的实验来说明问题,我们先看看在 Android 中使用 Widget 的时候,怎么定义大小。比如我们要在屏幕上使用一个 Button。
这样屏幕上就出现了一个按钮。
我们再把宽高固定。
再换一种情况,将按钮的宽度由父容器决定
上面就是我们日常开发中使用的步骤,通过 layout_width 和 layout_height 属性来设置一个 View 的大小。而在 xml 中,这两个属性有 3 种取值可能。
android:layout_height="wrap_content" //View 本身的内容决定高度
android:layout_height="match_parent" //与父视图等高
android:layout_height="fill_parent" //与父视图等高
android:layout_height="100dip" //精确设置高度值为 100dip
我们再进一步,现在给 Button 找一个父容器进行观察。父容器背景由特定颜色标识。
一
二
可以看到 RelativeLayout 包裹着 Button。我们再换一种情况。
三
四
似乎发生了不怎么愉快的事情,Button 想要的长度是 1000 dp,而 RelativeLayout 最终给予的却仍旧是在自己的有限范围参数内。就好比山水庄园向光明开发区政府要地 1 万亩,政府说没有这么多,最多 2000 亩。
Button 是一个 View,RelativeLayout 是一个 ViewGroup。那么对于一个 View 而言,它相当于山水庄园,而 ViewGroup 类似于政府的角色。View 芸芸众生,它们的多姿多彩构成了美丽的 Android 世界,ViewGroup 却有自己的规划,所谓规划也就是以大局为重嘛,尽可能协调管辖区域内各个成员的位置关系。
山水庄园拿地盖楼需要同政府协商沟通,自定义一个 View 也需要同它所处的 ViewGroup 进行协商。
那么,它们的协议是什么?
View 和 ViewGroup 之间的测量协议 MeasureSpec
我们自定义一个 View,onMeasure()是一个关键方法。也是本文重点研究内容。测量自己的大小,为正式布局提供建议。(注意,只是建议,至于用不用,要看onLayout);
public class TestView extends View {
public TestView(Context context) {
super(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
onMeasure() 中有两个参数 widthMeasureSpec、heightMeasureSpec。它们是什么?看起来和宽高有关。
它们确实和宽高有关,了解它们需要从一个类说起。MeasureSpec。
MeasureSpec
MeasureSpec 是 View.java 中一个静态类
/**
* MeasureSpec类的源码分析
**/
public class MeasureSpec {
// 进位大小 = 2的30次方
// int的大小为32位,所以进位30位 = 使用int的32和31位做标志位
private static final int MODE_SHIFT = 30;
// 运算遮罩:0x3为16进制,10进制为3,二进制为11
// 3向左进位30 = 11 00000000000(11后跟30个0)
// 作用:用1标注需要的值,0标注不要的值。因1与任何数做与运算都得任何数、0与任何数做与运算都得0
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
// UNSPECIFIED的模式设置:0向左进位30 = 00后跟30个0,即00 00000000000
// 通过高2位
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
// EXACTLY的模式设置:1向左进位30 = 01后跟30个0 ,即01 00000000000
public static final int EXACTLY = 1 << MODE_SHIFT;
// AT_MOST的模式设置:2向左进位30 = 10后跟30个0,即10 00000000000
public static final int AT_MOST = 2 << MODE_SHIFT;
/**
* makeMeasureSpec()方法
* 作用:根据提供的size和mode得到一个详细的测量结果吗,即measureSpec
**/
public static int makeMeasureSpec(int size, int mode) {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
// 设计目的:使用一个32位的二进制数,其中:第32和第31位代表测量模式(mode)、后30位代表测量大小(size)
}
/**
* getMode()方法
* 作用:通过measureSpec获得测量模式(mode)
**/
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
// 即:测量模式(mode) = measureSpec & MODE_MASK;
// MODE_MASK = 运算遮罩 = 11 00000000000(11后跟30个0)
//原理:保留measureSpec的高2位(即测量模式)、使用0替换后30位
// 例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值
}
/**
* getSize方法
* 作用:通过measureSpec获得测量大小size
**/
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
// size = measureSpec & ~MODE_MASK;
// 原理类似上面,即 将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size
}
}
MeasureSpec 代表测量规则,而它的手段则是用一个 int 数值来实现。我们知道一个 int 数值有 32 bit。MeasureSpec 将它的高 2 位用来代表测量模式 Mode,低 30 位用来代表数值大小 Size。
- wrap_content-> MeasureSpec.AT_MOST
- match_parent -> MeasureSpec.EXACTLY
- 具体值 -> MeasureSpec.EXACTLY
实际使用
/**
* MeasureSpec类的具体使用
**/
// 1. 获取测量模式(Mode)
int specMode = MeasureSpec.getMode(measureSpec)
// 2. 获取测量大小(Size)
int specSize = MeasureSpec.getSize(measureSpec)
// 3. 通过Mode 和 Size 生成新的SpecMode
int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);
上面讲了那么久MeasureSpec,那么MeasureSpec值到底是如何计算得来?
结论:子View的MeasureSpec值根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,具体计算逻辑封装在getChildMeasureSpec()里。如下图:
- 子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定
下面,我们来看getChildMeasureSpec()的源码分析:
/**
* 源码分析:getChildMeasureSpec()
* 作用:根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
* 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定
**/
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//参数说明
* @param spec 父view的详细测量值(MeasureSpec)
* @param padding view当前尺寸的的内边距和外边距(padding,margin)
* @param childDimension 子视图的布局参数(宽/高)
//父view的测量模式
int specMode = MeasureSpec.getMode(spec);
//父view的大小
int specSize = MeasureSpec.getSize(spec);
//通过父view计算出的子view = 父大小-边距(父要求的大小,但子view不一定用这个值)
int size = Math.max(0, specSize - padding);
//子view想要的实际大小和模式(需要计算)
int resultSize = 0;
int resultMode = 0;
//通过父view的MeasureSpec和子view的LayoutParams确定子view的大小
// 当父View的模式为EXACITY时,父view强加给子View确切的值
//一般是父View设置为match_parent或者固定值的ViewGroup
switch (specMode) {
case MeasureSpec.EXACTLY:
// 当子View的LayoutParams>0,即有确切的值
if (childDimension >= 0) {
//子View大小为子自身所赋的值,模式大小为EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
// 当子View的LayoutParams为MATCH_PARENT时(-1)
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//子view大小为父view大小,模式为EXACTLY
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
// 当子view的LayoutParams为WRAP_CONTENT时(-2)
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//子view决定自己的大小,但最大不能超过父view,模式为AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 当父View的模式为AT_MOST时,父view强加给子View一个最大的值。(一般是父view设置为wrap_content)
case MeasureSpec.AT_MOST:
// 道理同上
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 当父View的模式为UNSPECIFIED时,父容器不对View有任何限制,要多大给多大
// 多见于ListView、GridView
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// 子view大小为子自身所赋的值
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 因为父View为UNSPECIFIED,所以MATCH_PARENT的话子类大小为0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 因为父view为UNSPECIFIED,所以WRAP_CONTENT的话子类大小为0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
MeasureSpec.UNSPECIFIED
子元素告诉父容器它的宽高想要多大就要多大,你不要限制我(自己的事情自己做主,没有任何限制)。一般开发者几乎不需要处理这种情况,在 ScrollView 或者是 AdapterView 中都会处理这样的情况。所以我们可以忽视它。本文中的示例,基本上会跳过它。
MeasureSpec.EXACTLY
此模式说明可以给子元素一个精确的数值
MeasureSpec.AT_MOST
当一个 View 的 layout_width 或者 layout_height 的取值为 wrap_content 时,它的测量模式就是 MeasureSpec.AT_MOST。
此模式下,子 View 希望它的宽或者高由自己决定。ViewGroup 当然要尊重它的要求,但是也有个前提,那就是子视图不能超过ViewGroup 提供的最大值,也就是它期望宽高不能超过父类提供的建议宽高。(自己的事情只能在一个范围内做主)
了解上面的测量模式后,我们就要动手编写实例来验证一些想法了。
自定义 View
我的目标是定义一个文本框,中间显示黑色文字,背景色为绿色。
我们可以轻松地进行编码。首先,我们定义好它需要的属性,然后编写它的 java 代码。
attrs.xml
TestView.java
public class TestView extends View {
private int mTextSize;
private TextPaint mPaint;
private String mText;
public TestView(Context context) {
this(context,null);
}
public TestView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public TestView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.TestView);
mText = ta.getString(R.styleable.TestView_android_text);
mTextSize = ta.getDimensionPixelSize(R.styleable.TestView_android_textSize,24);
ta.recycle();
mPaint = new TextPaint();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.BLACK);
mPaint.setTextSize(mTextSize);
mPaint.setTextAlign(Paint.Align.CENTER);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int cx = (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
int cy = (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;
canvas.drawColor(Color.RED);
if (TextUtils.isEmpty(mText)) {
return;
}
canvas.drawText(mText,cx,cy,mPaint);
}
}
布局文件
我们可以看到在自定义 View 的 TestView 代码中,我们并没有做测量有关的工作,因为我们根本就没有复写它的 onMeasure() 方法。但它却完成了任务,给定 layout_width 和 layout_height 两个属性明确的值之后,它就能够正常显示了。我们再改变一下数值。
将 layout_width 的值改为 match_parent,所以它的宽是由父类决定,但同样它也正常。
我们已经知道,上面的两种情况其实就是对应 MeasureSpec.EXACTLY 这种测量模式,在这种模式下 TestView 本身不需要进行处理。
那么有人会问,如果 layout_width 或者 layout_height 的值为 wrap_content 的话,那么会怎么样呢?
我们继续测试观察。
效果和前面的一样,宽度和它的 ViewGroup 同样了。我们再看。
宽度正常,高度却和 ViewGroup 一样了。
再看一种情况
这次可以看到,宽高都和 ViewGroup 一致了。
但是,这不是我想要的啊!
wrap_content 对应的测量模式是 MeasureSpec.AT_MOST,所以它的第一要求就是 size 是由 View 本身决定,最大不超过 ViewGroup 能给予的建议数值。
TestView 如果在宽高上设置 wrap_content 属性,也就代表着,它的大小由它的内容决定,在这里它的内容其实就是它中间位置的字符串。显然上面的不符合要求,那么就显然需要我们自己对测量进行处理。
我们的思路可以如下:
- 对于 MeasureSpec.EXACTLY 模式,我们不做处理,将 ViewGroup 的建议数值作为最终的宽高。
- 对于 MeasureSpec.AT_MOST 模式,我们要根据自己的内容计算宽高,但是数值不得超过 ViewGroup 给出的建议值。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
/**resultW 代表最终设置的宽,resultH 代表最终设置的高*/
int resultW = widthSize;
int resultH = heightSize;
int contentW = 0;
int contentH = 0;
/**重点处理 AT_MOST 模式,TestView 自主决定数值大小,但不能超过 ViewGroup 给出的
* 建议数值
* */
if (widthMode == MeasureSpec.AT_MOST) {
if (!TextUtils.isEmpty(mText)) {
contentW = (int) mPaint.measureText(mText);
contentW += getPaddingLeft() + getPaddingRight();
resultW = Math.min(contentW, widthSize);
}
}
if (heightMode == MeasureSpec.AT_MOST) {
if (!TextUtils.isEmpty(mText)) {
contentH = mTextSize;
contentH += getPaddingTop() + getPaddingBottom();
resultH = Math.min(contentH, heightSize);
}
}
//一定要设置这个函数,不然会报错
setMeasuredDimension(resultW, resultH);
}
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int cx = getPaddingLeft() + (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
int cy = getPaddingTop() + (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;
Paint.FontMetrics metrics = mPaint.getFontMetrics();
cy += metrics.descent;
canvas.drawColor(Color.GREEN);
if (TextUtils.isEmpty(mText)) {
return;
}
canvas.drawText(mText, cx, cy, mPaint);
}
代码并不难,我们可以做验证。
在 MeasureSpec.EXACTLY 模式下同样没有问题。
现在,我们已经掌握了自定义 View 的测量方法,其实也很简单的嘛。
但是,还没有完。我们验证的刚刚是自定义 View,对于 ViewGroup 的情况是有些许不同的。
View 和 ViewGroup,鸡生蛋,蛋生鸡的关系
ViewGroup 是 View 的子类,但是 ViewGroup 的使命却是装载和组织 View。这好比是母鸡是鸡,母鸡下蛋是为了孵化小鸡,小鸡长大后如果是母鸡又下蛋,那么到底是蛋生鸡还是鸡生蛋?
自定义 View 的测量,我们已经掌握了,那现在我们编码来测试自定义 ViewGroup 时的测量变现。
假设我们要制定一个 ViewGroup,我们就给它起一个名字叫 TestViewGroup 好了,它里面的子元素按照对角线铺设,前面说过 ViewGroup 本质上也是一个 View,只不过它多了布局子元素的义务。既然是 View 的话,那么自定义一个 ViewGroup 也需要从测量开始,问题的关键是如何准确地得到这个 ViewGroup 尺寸信息?
我们还是需要仔细讨论。
- 当 TestViewGroup 测量模式为 MeasureSpec.EXACTLY 时,这时候的尺寸就可以按照父容器传递过来的建议尺寸。要知道 ViewGroup 也有自己的 parent,在它的父容器中,它也只是一个 View。
- 当 TestViewGroup 测量模式为 MeasureSpec.AT_MOST 时,这就需要 TestViewGroup 自己计算尺寸数值。就上面给出的信息而言,TestViewGroup 的尺寸非常简单,那就是用自身 padding + 各个子元素的尺寸(包含子元素的宽高+子元素设置的 marging )得到一个可能的尺寸数值。然后用这个尺寸数值与 TestViewGroup 的父容器给出的建议 Size 进行比较,最终结果取最较小值。
- 当 TestViewGroup 测量成功后,就需要布局了。自定义 View 基本上不要处理这一块,但是自定义 ViewGroup,这一部分却不可缺少。onLayout()是实现所有子控件布局的函数。注意,是所有子控件!那它自己的布局怎么办?后面我们再讲,先讲讲在onLayout()中我们应该做什么。
我们先看看ViewGroup onLayout()函数的默认行为是什么
在ViewGroup.java中
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
是一个抽象方法,说明凡是派生自ViewGroup的类都必须自己去实现这个方法。像LinearLayout、RelativeLayout等布局,都是重写了这个方法,然后在内部按照各自的规则对子视图进行布局的。
接下来,我们就可以具体编码了。
public class TestViewGroup extends ViewGroup {
public TestViewGroup(Context context) {
this(context,null);
}
public TestViewGroup(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public TestViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
//只关心子元素的 margin 信息,所以这里用 MarginLayoutParams
return new MarginLayoutParams(getContext(),attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
/**resultW 代表最终设置的宽,resultH 代表最终设置的高*/
int resultW = widthSize;
int resultH = heightSize;
/**计算尺寸的时候要将自身的 padding 考虑进去*/
int contentW = getPaddingLeft() + getPaddingRight();
int contentH = getPaddingTop() + getPaddingBottom();
/**对子元素进行尺寸的测量,这一步必不可少*/
measureChildren(widthMeasureSpec,heightMeasureSpec);
MarginLayoutParams layoutParams = null;
for ( int i = 0;i < getChildCount();i++ ) {
View child = getChildAt(i);
layoutParams = (MarginLayoutParams) child.getLayoutParams();
//子元素不可见时,不参与布局,因此不需要将其尺寸计算在内
if ( child.getVisibility() == View.GONE ) {
continue;
}
contentW += child.getMeasuredWidth()
+ layoutParams.leftMargin + layoutParams.rightMargin;
contentH += child.getMeasuredHeight()
+ layoutParams.topMargin + layoutParams.bottomMargin;
}
/**重点处理 AT_MOST 模式,TestViewGroup 通过子元素的尺寸自主决定数值大小,但不能超过
* ViewGroup 给出的建议数值
* */
if ( widthMode == MeasureSpec.AT_MOST ) {
resultW = contentW < widthSize ? contentW : widthSize;
}
if ( heightMode == MeasureSpec.AT_MOST ) {
resultH = contentH < heightSize ? contentH : heightSize;
}
//一定要设置这个函数,不然会报错
setMeasuredDimension(resultW,resultH);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int topStart = getPaddingTop();
int leftStart = getPaddingLeft();
int childW = 0;
int childH = 0;
MarginLayoutParams layoutParams = null;
for ( int i = 0;i < getChildCount();i++ ) {
View child = getChildAt(i);
layoutParams = (MarginLayoutParams) child.getLayoutParams();
//子元素不可见时,不参与布局,因此不需要将其尺寸计算在内
if ( child.getVisibility() == View.GONE ) {
continue;
}
childW = child.getMeasuredWidth();
childH = child.getMeasuredHeight();
leftStart += layoutParams.leftMargin;
topStart += layoutParams.topMargin;
child.layout(leftStart,topStart, leftStart + childW, topStart + childH);
leftStart += childW + layoutParams.rightMargin;
topStart += childH + layoutParams.bottomMargin;
}
}
}
然后我们将之添加进 xml 布局文件中进行测试。
再试验一下给 TestViewGroup 加上固定宽高。
结果如下:
自此,我们也知道了自定义 ViewGroup 的基本步骤,并且能够处理 ViewGroup 的各种测量模式。
但是,在现实工作开发过程中,需求是不定的,我上面讲的内容只是基本的规则,大家熟练于心的时候才能从容应对各种状况。
getMeasuredWidth()与getWidth()
趁热打铁,就这个例子,我们讲一个很容易出错的问题:getMeasuredWidth()与getWidth()的区别。他们的值大部分时间都是相同的,但意义确是根本不一样的,我们就来简单分析一下。
- 首先getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。
- getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过layout(left,top,right,bottom)方法设置的。
setMeasuredDimension()提供的测量结果只是为布局提供建议,最终的取用与否要看layout()函数。大家再看看我们上面写的TestViewGroup,是不是我们自己使用child.layout(leftStart,topStart, leftStart + childW, topStart + childH)来定义了各个子控件所应在的位置:
childW = child.getMeasuredWidth();
childH = child.getMeasuredHeight();
leftStart += layoutParams.leftMargin;
topStart += layoutParams.topMargin;
child.layout(leftStart, topStart, leftStart + childW, topStart + childH);
从代码中可以看到,我们使用child.layout(leftStart, topStart, leftStart + childW, topStart + childH);来布局控件的位置,其中getWidth()的取值就是这里的右坐标减去左坐标的宽度;因为我们这里的宽度是,leftStart + childW,而getMeasuredWidth()与getWidth()的值是一样的。如果我们在调用layout()的时候传进去的宽度值不与getMeasuredWidth()相同,那必然getMeasuredWidth()与getWidth()的值就不再一样了。
一定要注意的一点是:getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。再重申一遍!!!
TestViewGroup自己什么时候被布局
在onLayout()中布局它所有的子控件。那它自己什么时候被布局呢?它当然也有父控件,它的布局也是在父控件中由它的父控件完成的,就这样一层一层地向上由各自的父控件完成对自己的布局。真到所有控件的最顶层结点,在所有的控件的最顶部有一个ViewRoot,它才是所有控件的最终祖先结点。那让我们来看看它是怎么来做的吧。
/* final 标识符 , 不能被重载 , 参数为每个视图位于父视图的坐标轴
* @param l Left position, relative to parent
* @param t Top position, relative to parent
* @param r Right position, relative to parent
* @param b Bottom position, relative to parent
*/
public final void layout(int l, int t, int r, int b) {
boolean changed = setFrame(l, t, r, b); //设置每个视图位于父视图的坐标轴
if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
}
onLayout(changed, l, t, r, b);//回调onLayout函数 ,设置每个子视图的布局
mPrivateFlags &= ~LAYOUT_REQUIRED;
}
mPrivateFlags &= ~FORCE_LAYOUT;
在setFrame(l,t,r,b)就是设置自己的位置,设置结束以后才会调用onLayout(changed, l, t, r, b)来设置内部所有子控件的位置。
OK啦,到这里有关onMeasure()和onLayout()的内容就讲完啦,想必大家应该也对整个布局流程有了一个清楚的认识了,下面我们再看一个紧要的问题:如何得到自定义控件的左右间距margin值。
获取子控件Margin的方法
我会先简单粗暴的教大家怎么先获取到margin值,然后再细讲为什么这样写,他们的原理是怎样的。
如果要自定义ViewGroup支持子控件的layout_margin参数,则自定义的ViewGroup类必须重载generateLayoutParams()函数,并且在该函数中返回一个ViewGroup.MarginLayoutParams派生类对象,这样才能使用margin参数。
我们在上面TestViewGroup例子的基础上,添加上layout_margin参数;
我们在每个TestView中都添加了一android:layout_margin参数,而且值是10dp;背景也都分别改为了橙色,绿色和一张图片;
现在我们运行一上,看看效果:
我们在onLayout()中没有根据Margin来布局,当然不会出现有关Margin的效果啦。需要特别注意的是,如果我们在onLayout()中根据margin来布局的话,那么我们在onMeasure()中计算TestViewGroup的大小时,也要加上margin,不然会导致TestViewGroup太小,而控件显示不全的问题。费话不多说,我们直接看代码实现。
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
}
首先,在TestViewGroup在初始化子控件时,会调用LayoutParams generateLayoutParams(LayoutParams p)来为子控件生成对应的布局属性,但默认只是生成layout_width和layout_height所以对应的布局参数,即在正常情况下的generateLayoutParams()函数生成的LayoutParams实例是不能够取到margin值的。即:
/**
*从指定的XML中获取对应的layout_width和layout_height值
*/
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
/*
*如果要使用默认的构造方法,就生成layout_width="wrap_content"、layout_height="wrap_content"对应的参数
*/
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
所以,如果我们还需要margin相关的参数就只能重写generateLayoutParams()函数了:
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
由于generateLayoutParams()的返回值是LayoutParams实例,而MarginLayoutParams是派生自LayoutParam的;所以根据类的多态的特性,可以直接将此时的LayoutParams实例直接强转成MarginLayoutParams实例;
所以下面这句在这里是不会报错的:
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
大家也可以为了安全起见利用instanceOf来做下判断,如下:
MarginLayoutParams lp = null
if (child.getLayoutParams() instanceof MarginLayoutParams) {
lp = (MarginLayoutParams) child.getLayoutParams();
}
所以整体来讲,就是利用了类的多态特性!下面来看看MarginLayoutParams和generateLayoutParams()都做了什么。
generateLayoutParams()实现
//位于ViewGrop.java中
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
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();
}
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
width = a.getLayoutDimension(widthAttr, "layout_width");
height = a.getLayoutDimension(heightAttr, "layout_height");
}
从上面的代码中明显可以看出,generateLayoutParams()调用LayoutParams()产生布局信息,而LayoutParams()最终调用setBaseAttributes()来获得对应的宽,高属性。
这里是通过TypedArray对自定义的XML进行值提取的过程,难度不大,不再细讲。从这里也可以看到,generateLayoutParams生成的LayoutParams属性只有layout_width和layout_height的属性值。
下面再来看看MarginLayoutParams的具体实现,其实通过上面的过程,大家也应该想到,它也是通过TypeArray来解析自定义属性来获得用户的定义值的(大家看到长代码不要害怕,先列出完整代码,下面会分段讲):
public MarginLayoutParams(Context c, AttributeSet attrs) {
super();
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
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);
rightMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginRight,
UNDEFINED_MARGIN);
topMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginTop,
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);
}
a.recycle();
}
这段代码分为两部分:
第一部分:提取layout_margin的值并设置
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
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 {
…………
}
在这段代码中就是通过提取layout_margin的值来设置上,下,左,右边距的。
第二部分:如果用户没有设置layout_margin,而是单个设置的,那么就一个个提取,代码如下:
leftMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginLeft,
UNDEFINED_MARGIN);
rightMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginRight,
UNDEFINED_MARGIN);
topMargin = a.getDimensionPixelSize(
R.styleable.ViewGroup_MarginLayout_layout_marginTop,
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);
这里就是对layout_marginLeft、layout_marginRight、layout_marginTop、layout_marginBottom的值一个个提取的过程。
从这里大家也可以看到为什么非要重写generateLayoutParams()函数了,就是因为默认的generateLayoutParams()函数只会提取layout_width、layout_height的值,只有MarginLayoutParams()才具有提取margin间距的功能!!!!
TestViewGroup 作为一个演示用的例子,只为了说明测量规则和基本的自定义方法。对于 Android 开发初学者而言,还是要多阅读代码,关键是要多临摹别人的优秀的自定义 View 或者 ViewGroup。
我个人觉得,尝试自己动手去实现一个流式标签控件,对于提高自定义 ViewGroup 的能力是有很大的提高,因为只有在自己实践中思考,在思考和实验的过程你才会深刻的理解测量机制的用途。
不过自定义一个流式标签控件是另外一个话题了,也许我会另外开一篇来讲解,不过我希望大家亲自动手去实现它。
洋洋洒洒写了这么多的内容,其实基本上已经完结了,已经不耐烦的同学可以直接跳转到后面的总结。但是,对于有钻研精神的同学来讲,其实还不够。还没有完。
问题1:到底是谁在测量 View ?
问题2:到底是什么时候需要测量 View ?
针对问题 1:
我们在自定义 TestViewGroup 的时候,在 onMeasure() 方法中,通过了一个 API 对子元素进行了测量,这个 API 就是 measureChildren()。这个方法进行了什么样的处理呢?我们可以去看看。
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
/**
* 分析2:measureChild()
* 作用:a. 计算单个子View的MeasureSpec
* b. 测量每个子View最后的宽 / 高:调用子View的measure()
**/
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
// 1. 获取子视图的布局参数
final LayoutParams lp = child.getLayoutParams();
// 2. 根据父视图的MeasureSpec & 布局参数LayoutParams,计算单个子View的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,// 获取 ChildView 的 widthMeasureSpec
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,// 获取 ChildView 的 heightMeasureSpec
mPaddingTop + mPaddingBottom, lp.height);
// 3. 将计算好的子View的MeasureSpec值传入measure(),进行最后的测量
// 下面的流程即类似单一View的过程,此处不作过多描述
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
代码简短易懂,分别调用 child 的 measure() 方法。值得注意的是,传递给 child 的测量规格已经发生了变化,比如 widthMeasureSpec 变成了 childWidthMeasureSpec。原因是这两行代码:
一开始我们就了解子视图是如何测量的
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
我们继续向前,ViewGroup 的 measureChild() 方法最终会调用 View.measure() 方法。我们进一步跟踪。
/**
* 源码分析:measure()
* 定义:Measure过程的入口;属于View.java类 & final类型,即子类不能重写此方法
* 作用:基本测量逻辑的判断
**/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 参数说明:View的宽 / 高测量规格
...
int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
onMeasure(widthMeasureSpec, heightMeasureSpec);
// 计算视图大小 ->>分析1
} else {
...
}
/**
* 分析1:onMeasure()
* 作用:a. 根据View宽/高的测量规格计算View的宽/高值:getDefaultSize()
* b. 存储测量后的View宽 / 高:setMeasuredDimension()
**/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 参数说明:View的宽 / 高测量规格
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected int getSuggestedMinimumWidth() {
//mMinWidth = android:minWidth属性所指定的值;
return (mBackground == null) ? mMinWidth : max(mMinWidth,mBackground.getMinimumWidth());
}
//getSuggestedMinimumHeight()同理
/**
* 分析2:setMeasuredDimension()
* 作用:存储测量后的View宽 / 高
* 注:该方法即为我们重写onMeasure()所要实现的最终目的
**/
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
// 将测量后子View的宽 / 高值进行传递
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
// 由于setMeasuredDimension()的参数是从getDefaultSize()获得的
// 下面我们继续看getDefaultSize()的介绍
/**
* 分析3:getDefaultSize()
* 作用:根据View宽/高的测量规格计算View的宽/高值
**/
public static int getDefaultSize(int size, int measureSpec) {
// 参数说明:
// size:提供的默认大小
// measureSpec:宽/高的测量规格(含模式 & 测量大小)
// 设置默认大小
int result = size;
// 获取宽/高测量规格的模式 & 测量大小
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
// 模式为UNSPECIFIED时,使用提供的默认大小 = 参数Size
case MeasureSpec.UNSPECIFIED:
result = size;
break;
// 模式为AT_MOST,EXACTLY时,使用View测量后的宽/高值 = measureSpec中的Size
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
// 返回View的宽/高值
return result;
}
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
//返回背景图Drawable的原始宽度
return intrinsicWidth > 0 ? intrinsicWidth :0 ;
}
// 由源码可知:mBackground.getMinimumWidth()的大小 = 背景图Drawable的原始宽度
// 若无原始宽度,则为0;
// 注:BitmapDrawable有原始宽度,而ShapeDrawable没有
最后,我们在看看测量的流程图
Activity 中的道,最顶层的那个 View?
道生一,一生二,二生三,三生万物,万物负阴而抱阳,冲气以为和。– 《道德经》
我们已经知道,不管是对于 View 还是 ViewGroup 而言,测量的起始是 measure() 方法,沿着控件树一路遍历下去。那么,对于 Android 一个 Activity 而言,它的顶级 View 或者顶级 ViewGroup 是哪一个呢?
从 setContentView 说起
我们知道给 Activity 布局的时候,在 onCreate() 中设置 setContentView() 的资源文件就是我们普通开发者所能想到的比较顶层的 View 了。比如在 activity_main.xml 中设置一个 RelativeLayout,那么这个 RelativeLayout 就是 Activity 最顶层的 View 吗?谁调用它的 measure() 方法触发整个控件树的测量?
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID);
initActionBar();
}
public Window getWindow() {
return mWindow;
}
/**
* Abstract base class for a top-level window look and behavior policy. An
* instance of this class should be used as the top-level view added to the
* window manager. It provides standard UI policies such as a background, title
* area, default key processing, etc.
*
* The only existing implementation of this abstract class is
* android.policy.PhoneWindow, which you should instantiate when needing a
* Window. Eventually that class will be refactored and a factory method
* added for creating Window instances without knowing about a particular
* implementation.
*/
public abstract class Window {
}
public class PhoneWindow extends Window implements MenuBuilder.Callback {
}
可以看到,调用 Activity.setContentView() 其实就是调用 PhoneWindow.setContentView()。
PhoneWindow.java
@Override
public void setContentView(View view) {
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
if (mContentParent == null) {
installDecor();
} else {
mContentParent.removeAllViews();
}
mContentParent.addView(view, params);
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
注意,在上面代码中显示,通过 setContentView 传递进来的 view 被添加到了一个 mContentParent 变量上了,所以可以回答上面的问题,通过 setContentView() 中传递的 View 并不是 Activity 最顶层的 View。我们再来看看 mContentParent。
它只是一个 ViewGroup。我们再把焦点聚集到 installDecor() 这个函数上面。
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor();
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
// Set up decor part of UI to ignore fitsSystemWindows if appropriate.
mDecor.makeOptionalFitsSystemWindows();
mTitleView = (TextView)findViewById(com.android.internal.R.id.title);
if (mTitleView != null) {
mTitleView.setLayoutDirection(mDecor.getLayoutDirection());
if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
View titleContainer = findViewById(com.android.internal.R.id.title_container);
if (titleContainer != null) {
titleContainer.setVisibility(View.GONE);
} else {
mTitleView.setVisibility(View.GONE);
}
if (mContentParent instanceof FrameLayout) {
((FrameLayout)mContentParent).setForeground(null);
}
} else {
mTitleView.setText(mTitle);
}
} else {
mActionBar = (ActionBarView) findViewById(com.android.internal.R.id.action_bar);
}
}
}
代码很长,我删除了一些与主题无关的代码。这个方法体内引出了一个 mDecor 变量,它通过 generateDecor() 方法创建。DecorView 是 PhoneWindow 定义的一个内部类,实际上是一个 FrameLayout。
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
}
我们回到 generate() 方法
protected DecorView generateDecor() {
return new DecorView(getContext(), -1);
}
DecorView 怎么创建的我们已经知晓,现在看看 mContentParent 创建方法 generateLayout()。它传递进了一个 DecorView,所以它与 mDecorView 肯定有某种关系。
protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
WindowManager.LayoutParams params = getAttributes();
// Inflate the window decor.
// Embedded, so no decoration is needed.
layoutResource = com.android.internal.R.layout.screen_simple;
// System.out.println("Simple!");
mDecor.startChanging();
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
mDecor.finishChanging();
return contentParent;
}
原代码很长,我删除了一些繁琐的代码,整个流程变得很清晰,这个方法内 inflate 了一个 xml 文件,然后被添加到了 mDecorView。而 mContentParent 就是这个被添加进去的 view 中。
这个 xml 文件是 com.android.internal.R.layout.screen_simple,我们可以从 SDK 包中找出它来。
就是一个 LinearLayout ,方向垂直。2 个元素,一个是 actionbar,一个是 content。并且 ViewStub 导致 actionbar 需要的时候才会进行加载。
总之由以上信息,我们可以得到 Activity 有一个 PhoneWindow 对象,PhoneWindow 中有一个 DecorView,DecorView 内部有一个 LinearLayout,LinearLayout 中存在 id 为 android:id/content 的布局 mContentParent。 mContentParent加载Activity 通过 setContentView 传递进来的 View,所以整个结构呼之欲出。
注意:因为代码有删简,实际上 LinearLayout 由两部分组成,下面的是 Content 无疑,上面的部分不一定是 ActionBar,也可能是 title,不过这不影响我们,我们只需要记住 content 就好了。
DecorView 才是 Activity 中整个控件树的根。
谁测绘了顶级 View ?
既然 DecorView 是整个测绘的发起点,那么谁对它进行了测绘?谁调用了它的 measure() 方法,从而导致整个控件树自上至下的尺寸测量?
我们平常开发知道调用一个 View.requestLayout() 方法,可以引起界面的重新布局,那么 requestLayout() 干了什么?
我们再回到 PhoneWindow 的 setContentView() 中来。
public void setContentView(View view, ViewGroup.LayoutParams params) {
if (mContentParent == null) {
installDecor();
} else {
mContentParent.removeAllViews();
}
mContentParent.addView(view, params);
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}
我们看看 mContentParent.addView(view, params) 的时候发生了什么。
public void addView(View child, int index, LayoutParams params) {
if (DBG) {
System.out.println(this + " addView");
}
// 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);
}
我们这篇文章就到这里