您的位置:首页 > 移动开发 > Android开发

【Android小品】从使用出发完全理解View(ViewGroup)测量机制,并分析部分源码(修复图片)

2016-11-29 17:06 483 查看
在学习过程中在自定义View、ViewGroup的时候经常会碰到尺寸测量方面的问题。开始我以为已经理解了测量机制,实际上发现理解是错误的,Android的View测量机制真的有点“迷”。市面上把onMeasure()和onLayout()分开来分析的文章有很多,但是把二者结合起来,并且梳理整套逻辑的文章比较少。因此我打算自己动手,把View的测量机制搞清楚!

一、梳理背景知识

1.LayoutParms

我们知道,在XML文件里面,所有的View和ViewGroup都可以(其实是必须)指定下面两个属性。根据官方文档来看总共有三种情况

android:layout_width="xxx"
android:layout_height="xxx"






1. wrap_content

尺寸应该由内容的大小决定(例如,TextView的高度可以由内部的文本行数、字号决定,可以理解为,View自己根据内容的大小确定自己的大小)

2. match_parent(API 8 以前老版本叫做FILL_PARENT)

尺寸由它的父亲决定(例如,TextView的宽度我们往往希望和整个屏幕宽度对齐)

3. 具体的数字(可以带单位,不过系统在读取时会自动转换为像素值)

这个很好理解,指定多大就多大

这些XML属性实际上对应的是LayoutParms类



根据文档:

概念一、LayoutParms是view用来告诉上一级Viewgroup,View自己想要怎么进行测量。(或者说使用View的开发者想要怎么测量)

2.MeasureSpec



MeasureSpec包含了父空间对子控件的测量要求。每个对象都包含了两方面信息,尺寸(宽度、高度)、模式(UNSPECIFIED、EXACTLY、AT_MOST)

UNSPECIFIED

父亲没有指定任何约束

EXACTLY

父亲已经指定了具体的尺寸

AT_MOST

子View像多大就多大,但是要小于最大值

概念二、MeasureSpec只是一个容器对象,可以通过它的getxxx(…)传入一个int型变量(key),获取一些信息(value)

我们等会儿也顺便看一下MeasureSpec是如何实现的

3.与开发者相关的一些函数

注意:这里只是罗列,之后会具体的分析这些函数的作用和差异,现在只需要大概了解即可,无需深究

View

(View)v.onMeasure(…)

