0、预备知识

我们的手机屏幕的布局其实是嵌套的,最外层是一个phoneWindow,这个view和手机屏幕一样大,里面是一个frameLayout,再里面才是我们自己写的布局文件。

我们在绘制控件前必须要经历measure的过程,这个过程需要从最外层的PhoneWindow开始进行。phonewindow调用内部frameLayout的measure,frameLayout又调用内部view的onMeasure,依次类推。总之就是不断的调用自己内部view的measure方法(measure中会调用onMeasure),直到到达了最内部的view。其实说白了就是这么个流程,下面就是onMeasure方法:

void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

这个方法真是简单直观,让人一下子觉得都没什么可说的了(真的么?)

没有返回值,只有两个参数,我们唯一需要分析的就是这两个参数了。然而,通过上面的分析,我们已经知道了这两个参数肯定是从它外层的view传来的,毕竟外层的view调用了它的measure嘛,而onMeasure又会调用measure,额,貌似又没啥可说的了。姑且说下这两个参数是怎么传进来的,参数的意义又是什么吧。

1.childWidthMeasureSpec和childHeightMeasureSpec

首先最外层的view调用了performTraversals方法,得到了childWidthMeasureSpec和childHeightMeasureSpec两个值:

private void performTraversals() {
// ………省略宇宙尘埃数量那么多的代码……… if (!mStopped) {
// ……省略一些代码 int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); // ……省省省 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
} // ………省略人体细胞数量那么多的代码………
}

我们看看上面代码中的三行代码是什么意思:

第一行、第二行的操作一样,我们分析第一行就得了。得到widthMeasureSpec的途径是调用getRootMeasureSpec做的,给他传的是width和lp.width。这两个值我们太熟悉了,每个view里面都有。

1.view的width:这个不用多说,需要说明的是最外层的view(phoneWindow)宽度肯定和手机屏幕一样,而手机屏幕的宽度是确定的,于是这个值在最外层就有了初始值。

2.lp.width:

TextView view = (TextView) findViewById(R.id.text);
ViewGroup.LayoutParams lp = view.getLayoutParams();
lp.width;
lp.height;

这个东西我们也常见,布局属性嘛!需要说明的是最外层view的lp.width和lp.height均为MATCH_PARENT,其在mWindowAttributes(WindowManager.LayoutParams类型)将值赋予给lp时就已被确定。

现在我们知道参数了意义了,而且这些参数都是我们熟悉的,但我好好奇,这个getRootMeasure中到底做了什么不得了的事情!

 //                          参数解释:        width/height   lp.width/lp.height
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) { case ViewGroup.LayoutParams.MATCH_PARENT:
// Window不能调整其大小,强制使根视图大小与Window一致
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window可以调整其大小,为根视图设置一个最大值
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window想要一个确定的尺寸,强制将根视图的尺寸作为其尺寸
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}

我们终于遇到不清楚的常量了,不过不用怕!常量嘛,其实就是标识的作用,来看看这些常量是什么意思:

1. EXACTLY

表示父视图“希望”子视图的大小应该是由specSize的值来决定的!

系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。

2. AT_MOST

表示子视图“最多”只能是specSize中指定的大小!

开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。

3. UNSPECIFIED

表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制!

这种情况比较少见,不太会用到。

现在分析如下:

外层view是match_parent时,我们允许内层view在外层view的所在区域内绘制自己。

当外层view是wrap_content时,我们应在最外层view的所在区域内尽可能小的绘制内层view。

③如果外层view有固定大小,那么我们内层view的绘制区域就是lp.xxx,因为这时候lp.xxx = 确定值,所以在这种情况下内层view的绘制区域就是外部view的绘制区域。

最终我们通过MeasureSpec.makeMeasureSpc来把这两个值拼接起来,变成一个64位的值,传递下去。

我隐隐的感到,以后我们还是要把这个拼接好的值解析出来的,感觉MeasureSpec以后还会见到。

2.探寻onMeasure方法

view的measure如下:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// 省略部分代码…… /*
* 判断当前mPrivateFlags是否带有PFLAG_FORCE_LAYOUT强制布局标记
* 判断当前widthMeasureSpec和heightMeasureSpec是否发生了改变
*/
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) { // 如果发生了改变表示需要重新进行测量此时清除掉mPrivateFlags中已测量的标识位PFLAG_MEASURED_DIMENSION_SET
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; resolveRtlPropertiesIfNeeded(); int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// 测量View的尺寸
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} else {
long value = mMeasureCache.valueAt(cacheIndex); setMeasuredDimension((int) (value >> 32), (int) value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
} /*
* 如果mPrivateFlags里没有表示已测量的标识位PFLAG_MEASURED_DIMENSION_SET则会抛出异常
*/
if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
throw new IllegalStateException("onMeasure() did not set the"
+ " measured dimension by calling"
+ " setMeasuredDimension()");
} // 如果已测量View那么就可以往mPrivateFlags添加标识位PFLAG_LAYOUT_REQUIRED表示可以进行布局了
mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
} // 最后存储测量完成的测量规格
mOldWidthMeasureSpec = widthMeasureSpec;
mOldHeightMeasureSpec = heightMeasureSpec; mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
(long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}

