View学习(二)-View的测量(measure)过程
在上一篇文章中,我们介绍了DecorView
与MeasureSpec
, 下面的文章就开始讨论View的三大流程。
View的三大流程都是通过ViewRoot
来完成的。ViewRoot
对应于ViewRootImpl
类,它是连接WindowManager
与DecorView
的纽带。在ActivityThread中,当Activity
对象被创建完毕之后,会将DecorView
添加到Window
中,同时创建ViewRootImpl
对象,并将ViewRootImpl
对象和DecorView
建立关联。
View的绘制流程是从ViewRootImpl#performTraversals()
开始的,它经过measure(测量),layout(布局),draw(绘制)三个过程才能最终将一个view显示出来。
- measure过程测量View的宽高尺寸。Measure完成以后,就可以通过
getMeasuredWidth()
和getMeasuredHeight()
来获取View的宽高了。 - layout过程确定View在父容器中的摆放位置。layout()完成之后,就可以通过
getTop()
,getLeft()
,getRight()
,getBottom()
来拿到View的左上角,右下角的坐标数据了。 - draw过程负责将View绘制在屏幕上。draw()方法完成之后,View才能显示在屏幕上。
ViewRootImpl#performTraversals()
依次调用performMeasure
, performLayout
,performDraw
三个方法。这三个方法依次完成View的 measure,layout,draw过程。
借用网上的一张图,可以清晰的表达这个过程。
performMeasure
调用View#measure
方法,而View#measure
则又调用onMeasure
方法,而对于View中的onMeasure
方法,直接保存了测量得到的尺寸,而类似FrameLayout
,RelativeLayout
等各种容器ViewGroup,则在自己覆盖重写的的onMeasure
方法中,对所有的子元素进行measure过程,此时measure流程就从父容器传递到子View中了,这就完成了一次measure过程,最终这个递归的过程完成了整个View树的遍历。performLayout
,performDraw
的传递流程也是类似的。唯一不同的是performDraw
的传递过程是通过draw
方法中通过dispatchDraw
来实现的,但是道理都是相同的。- traversals的意思就是遍历。
其实到了这个时候,我们大概的流程就已经知道了,那么剩下的具体就看看View和ViewGroup当中的具体的测量过程了。这两个还要分情况讨论,因为View是属于自己一人吃饱,全家不饿,把自己测量好就行了,可是ViewGroup不仅要测量自己,还要遍历去调用所有子元素的measure过程,从而形成一个递归过程。
而measure的大流程则如下:
测量的时候父控件的
onMeasure
方法会遍历他所有的子控件,挨个调用子控件的measure方法,measure方法会调用onMeasure,然后会调用setMeasureDimension方法保存测量的大小,一次遍历下来,第一个子控件以及这个子控件中的所有孙控件都会完成测量工作;然后开始测量第二个子控件…;最后父控件所有的子控件都完成测量以后会调用setMeasureDimension方法保存自己的测量大小。值得注意的是,这个过程有可能重复执行多次,因为有的时候,一轮测量下来,父控件发现某一个子控件的尺寸不符合要求,就会重新测量一遍。
View的measure过程
View的测量过程由measure()
来完成,measure()
方法是final
的,子类无法重写覆盖。它会调用onMeasure
方法。
根据
measure
的源码,View其实也是比较喜欢偷懒的。意思是执行了measure
方法并不一定将测量过程完整走一遍(就是调用onMeasure
方法)。具体来说,如果View发现不是强制测量,且本次传入的MeasureSpec
与上次传入的相同,那么View就没必要重新测量一遍。如果真的需要测量,View也先查看之前是否缓存过相应的计算结果,如果有缓存,直接保存计算结果,就不用再调用onMeasure
了。这样也是最大限度的提高效率,避免重复工作。
onMeasure
方法代码如下:
//View#onMeasure
/**
* Measure the view and its content to determine the measured width and the
* measured height. This method is invoked by {@link #measure(int, int)} and
* should be overridden by subclasses to provide accurate and efficient
* measurement of their contents.
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
首先需要注意,该方法注释的一句话,
This method is invoked by {@link #measure(int, int)} and should be overridden by subclasses to provide accurate and efficient measurement of their contents.
这就意味着,我们在进行自定义View时,是应该重写覆盖这个方法的。
先看一下onMeasure
方法中调用的getDefaultSize
方法。
//View#getDefaultSize
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
UNSPECIFIED
模式一般用于系统内部的测量过程,在这种情况下,View的大小为getDefaultSize
的第一个参数size,即宽度为getSuggestedMinimumWidth()
返回的值,而getSuggestedMinimumWidth()
可以用一句伪代码来表示。
mMinWidth = android:minWidth;
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
对于测量过程而言,width,height过程都是一样的流程。所以为了行文简单,所以我们就简单的只说width。
再回过头来看 getDefaultSize
方法的源码。可以看到,不管我们的View的LayoutParams
设置的是 match_parent
或者 wrap_content
,它的最终尺寸都是 相同的。再结合上一篇文章末尾我们根据getChildMeasureSpec
方法而整理出来的那张表。
当View为wrap_content
时,它的size就是parentSize-padding,这和match_parent
时,size是一样的。(虽然mode可能不一样)。
结论就是: 对于View来讲,使用wrap_content
和使用match_parent
效果是一样的,它俩的默认size是相同的。
所以各个子View,(当然也包括我们extends View
,自定义的View),在测量阶段,针对wrap_content
都要做相应的处理,否则使用 wrap_content
就和使用 match_parent
效果都是一样的, 那就是默认填充满父View剩下的空间。
而我们在自定义View时,针对wrap_content
情况一般处理方式是,在onMeasure
中,增加对MeasureSpec.AT_MOST
的判断语句,结合具体业务场景或情况,设定一个默认值,或计算出一个值。
wrap_content
的意思就是 包裹内容,但是仔细思考一下,内容又是什么呢,具体到不同的子View场景,肯定有不同的意义,所以从这个角度来思考,作为继承结构上最顶级,同时也是最抽象的View而言,wrap_cotent
和match_parent
默认尺寸一样,也就有道理了。
其实普通View的onMeasure逻辑大同小异,基本都是测量自身内容和背景,然后根据父View传递过来的MeasureSpec进行最终的大小判定,例如TextView会根据文字的长度,大小,行高,行宽,显示方式,背景图片,以及父View传递过来的模式和大小最终确定自身的大小.
在测量结束时,调用了setMeasuredDimension
来存储测量得到的宽高值,该方法源码当中,注释是这样的。
This method must be called by {@link #onMeasure(int, int)} to store the measured width and measured height. Failing to do so will trigger an exception at measurement time.
如果我们自定义View时,重写了onMeasure
方法,那么就需要调用setMeasuredDimension
方法来保存结果,否则就会抛出异常。
ViewGroup的measure过程
ViewGroup比View复杂,因为它要遍历去调用所有子元素的measure过程,各个子元素再递归去执行这个过程。正所谓领导都要能者多劳之,领导要干的工作也比较多。
ViewGroup是一个抽象类,它没有重写View的onMeasure
方法,这是合理的,因为各个不同的子容器(比如LinearLayout
, FrameLayout
,RelativeLayout
等)它们有它们特定的具体布局方式(比如如何摆放子View),所以ViewGroup没办法具体统一,onMeasure
的实现逻辑,都是在各个具体容器类中实现的。
但是ViewGroup当中,提供了三个测量子控件的方法。
/**
*遍历ViewGroup中所有的子控件,调用measuireChild测量宽高
*/
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);
}
}
}
/**
* 测量某一个child的宽高
*/
//ViewGroup中方法。
protected void measureChild (View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
//获取子控件的宽高约束规则
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp. width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp. height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
/**
* 测量某一个child的宽高,考虑margin值
*/
protected void measureChildWithMargins (View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//获取子控件的宽高约束规则,相比于 measureChild方法,这里考虑了 lp.margin值
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp. leftMargin + lp.rightMargin
+ widthUsed, lp. width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp. topMargin + lp.bottomMargin
+ heightUsed, lp. height);
//测量子控件
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
望名知意,根据方法的命名我们就能知道每个方法的作用。measureChild
,或measureChildWithMargin
,它们的作用就是取出child的layoutParams
,然后再通过getChildMeasureSpec
来创建child的MeasureSpec
,然后再将MeasureSpec
传递给child进行measure。这样就完成了一轮递归。
所以我们在上一篇博客中,着重介绍了
getChildMeasureSpec
方法,指出这个方法是很重要的。
measureChildWithMargin
和measureChild
的区别就是父控件是否支持margin属性。
因为各个不同的容器(比如LinearLayout
,FrameLayout
,RelativeLayout
等),都有各自的measure方式,所以我们就挑选LinearLayout
来看一下它的measure流程。
//LinearLayout
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
我们就选择竖直方向的measure过程来分析。
源码比较长,我们只看主流程。
//LinearLayout#measureVertical
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
final View child = getVirtualChildAt(i);
// ....
// Determine how big this child would like to be. If this or
// previous children have given a weight, then we allow it to
// use all available space (and we will shrink things later
// if needed).
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight();
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
}
系统会遍历子元素,并对每个子元素执行measureChildBeforeLayout
方法,该方法内部会调用子元素的measure方法,这样各个子元素就依次进入measure过程。系统通过mTotalLength
这个变量动态存储LinearLayout在竖直方向上的高度,并且伴随着每测量一个子元素,mTotalLength
则会逐步增加。增加的部分包括了子元素的高度以及子元素在竖直方向上的margin等。
而当所有子元素都测量完毕之后,LinearLayout则会测量自己的大小。
//LinearLayout#measureVertical
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
针对竖直方向的LinearLayout而言,它在水平方向上的测量过程遵循View的测量过程,而竖直方向则和View有所不同,具体来说,如果它的高度参数是具体数值或match_parent
,则测量过程和View一致,即高度为SpecSize;而如果高度参数采用的是wrap_content
,那么它的高度就是所有子元素所占用的高度总和,但是仍然不能超过它父容器的剩余空间。并且最终高度还要考虑竖直方向的padding。
具体可以参考以下源码:
//LinearLayout
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
//当specMode为AT_MOST,并且父控件指定的尺寸specSize小于View自己想要的尺寸时,
//我们就会用掩码MEASURED_STATE_TOO_SMALL向测量结果加入尺寸太小的标记
//这样其父ViewGroup就可以通过该标记知道其给子View的尺寸太小了,
//然后可能分配更大一点的尺寸给子View
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
resolveSizeAndState
方法和getDefaultSize
方法类似,其内部实现逻辑是一样的,不过区别在于,resolveSizeAndState
方法除了返回尺寸信息,还会返回测量的status标志位信息。
View的测量过程是三大流程中比较复杂的,只有测量完毕之后,我们才有可能得到正确的宽高值。
而View的measure过程和Activity的生命周期方法是不同步的。所以我们不能简单的通过onStart
,onResume
方法来获取View的尺寸值。
参考内容:
github:
https://github.com/yaowen369
欢迎对于本人的博客内容批评指点,如果问题,可评论或邮件(yaowen369@gmail.com)联系
<p >
欢迎转载,转载请注明出处.谢谢
</p>
<script type="text/javascript">
function Curgo()
{
window.open(window.location.href);
}
</script>
View学习(二)-View的测量(measure)过程的更多相关文章
- Qt Model/View学习(二)
Model和View的搭配使用 DEMO pro文件 #------------------------------------------------- # # Project created by ...
- vue 源码学习二 实例初始化和挂载过程
vue 入口 从vue的构建过程可以知道,web环境下,入口文件在 src/platforms/web/entry-runtime-with-compiler.js(以Runtime + Compil ...
- 浅析Android View(二)
深入理解Android View(一) View的位置參数信息 二.View的绘制过程 View的绘制过程一共分为三个部分: - measure(測量View的大小) - layout(确定View的 ...
- [Android学习笔记]View的measure过程学习
View从创建到显示到屏幕需要经历几个过程: measure -> layout -> draw measure过程:计算view所占屏幕大小layout过程:设置view在屏幕的位置dr ...
- 自定义View Measure过程(2)
目录 目录 1. 作用 测量View的宽/高 在某些情况下,需要多次测量(measure)才能确定View最终的宽/高: 在这种情况下measure过程后得到的宽/高可能是不准确的: 建议在layou ...
- Android学习笔记之View(二)
View加载的流程之测量:rootView调用measure()→onMeasure(): measure()是final方法,表明Android不想让开发者去修改measure的框架,开发者可以on ...
- View学习(三)- View的布局(layout)过程
前段开始学习View的工作原理,前两篇博客的草稿都已经写好了,本想一鼓作气写完所有的相关文章,然后经历了一段连续加班,结果今天准备继续写文章时,把之前写好的东西都忘记了,又重新梳理了一遍,所以说那怕就 ...
- 【Android - 自定义View】之View的measure过程解析
measure(测量)过程是View的工作流程中最开始.最核心的过程,在这个过程中负责确定View的测量宽/高. 对于View和ViewGroup,measure过程有不同的执行方法:如果目标是一个原 ...
- Material Calendar View 学习记录(二)
Material Calendar View 学习记录(二) github link: material-calendarview; 在学习记录一中简单翻译了该开源项目的README.md文档.接下来 ...
随机推荐
- laravel实现多数据库连接配置
只需三步,便可实现. 第一步,在.env文件中配置 DB_HOST=localhost DB_DATABASE=test DB_USERNAME=root DB_PASSWORD=root DB_HO ...
- iOS简单快速集成Cordova
如果你对于什么是Cordova还不了解,可以先移步到我另一个文章:Cordoval在iOS中的运用整理 里面有详细的介绍跟如何搭建Cordova:而本文则是要介绍JiaCordova插件,如果你有一点 ...
- LVM学习
LVM Logical Volume Manager Volume management creates a layer of abstraction over physical storage, a ...
- 通过demo学python
链接 Github项目地址 软件安装包(pycharm.注册码.解析器等) Python 一切皆对象 Python 编码规范 The Python Standard Library The Pytho ...
- 全景智慧掌上城,飞入寻常百姓家——VR全景智慧城市
随着腾讯和阿里陆续将AR技术加入到新年抢红包大战之中,人们对于VR.AR未来的应用空间又多了一些想象.同传统的基于二维元素的抢红包不同,借助VR.AR的技术能够让用户获得一种更加真切的体验,这种体验相 ...
- VR全景:vr元年过后,这些企业如何发动“vr+”应用引擎?
2016年,VR可谓是四处衍生.从如痴如迷的游戏行业到喜闻乐见的影视行业,再到医疗.军事.房地产,随便呼出一个"+",VR便能左右逢源,VR+各行各业,俨然成为一种标配.最近,Ma ...
- hdu3622
hdu3622 题意 每回合给定两个坐标点,可以选择一个放置炸弹,自己决定炸弹的半径,问 n 个回合后,使炸弹半径最小值最大. 分析 存在对立关系:每回合只能选择一个地方放置炸弹.i 表示 第一个位置 ...
- wpf XAML xaml 进行 数据绑定,Resource DataContext ElementName
先做个声明:这里绑定都在前台实现,至于后台怎么写,那比前台简单多了,但更常用的是xaml中绑定.我们分析下最简单的字符串绑定来弄清楚原理,其他的类推就是. 数据绑定主要是要弄清楚两个东西,一个是源So ...
- cpp(第五章)
1.副作用,指的是在计算表达式时对某些东西(如存储在变量的值)进行修改:顺序点,是程序执行过程中的一个点,在这里,进入下一步之前将确保对所有的副作用 都进行评估.(分号就是一个顺序点).for exa ...
- idea live template高级知识, 进阶(给方法,类,js方法添加注释)
为了解决用一个命令(宏)给方法,类,js方法添加注释,经过几天的研究.终于得到结果了. 实现的效果如下: 给Java中的method添加方法: /** * * @Method : addMenu * ...