![这里写图片描述](https://img-blog.csdn.net/20161126203328535)

onMeasure是让view自己测量出自己的width、height。在复写本方法之后,必须调用**setMeasuredDimension(测量完毕的宽,测量完毕的高)** ,否则会报异常。子view应该保证测量出来的宽高分别大于**getSuggestedMinimumWidth()**、**getSuggestedMinimumHeight()**

(View)v.getMeasured[Width/Height](…)

![这里写图片描述](https://img-blog.csdn.net/20161126204339328)

![这里写图片描述](https://img-blog.csdn.net/20161126204631536)
顾名思义,记录了上次测量后的结果

(View)v.get[Width/Height](…)

![这里写图片描述](https://img-blog.csdn.net/20161126204945855)

![这里写图片描述](https://img-blog.csdn.net/20161126204850261)
返回view的真实大小

(View)v.layout(…)

![这里写图片描述](https://img-blog.csdn.net/20161128201554571)

设置view的真实边界

ViewGroup

(ViewGroup)onLayout(…)

![这里写图片描述](https://img-blog.csdn.net/20161128135504903)

当系统需要本ViewGroup指定View的位置时被调用

(ViewGroup)measureChild(…)

![这里写图片描述](https://img-blog.csdn.net/20161128135428715)

(ViewGroup)measureChildren(…)

![这里写图片描述](https://img-blog.csdn.net/20161128135535498)

(ViewGroup)measureChildWithMargins(…)

![这里写图片描述](https://img-blog.csdn.net/20161128135700938)

二、Log跟踪测量流程

我们自己实现两个类,复写一些我们感兴趣的方法打Log来试试看。
(为了让大家看的更清楚,代码中剔除了一些不太重要的代码,大家可以看注释来大概理解一下代码就好了,没什么特别)

布局文件

<?xml version="1.0" encoding="utf-8"?>
<com.xttech.onmeasureonlayouttest.Layout
android:layout_width="match_parent"
android:layout_height="match_parent">

<com.xttech.onmeasureonlayouttest.View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</com.xttech.onmeasureonlayouttest.Layout>


(ViewGroup)Layout

package com.xttech.onmeasureonlayouttest;
/*
* 这个是自定义的布局
*/public class Layout extends ViewGroup {
/*
* onMeasure方法
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
/*
* 直到下一行注释的代码的作用是从MeasureSpec中获取给本view传入的测量模式
* 1.如果是AT_MOST,就说明指定的是WRAP_CONTENT
* 2.如果是EXACTLY,就说明是MATCH_PARENT或者精确值
*/
String widthMode = null;
String heightMode = null;

switch (MeasureSpec.getMode(widthMeasureSpec)) {
case MeasureSpec.AT_MOST:
widthMode = "WRAP_CONTENT";
break;
case MeasureSpec.EXACTLY:
widthMode = "MATCH_PARENT或者精确值";
break;
case MeasureSpec.UNSPECIFIED:
widthMode = "UNSPECIFIED";
break;
}
switch (MeasureSpec.getMode(heightMeasureSpec)) {
case MeasureSpec.AT_MOST:
heightMode = "WRAP_CONTENT";
break;
case MeasureSpec.EXACTLY:
heightMode = "MATCH_PARENT或者精确值";
break;
case MeasureSpec.UNSPECIFIED:
heightMode = "UNSPECIFIED";
break;
}
//打印onMeasure被调用、以及宽度模式的信息
Log.d(TAG, "ViewGroup: onMeasure被调用  宽度模式:" + widthMode + " 高度模式:" + heightMode);

//调用measureChildren方法,传入两个EXACTLY作为参数
Log.d(TAG, "ViewGroup调用了measureChildren");
measureChildren(widthMeasureSpec, heightMeasureSpec);

//告诉系统测量的结果(仍然不变)
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));

}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//给子view指定边界
Log.d(TAG, "ViewGroup调用了onLayout");
getChildAt(0).layout(0, 0, 100, 100);

}
}


(View)View

package com.xttech.onmeasureonlayouttest;
//自定义view
public class View extends android.view.View {
@Override
//onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
/*
* 直到下一行注释的代码的作用是从MeasureSpec中获取给本view传入的测量模式
* 1.如果是AT_MOST,就说明指定的是WRAP_CONTENT
* 2.如果是EXACTLY,就说明是MATCH_PARENT或者精确值
*/
String widthMode = null;
String heightMode = null;
switch (MeasureSpec.getMode(widthMeasureSpec)) {
case MeasureSpec.AT_MOST:
widthMode = "WRAP_CONTENT";
break;
case MeasureSpec.EXACTLY:
widthMode = "MATCH_PARENT或者精确值";
break;
case MeasureSpec.UNSPECIFIED:
widthMode = "UNSPECIFIED";
break;
}
switch (MeasureSpec.getMode(heightMeasureSpec)) {
case MeasureSpec.AT_MOST:
heightMode = "WRAP_CONTENT";
break;
case MeasureSpec.EXACTLY:
heightMode = "MATCH_PARENT或者精确值";
break;
case MeasureSpec.UNSPECIFIED:
heightMode = "UNSPECIFIED";
break;
}
//打印onMeasure被调用、以及宽度模式的信息
Log.d(TAG, "View: onMeasure被调用  宽度模式:" + widthMode + " 高度模式:" + heightMode);
//告诉系统测量的结果(仍然不变)
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
}
//onDraw
@Override
protected void onDraw(Canvas canvas) {
//用getWidth()获取view的width
Log.d(TAG, "View width: " + getWidth());
//用getMeasuredWidth()来获取view的width
Log.d(TAG, "View MeasuredWidth: " + getMeasuredWidth());
}

}


输出结果

ViewGroup: onMeasure被调用 宽度模式:MATCH_PARENT或者精确值 高度模式:MATCH_PARENT或者精确值

ViewGroup调用了measureChildren

View: onMeasure被调用 宽度模式: WRAP_CONTENT 高度模式:WRAP_CONTENT

ViewGroup: onMeasure被调用 宽度模式:MATCH_PARENT或者精确值 高度模式:MATCH_PARENT或者精确值

ViewGroup调用了measureChildren

View: onMeasure被调用 宽度模式:WRAP_CONTENT 高度模式:WRAP_CONTENT

ViewGroup调用了onLayout

ViewGroup: onMeasure被调用 宽度模式:MATCH_PARENT或者精确值 高度模式:MATCH_PARENT或者精确值

ViewGroup调用了measureChildren

View: onMeasure被调用 宽度模式:WRAP_CONTENT 高度模式:WRAP_CONTENT

ViewGroup调用了onLayout

View width: 100

View MeasuredWidth: 984

一些疑问

上面的输出结果我进行了分段,可以发现在view的onDraw被调用之前一共进行了三次测量,为什么会进行这么多次呢?

布局文件给view的宽高设置的是WRAP_CONTENT,但是我们在Layout类中可指定的是EXACTLY。根据输出来看,View最终收到的参数还是WRAP_CONTENT。那么为什么在measureChildren(…)传入的效果没有达到效果呢,设置来有什么用?

getWidth返回100,getMeasuredWidth返回是984,二者区别是什么

viewgroup似乎能够通过layout来“指定”view的大小(因为我们虽然其他地方都没有传入宽度、高度,只是在onLayout中调用了view.layout(0,0,100,100),view的宽度就真的变成100了,说明viewGroup最终通过调用每一个子view的layout(…)方法决定子view的大小),决定view自己本身的因素到底是谁?

LayoutParm、MeasureSpec对应关系到底是什么

通过以上几个问题就可以感受到View(ViewGroup)测量机制并非那么简单,我们带着这些问题来阅读源码看看具体实现。

三、追踪源码

跟踪onMeasure

首先,我们从onMeasure入手,看看系统是如何调用onMeasure的,然后分析相关的源码。

这里onMeasure的60行是Layout类的onMeasure中我们自己写的那一句,所以从这一句开始找就好了。

measureChildren(MeasureSpec.EXACTLY, MeasureSpec.EXACTLY);




下面我们来看源码



measureChildren(…)对所有子view进行遍历并且调用对每个view调用measureChild(…)而且传入了ViewGroup的MeasureSpec int类型的key

下面看measureChild(…)



这里面出现了LayoutParms,调用了一个private函数getChildMeasureSpec(ViewGroup传入的MeasureSpec相关int型key+子view的LayoutParm内存放的值)。注意,这里的lp.width,lp.height不一定必须是精确值,也可以是WRAP_CONTENT(-2)或者MATCH_PARENT(-1)。所以说相当于传入了LayoutParm。这里相当于是根据几个数据算出来的MeasureSpec。

这里先不着急看child.measure(…),先看如何算出MeasureSpec的

由于代码太长,不方便截图了,下面是官方源码+我自己写的注释,分析都在里边。如果觉得看着头大就多看几遍。

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//ViewGroup传入自己的信息(测量模式、以及具体尺寸)
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);

//如果说ViewGroup传入的尺寸-内边框<0,那么就取0,因为尺寸不可能为负值
//特别注意,不要在measureChild里面传入MeasureSpec.WRAP_CONTENT这样的值
//因为这个int是一个key而不是一个mode,如果传入MeasureSpec.WRAP_CONTENT这样的值,系统就找不到对应的MeasureSpec
//实际上这个spec是自己本身(viewGroup)的LayoutParms决定的
int size = Math.max(0, specSize - padding);

//计算出来的结果量
int resultSize = 0;
int resultMode = 0;

switch (specMode) {
// 自己本身(viewGroup)的测量模式是Exactly(代表自己的layoutParm是match_parent或者精确值)
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
//如果子view的layoutParm是精确值,那么子view的测量模式应该是EXACTLY!
//尺寸就是子view LayoutParam指定的尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//如果子view的LayoutParm是MATCH_PARENT,那么子view的测量模式应该是EXACTLY!
//尺寸就是子view的父view(也就是自己本身)的尺寸
//这就解释了为什么LayoutParm设置为match_parent测量模式是Exactly,因为实际
//上在进行测量模式计算的时候会给size赋值,只不过这个值是根据子View的父view
//的大小确定的
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//如果子view的LayoutParm是WRAP_CONTENT,那么子view的测量模式应该是AT_MOST!
//这个时候实际上view的尺寸是自己确定的。想想看如果我们在子view的onMeasure中碰到了
//AT_MOST测量模式,那么我们会忽略掉size,直接自顾自的进行测量。
//实际上,子view的大小应该是要小于父view的大小的。而在AT_MOST时,通过MeasureSpec的size
//就可以获取到父亲的大小
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

//上面一个case代表自己本身(viewGroup)的尺寸是确定的。要么是在layout文件中设置了具体的尺寸
//要么就是因为在layout文件中设置了match_parent的再上一级父viewgroup为自己指定的
//但是如果自己也是wrap_content怎么办呢?
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
//如果子view的layoutParm是精确值,那么子view的测量模式应该是EXACTLY!
//尺寸就是子view LayoutParam指定的尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//如果子view的LayoutParm是MATCH_PARENT,我们以为子view的测量模式应该是EXACTLY!
//但是这样是有问题的。因为自己本身(viewGroup)的尺寸就还没有确定,怎么给子view提供值呢?
//所以只能让子view的测量模式设置为AT_MOST(相当于把子view的layout_parm改为了wrap_content)
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//如果子view的LayoutParm是WRAP_CONTENT,那么子view的测量模式应该是AT_MOST!
//这个时候实际上view的尺寸是自己确定的。想想看如果我们在子view的onMeasure中碰到了
//AT_MOST测量模式,那么我们会忽略掉size,直接自顾自的进行测量。
//实际上,子view的大小应该是要小于父view的大小的。而在AT_MOST时,通过MeasureSpec的size
//就可以获取到父亲的大小
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// 竟然没有指定LayoutParms,就连new一个view出来添加到布局中去
// LayoutParams默认就是wrap_content......
// 所以只有自己调用MeasureSpec.makeMeasureSpec(...)强制指定一个才可以!
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
//如果子view的layoutParm是精确值,那么子view的测量模式应该是EXACTLY!
//尺寸就是子view LayoutParam指定的尺寸
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//如果子view的LayoutParm是MATCH_PARENT,我们以为子view的测量模式应该是EXACTLY!
//但是这样是有问题的。因为自己本身(viewGroup)的尺寸就还没有确定,怎么给子view提供值呢?
//这里比较特殊,直接给子view设置了UNSPECIFIED
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//如果子view的LayoutParm是WRAP_CONTENT,那么子view的测量模式应该是AT_MOST!
//这里比较特殊,直接给子view设置了UNSPECIFIED
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//最后构造了一个新的MeasureSpec
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}


分析了这么多我们可以从这个重要的函数中了解到很多东西:

1.我们发现LayoutParms和测量模式之间是有很大关系的。可以这样说,是父view的LayoutParms和子view的LayoutParms共同决定子view的测量模式!

2.子view应该是哪种测量模式呢?,我们用以下表格来表示

父 LayoutParm子LayoutParm子测量模式子测量得到的尺寸
精确值精确值EXACTLY子LayoutParm定义的精确值
精确值match_parentEXACTLY父LayoutParm的测量值
精确值wrap_contentAT_MOST父LayoutParm的测量值
match_parent精确值EXACTLY子LayoutParm定义的精确值
match_parentmatch_parentEXACTLY父的父 提供的值
match_parentwrap_contentAT_MOST父的父 提供的值
wrap_content精确值EXACTLY子LayoutParm定义的精确值
wrap_contentmatch_parentAT_MOST父自己测量的值
wrap_contentwrap_contentAT_MOST父自己测量的值
3.从上表可以得出LayoutParms和测量模式的对应关系(父亲的测量模式不为UNDEFINED)

View的LayoutParm测量模式
精确值EXACTLY
match_parent(父不为wrap_content)EXACTLY
match_parent(父不为wrap_content)AT_MOST
wrap_contentAT_MOST
4.从代码中看到,需要(int)xxxxMeasureSpec的地方绝对不能传什么MeasureSpec.WRAP_CONTENT这种。只能用系统传进来的。否则会造成测量错误。如果实在需要自己构造,那么请使用MeasureSpec.makeMeasureSpec(size, mode)

5.最终决定View如何测量的是测量模式,而不是LayoutParm

在了解了getChildMeasureSpec(…)之后,我们来继续回去



分析一下child.measure(…);



可以看到,主要是为了调用onMeasure做一些准备。其中比较重要的就是出现了一些旗标,如果没有调用setMeasuredDimension(…)就会报异常

那么setMeasuredDimension(…)又干了什么呢?



不言自明,可以看到执行完这个之后,就可以调用getMeasuredxxx(…)了。

四、问题解答

Q1. 上面的输出结果我进行了分段,可以发现在view的onDraw被调用之前一共进行了三次测量,为什么会进行这么多次呢?

A1.可以看到,在(view)v.layout(…)函数中有调用onMeasure的行为。这个我们可以这样去看。因为measureChildren(…)是可以随便去调的。当layout有需要的时候就应该去调用。父view实际上甚至可以直接指定子view的测量模式为undefined,以达到让所有子view都measure一遍的效果(因为不管子view是什么模式,都会被指定测量模式为UNSPECIFIED,会一直传递下去)。也给我们自定义view提了个醒,那就是UNSPECIFIED应该根据内容返回大小,和AT_MOST差不多,只不过无法获取父亲的值而已。

Q2. 布局文件给view的宽高设置的是WRAP_CONTENT,但是我们在Layout类中可指定的是EXACTLY。根据输出来看,View最终收到的参数还是WRAP_CONTENT。那么为什么在measureChildren(…)传入的效果没有达到效果呢,设置来有什么用?

A2.这个答案已经在上面的分析中了

Q3. getWidth返回100,getMeasuredWidth返回是984,二者区别是什么

这个我们需要看一下layout方法的源码

Q4. viewgroup似乎能够通过layout来“指定”view的大小(因为我们虽然其他地方都没有传入宽度、高度,只是在onLayout中调用了view.layout(0,0,100,100),view的宽度就真的变成100了,说明viewGroup最终通过调用每一个子view的layout(…)方法决定子view的大小),决定view自己本身的因素到底是谁?

A4.最终决定权在ViewGroup的onLayout传入的矩形尺寸。getWidth()/getHeight()就是最终onLayout()中获得的实际值。而onMeasuredxxx(…)获取的是测量值。

Q5. LayoutParm、MeasureSpec对应关系到底是什么

A5.文中已经提到

关注作者

作者:Steven

邮箱:gammapi@qq.com

保留版权,抄袭必究

希望您能在评论区指出宝贵意见,也欢迎关注我的微信号与我交流互动

更新进程

日期日志作者
2016年11月28日创建文档Steven
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android 图片 源码
相关文章推荐