过程如下:

判断传入的参数是否和之前的一样,如果一样,那么就不做改变(这点是优化中需要注意的,在setVisible中也有类似的判断)。

如果是强制测量,那么不管现在的参数和之前的一不一样,都进行重新测量。

如果有缓存则用缓存(这里的缓存也如我所料,用了LongSparseLongArray这种散列表),没有缓存就调用onMeasure。

测量完毕后设置标志位并且存储测量后的数据,最后把这些结果放入缓存。

多说一点:我们发现用缓存后调用了一个方法:setMeasureDimension。我们有理由推断,在onMeasure中肯定也需要调用这个方法来设置最终的view大小。

我们知道可以重写onMeasure来做自己的测量工作,在此之前我们先来看看默认的实现方案,之后我们就可以照猫画虎地做啦。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

这里面其实就一个setMeasureDimension,验证了之前的猜想。这个方法传入了width和height,来最终确定控件的大小。getDefaultSize方法给人的感觉是通过传入的值和建议的值的得到一个最终合理的值,我感觉里面有可能用到了各种比较、取大小、取上下限等操作

/**
* Utility to return a default size. Uses the supplied size if the
* MeasureSpec imposed no constraints. Will get larger if allowed
* by the MeasureSpec.
*
* @param size Default size for this view
* @param measureSpec Constraints imposed by the parent
* @return The size this view should be.
*/
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;
}

好嘛,MeasureSpec你又出现了!现在我们把之前传过来的值分前32位后32位得到了mode和size,如果外层view没有设置任何测量标准,那么就用推荐的size,否则就是用外层view传来的size。

注意:上述代码中当模式为AT_MOSTEXACTLY时均会返回计算出的测量尺寸,还记得上面我们说的PhoneWindow、DecorView么从它们那里获取到的测量规格层层传递到我们的自定义View中,这就是为什么我们的自定义View在默认情况下不管是math_parent还是warp_content都能占满父容器的剩余空间的原因。

得到系统推荐的size也很简单:

protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

如果背景为空那么我们直接返回mMinWidth最小宽度否则就在mMinWidth和背景最小宽度之间取一个最大值,反正就是得到控件的最小大小。哈哈,这里又印证了我的猜想,用到了比较和取大小来得到最终的结果。

3.自定义onMeasure

我们已经分析清楚了这样一个流程,那么我们就来自定义一个onMeasure方法吧~

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(300, 300);
}

哈哈,太简单了,完全没有任何难度嘛。咦,之前说道的UNSPECIFIED、EXACTLY、AT_MOST这些常量好像没啥用了,size也没有用到。是不是简单的有点过分,不太好吧。我们之前解释了这些常量的意思(忘记了请回到上面看看,顺便看看红字),他们其实也就是个建议,至于开发者想要怎么设置view的大小,android框架是不管的。虽然如此,我们还是应该做个听话的孩子,让view的最终测量尺寸由view本身和外层view共同决定才好。下面的代码展示了我们应如何更好的自定义onMeasure。

下面给个源码:

package com.example.kale.text;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View; /**
* @author Jack Tony
* @date 2015/8/3
*/
public class TestView extends View { Bitmap mBitmap; 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);
initView();
} public void initView() {
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.kale);
} @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 声明一个临时变量来存储计算出的测量值
int resultWidth = 0; // 获取宽度测量规格中的mode
int modeWidth = MeasureSpec.getMode(widthMeasureSpec); // 获取宽度测量规格中的size
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec); /*
* 如果外层view心里有数
*/
if (modeWidth == MeasureSpec.EXACTLY) {
// 取外层view给的大小
resultWidth = sizeWidth;
}
/*
* 如果外层view没数
*/
else {
// 可要自己看看自己需要多大了
resultWidth = mBitmap.getWidth(); /*
* 如果外层view给的是一个限制值
*/
if (modeWidth == MeasureSpec.AT_MOST) {
// 那么自己的需求就要跟限制比比看谁小要谁
resultWidth = Math.min(resultWidth, sizeWidth);
}
}
resultWidth += getPaddingLeft() + getPaddingRight(); int resultHeight = 0;
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec); if (modeHeight == MeasureSpec.EXACTLY) {
resultHeight = sizeHeight;
} else {
resultHeight = mBitmap.getHeight();
if (modeHeight == MeasureSpec.AT_MOST) {
resultHeight = Math.min(resultHeight, sizeHeight);
}
}
resultHeight += getPaddingTop() + getPaddingBottom(); // 设置测量尺寸
setMeasuredDimension(resultWidth, resultHeight);
} @Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas); canvas.drawBitmap(mBitmap, getPaddingLeft(), getPaddingRight(), null);
}
}

4.viewGroup的measureChild

measureChildWithMargins和measureChildren类似只是加入了对Margins外边距的处理,ViewGroup提供对子元素测量的方法从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);
}
}
}

这里的操作是遍历viewgroup中的view,然后在view可见的前提下调用measureChild方法。这里传入三个值,参数都是大家熟悉的,就不错过多说明了。

protected void measureChild(View child, int parentWidthMeasureSpec,  int parentHeightMeasureSpec) {
// 获取子元素的布局参数
final LayoutParams lp = child.getLayoutParams();
/*
* 将父容器的测量规格已经上下和左右的边距还有子元素本身的布局参数传入getChildMeasureSpec方法计算最终测量规格
*/
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); // 调用子元素的measure传入计算好的测量规格
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

getChildMeasureSpec这个方法和getRootMeasureSpec很相似,那么我们主要就是看看getChildMeasureSpec方法是如何确定最终测量规格的:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 获取父容器的测量模式和尺寸大小
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec); // 这个尺寸应该减去内边距的值
int size = Math.max(0, specSize - padding); // 声明临时变量存值
int resultSize = 0;
int resultMode = 0; /*
* 根据模式判断
*/
switch (specMode) {
case MeasureSpec.EXACTLY: // 父容器尺寸大小是一个确定的值
/*
* 根据子元素的布局参数判断
*/
if (childDimension >= 0) { //如果childDimension是一个具体的值
// 那么就将该值作为结果
resultSize = childDimension; // 而这个值也是被确定的
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的布局参数为MATCH_PARENT
// 那么就将父容器的大小作为结果
resultSize = size; // 因为父容器的大小是被确定的所以子元素大小也是可以被确定的
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的布局参数为WRAP_CONTENT
// 那么就将父容器的大小作为结果
resultSize = size; // 但是子元素的大小包裹了其内容后不能超过父容器
resultMode = MeasureSpec.AT_MOST;
}
break; case MeasureSpec.AT_MOST: // 父容器尺寸大小拥有一个限制值
/*
* 根据子元素的布局参数判断
*/
if (childDimension >= 0) { //如果childDimension是一个具体的值
// 那么就将该值作为结果
resultSize = childDimension; // 而这个值也是被确定的
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的布局参数为MATCH_PARENT
// 那么就将父容器的大小作为结果
resultSize = size; // 因为父容器的大小是受到限制值的限制所以子元素的大小也应该受到父容器的限制
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的布局参数为WRAP_CONTENT
// 那么就将父容器的大小作为结果
resultSize = size; // 但是子元素的大小包裹了其内容后不能超过父容器
resultMode = MeasureSpec.AT_MOST;
}
break; case MeasureSpec.UNSPECIFIED: // 父容器尺寸大小未受限制
/*
* 根据子元素的布局参数判断
*/
if (childDimension >= 0) { //如果childDimension是一个具体的值
// 那么就将该值作为结果
resultSize = childDimension; // 而这个值也是被确定的
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) { //如果子元素的布局参数为MATCH_PARENT
// 因为父容器的大小不受限制而对子元素来说也可以是任意大小所以不指定也不限制子元素的大小
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) { //如果子元素的布局参数为WRAP_CONTENT
// 因为父容器的大小不受限制而对子元素来说也可以是任意大小所以不指定也不限制子元素的大小
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
} // 返回封装后的测量规格
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

我们通过上面的代码知晓了内部view的最终测量值是由它自身和外部的viewGroup共同决定的,最终把这个算好的值传入到了内部view的measure中,measure由会把这些值传到onMeasure中。需要再次说明的是,我们虽然在onMeasure中接收到了这些值,但它们仅仅是一个建议,我们是仍旧还是可以随意指定的~

参考自:

http://blog.csdn.net/aigestudio/article/details/42989325

http://blog.csdn.net/guolin_blog/article/details/16330267

onMeasure流程解析的更多相关文章

  1. TCP/IP协议三次握手与四次握手流程解析

    原文链接地址:http://www.2cto.com/net/201310/251896.html TCP/IP协议三次握手与四次握手流程解析 TCP/IP协议的详细信息参看<TCP/IP协议详 ...

  2. SSL/TLS算法流程解析

    SSL/TLS 早已不是陌生的词汇,然而其原理及细则却不是太容易记住.本文将试图通过一些简单图示呈现其流程原理,希望读者有所收获. 一.相关版本 Version Source Description ...

  3. TCP/IP协议三次握手与四次握手流程解析(转载及总结)

    原文地址:http://www.2cto.com/net/201310/251896.html,转载请注明出处: TCP/IP协议三次握手与四次握手流程解析 一.TCP报文格式  TCP/IP协议的详 ...

  4. Django生命周期 URL ----> CBV 源码解析-------------- 及rest_framework APIView 源码流程解析

    一.一个请求来到Django 的生命周期   FBV 不讨论 CBV: 请求被代理转发到uwsgi: 开始Django的流程: 首先经过中间件process_request (session等) 然后 ...

  5. [MapReduce_3] MapReduce 程序运行流程解析

    0. 说明 Word Count 程序运行流程解析 &&  MapReduce 程序运行流程解析 1. Word Count 程序运行流程解析 2. MapReduce 程序运行流程图

  6. HBase - 数据写入流程解析

    本文由  网易云发布. 作者:范欣欣 本篇文章仅限内部分享,如需转载,请联系网易获取授权. 众所周知,HBase默认适用于写多读少的应用,正是依赖于它相当出色的写入性能:一个100台RS的集群可以轻松 ...

  7. EurekaClient自动装配及启动流程解析

    在上篇文章中,我们简单介绍了EurekaServer自动装配及启动流程解析,本篇文章则继续研究EurekaClient的相关代码 老规矩,先看spring.factories文件,其中引入了一个配置类 ...

  8. Mysql流程解析

    Mysql流程解析 流程图 流程图解析 客户端发送一条sql语句. 1.此时,mysql会检查sql语句,查看是否命中缓存,如果命中缓存,直接返回结果,不继续执行.没有命中则进入解析器. 2.解析器会 ...

  9. Session (简介、、相关方法、流程解析、登录验证)

    Session简介 Session的由来 Cookie虽然在一定程度上解决了"保持状态"的需求,但是由于Cookie本身最大支持4096字节,以及Cookie本身保存在客户端,可能 ...

随机推荐

  1. 【深入BFC】 关于CSS中float布局,清除浮动,和margin合并的原理解析,解开你心中的那些困惑!

    BFC的通俗理解: Block Formatting Context(块级格式化上下文)是W3C CSS 2.1 规范中的一个概念,它决定了元素如何对其内容进行定位,以及与其他元素的关系和相互作用. ...

  2. SQL Server代理(3/12):代理警报和操作员

    SQL Server代理是所有实时数据库的核心.代理有很多不明显的用法,因此系统的知识,对于开发人员还是DBA都是有用的.这系列文章会通俗介绍它的很多用法. 如我们在这个系列的文章里所见,SQL Se ...

  3. 基于HTML5 WebGL实现3D飞机叶轮旋转

    在上一篇<基于HT for Web矢量实现2D叶轮旋转>中讲述了叶轮旋转在2D拓扑上的应用,今天我们就来讲讲叶轮旋转在3D上的应用. 在3D拓扑上可以创建各种各样的图元,在HT for W ...

  4. JS包装对象

    一.包装对象 var s = "hello word"; s.len = 4; var t = s.len; //=>undefined 原因由于s是一个字符串,在执行第二行 ...

  5. HtmlAgilityPack 删除script、style以及注释标签

    foreach(var script in doc.DocumentNode.Descendants("script").ToArray()) script.Remove(); f ...

  6. 关于C#基础

    前几天帮人做个社交网站,还是用的控件方式,不过学习了ajax和一般处理程序ashx后,也用在了里面一些,今天回来继续写博客.继续上次总结下基础知识,学的内容多,总结的可能比较杂乱,分条总结为平时能自己 ...

  7. Linux下快速设定ip bond

    在计算机网路普及的初期,很多OS系统都使用的为单网卡方式,即一个网卡使用一个IP地址.随着网络要求的不断提高,我们可以对多个网卡进行绑定聚合当一个逻辑网络接口来使用,从而大幅提升服务器的网络吞吐(I/ ...

  8. 如何显示二进制流的图片(利用img控件)

    之前在http://www.cnblogs.com/JsonZhangAA/p/5568575.html博文中是利用的image控件来显示的二进制流图片,我现在想的是能 通过普通的<img id ...

  9. C#通用类Helper整理

    ★前言     最近下载了tita_chou在CSDN上传的一个资源,是在工作中整理的C#帮助类,里面包含了很多实用的类,想到我之前收集过自己用到少的可怜的类,心生敬意啊.当粗略的查看了那个资源,发现 ...

  10. js Date 时间格式化的扩展

    js Date 时间格式化的扩展: Date.prototype.format = function (fmt) { var o = { , //月 "d+": this.getD ...