measure(测量)过程是View的工作流程中最开始、最核心的过程,在这个过程中负责确定View的测量宽/高。

  对于View和ViewGroup,measure过程有不同的执行方法:如果目标是一个原始的View,那么通过measure过程就完成了其测量过程;如果是一个ViewGroup,那么除了完成自己的测量过程外,还会遍历调用所有子元素的measure过程,每个子元素又要递归地执行这个过程。

  下面针对这两种情况分别进行讨论。

1、View的measure过程

  View的measure过程的源头是从 ViewRootImpl 类的 performTraversals() 方法开始的,在performTraversals()方法中调用 performMeasure() 方法,performMeasure()方法中调用View类的 measure() 方法,measure()方法调用View类的 onMeasure() 方法。View类的onMeasure()方法的源码如下:

/**
* 测量View及其内容,决定其宽度和高度;
* 这个方法被View类中的measure(int, int)方法调用;
* View的子类应该重写这个方法以提供更精准、更高效的测量数据;
* 调用这个方法时,必须调用setMeasuredDimension(int, int)方法存储这个View的测量宽高;
* 如果不调用setMeasuredDimension()方法,会触发IllegalStateException异常;
* View类的measure()方法默认返回的是这个控件的背景的尺寸;
* View的子类应该重写onMeasure(int, int)方法来更好的测量其内容;
* 如果这个方法被重写,子类必须保证测量得到的宽度和高度都最小是这个View的最小宽高
* (最小宽高通过getSuggestedMinimumHeight()和getSuggestedMinimumWidth()得到)。
*
* @param widthMeasureSpec 通过 MeasureSpec 打包的,父容器提供的宽度尺寸
* @param heightMeasureSpec 通过 MeasureSpec 打包的,父容器提供的高度尺寸
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

  上面代码中提到了两个方法: getDefaultSize() 和 getSuggestedMinimumXXX() 。先来看getDefaultSize()方法,源码如下:

/**
* 返回默认尺寸的工具方法;
* 如果MeasureSpec没有施加任何约束,则返回系统提供的尺寸(最小尺寸);
* 如果MeasureSpec施加了约束,则会返回一个由MeasureSpec主导的尺寸。
*
* @param size 这个View的默认尺寸
* @param measureSpec 从父容器传递过来的尺寸约束(MeasureSpec)
* @return 返回这个View应有的尺寸
*/
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = View.MeasureSpec.getMode(measureSpec);
int specSize = View.MeasureSpec.getSize(measureSpec); switch (specMode) {
case View.MeasureSpec.UNSPECIFIED:
result = size;
break;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

  可以看到,只要我们给View设置了尺寸(无论是MATCH_PARENT、WRAP_CONTENT,还是一个具体的值),getDefaultSize()方法都会原封不动的返回这个值;而如果我们不设置值,就会返回这个View的默认大小。从这一点,我们可以得到如下结论如果一个自定义控件继承自View类,那么就一定要在它的onMeasure()方法中设置wrap_content时的大小,否则就必须要在使用这个自定义控件的时候将尺寸都设置为固定大小或match_parent。这么做的原因从 getDefaultSize() 方法的源码中就可以看到:wrap_content反应到SpecMode中是 AT_MOST ,返回的就是测量的尺寸值specSize,如果我们不在onMeasure()方法中设置这个值,那么就会直接返回父容器的值(这一点可以参考【这篇文章】最后的表格)。

  再来看getSuggestedMinimumXXX()方法,这里以getSuggestedMinimumWidth()方法为例,看下源码:

/**
* 返回当前View应有的、系统建议使用的最小宽度;
* 返回的是 系统建议的最小宽度 和 背景的最小宽度 之间的较大值;
* 当在onMeasure(int, int)方法中调用时,调用者仍然要保证返回的宽度在父容器要求的范围内。
*
* @return 返回系统建议这个View使用的最小宽度
*/
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

  从源码中可以看出这个方法的逻辑:如果这个View没有设置背景,那么返回mMinWidth的值(这个值对应的是 android:minWidth 属性指定的值),这个值可以为0;如果这个View设置了背景,则返回mMinWidth和背景的最小宽度(就是Drawable的原始宽度,可能是0)这两者之间的较大值。如果onMeasure()方法中返回的宽或高的测量模式是UNSPECIFIED,则返回的就是这个方法返回的值。

  注意:在某些情况下,系统可能需要多次measure才能确定最终的测量宽/高,这种情况下,在onMeasure()方法中拿到的测量值可能是不准确的。一个比较好的习惯是在 onLayout() 方法中去获取。

2、ViewGroup的measure过程

  对于ViewGroup来说,除了完成自己本身的measure过程以外,还要遍历调用所有子元素的measure过程,各个子元素再递归地去执行这个过程。

  ViewGroup是一个抽象类,不同的ViewGroup的子类有不同的布局特性,因此,ViewGroup中没有onMeasure()方法,onMeasure()方法都被下放到了ViewGroup的子类中去单独实现。这样,ViewGroup只需要递归地让所有子元素都对自己进行测量,就完成了View树的measure过程。因此,ViewGroup中提供了一个 measureChildren() 方法,其源码如下:

/**
* 让这个ViewGroup中的所有子元素都测量它们自身(将MeasureSpec的要求和padding都计算入内);
* 如果某个子元素的显示状态为GONE,则跳过这个子元素不测量。
*
* @param widthMeasureSpec 父容器对这个ViewGroup的宽度的要求
* @param heightMeasureSpec 父容器对这个ViewGroup的高度的要求
*/
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中逐个取出子元素,并对那些可见的子元素调用 measureChild() 方法。measureChild()方法的源码如下:

/**
* 让这个ViewGroup中的某一个子元素测量它自身(将MeasureSpec的要求和padding都计算入内)
*
* @param child 要测量的子元素
* @param parentWidthMeasureSpec 父容器对这个ViewGroup的宽度的要求
* @param parentHeightMeasureSpec 父容器对这个ViewGroup的高度的要求
*/
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
final ViewGroup.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);
}

  可以看到,在这个方法中,我们取出这个View的LayoutParams,然后通过 getChildMeasureSpec() 方法得到这个View的宽和高的MeasureSpec,最后将这个View的宽和高的MeasureSpec都交给 measure() 方法处理(这里的measure()方法就是View类中的measure()方法)。可以发现,转了一圈,原来ViewGroup的测量最终都是回到View类进行测量的

#、获取测量值为0的解决方案

  这个问题我在【这篇文章】中有提到过,就是在Activity的onCreate()方法中获取某个View的测量宽/高时通常都会返回0。实际上,在Activity的 onCreate()  onStart() 、 onResume() 生命周期函数中,都无法获取到View的测量值,这是因为View的measure过程和Activity的生命周期方法不是同步执行的,因此我们无法保证Activity在执行onCreate()、onStart()或onResume()时,某个View已经测量完毕了。如果View还没有测量完毕,那么获得的宽/高就是0

  对于这个问题,有以下三种解决方案。

(1)Activity/View # onWindowFocusChanged()

   onWindowFocusChanged() 这个方法的含义是:View已经初始化完毕了,宽高已经准备好了,在这个时候去获取宽高是没有问题的。需要注意的是,这个方法在Activity获取焦点和失去焦点的时候分别会调用一次,也就是说在Activity生命周期函数的 onResume() 和 onPause() 方法被调用时,这个方法也会被调用。典型代码如下:

@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}

(2)view.post(runnable)

  通过 post() 方法可以将一个Runnable对象投递到消息队列的尾部,等到Looper调用次Runnable的时候,View也已经测量完成了。典型代码如下:

@Override
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}

(3)ViewTreeObserver

   ViewTreeObserver 是视图树的观察者,用来监听视图树的全局变化,包括视图树的布局变化、触摸状态改变等。在这个问题的解决中,我们使用的是 ViewTreeObserver.OnGlobalLayoutListener 这个监听器接口,当视图树的状态发生改变或树中某个View的可见性发生改变时,接口中的 onGlobalLayout() 方法将被回调。需要注意的是,伴随着视图树状态的改变,onGlobalLayout()方法会被回调多次。典型代码如下:

@Override
protected void onStart() {
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}

【Android - 自定义View】之View的measure过程解析的更多相关文章

  1. Android自定义View4——统计图View

    1.介绍 周末在逛慕课网的时候,看到了一张学习计划报告图,详细记录了自己一周的学习情况,天天都是0节课啊!正好在学习Android自定义View,于是就想着自己去写了一个,这里先给出一张慕课网的图,和 ...

  2. 自定义View Measure过程(2)

    目录 目录 1. 作用 测量View的宽/高 在某些情况下,需要多次测量(measure)才能确定View最终的宽/高: 在这种情况下measure过程后得到的宽/高可能是不准确的: 建议在layou ...

  3. 【Android - 自定义View】之View的工作过程简介

    View的工作过程分为三个过程: View的measure过程: View的layout过程: View的draw过程. 我们知道,一个Activity就是一个窗口,这个窗口中包含一个Window.一 ...

  4. Android View 测量流程(Measure)完全解析

    前言 上一篇文章,笔者主要讲述了DecorView以及ViewRootImpl相关的作用,这里回顾一下上一章所说的内容:DecorView是视图的顶级View,我们添加的布局文件是它的一个子布局,而V ...

  5. Android应用层View绘制流程之measure,layout,draw三步曲

    概述 上一篇博文对DecorView和ViewRootImpl的关系进行了剖析,这篇文章主要是来剖析View绘制的三个基本流程:measure,layout,draw.仅仅有把这三个基本流程搞清楚了, ...

  6. 源码分析篇 - Android绘制流程(二)measure、layout、draw流程

    performTraversals方法会经过measure.layout和draw三个流程才能将一帧View需要显示的内容绘制到屏幕上,用最简化的方式看ViewRootImpl.performTrav ...

  7. Android 自定义View及其在布局文件中的使用示例(三):结合Android 4.4.2_r1源码分析onMeasure过程

    转载请注明出处 http://www.cnblogs.com/crashmaker/p/3549365.html From crash_coder linguowu linguowu0622@gami ...

  8. [Android学习笔记]View的measure过程学习

    View从创建到显示到屏幕需要经历几个过程: measure -> layout -> draw measure过程:计算view所占屏幕大小layout过程:设置view在屏幕的位置dr ...

  9. Android自定义View前传-View的三大流程-Measure

    Android自定义View前传-View的三大流程-Measure 参考 <Android开发艺术探索> https://developer.android.google.cn/refe ...

随机推荐

  1. IIS中如何设置域名

    如何在IIS中设置域名: 1,想好我们想要配置的本地域名,我们以www.baidu.com为例. 2,打开系统盘,一般默认的系统盘为C盘,打开:C:\Windows\System32\drivers\ ...

  2. 通过 Django Pagination 实现简单分页

    作者:HelloGitHub-追梦人物 文中所涉及的示例代码,已同步更新到 HelloGitHub-Team 仓库 当博客上发布的文章越来越多时,通常需要进行分页显示,以免所有的文章都堆积在一个页面, ...

  3. 学习笔记42_SpringMVC

    SpringMVC中,Global.axas发生变化,其中 1.原来是 public class MvcApplication:System.web.HttpApplication 现在是 publi ...

  4. mysql设计规范二

    一.基本规范 必须使用InnoDB存储引擎 必须使用UTF8字符集 数据表.数据字段必须加入中文注释 二.设计规范 库名称.表名称.字段名称必须使用小写,最好不要使用驼峰式,使用“_”区分,例如use ...

  5. CSPS模拟 54

    T1    $2^{联通块个数}-2$ T2 $bitset$随便水 T3 分类讨论,讨论,讨论

  6. CSPS模拟 52

    我貌似曾经说过我是个只会做水题的巨型辣鸡.. 这次证明我水题都不会做.. T1 平均数 区间数$n^2$ 枚举是不可能了 可是好像没有无用的计算量.. 刚想到这里,此时开考15min 看见天皇比手势说 ...

  7. spring boot打包成war包的页面该放到哪里?

    背景 经常有朋友问我,平时都是使用spring mvc,打包成war包发布到tomcat上,如何快速到切换到spring boot的war或者jar包上? 先来看看传统的war包样式是什么样子的? 1 ...

  8. pandas处理excel的常用方法技巧(上)

    1. 导库 import pandas as pd 2. 读取excel文件 这里要注意的就是第二个参数header如果不设置,pandas会默认把excel的第一行当作columns,header= ...

  9. Hbase简介以及简单的入门操作

    Hbase是一个分布式的.面向列的开源数据库,可实时的读写.随机访问超大规模的数据集. Hbase主要分为两种模型: 逻辑模型和物理模型 1. 逻辑模型 Hbase的名字的来源是Hadoop data ...

  10. 创建基于OData的Web API - Knowledge Builder API, Part IV: Write Controller

    基于上一篇<创建基于OData的Web API - Knowledge Builder API, Part III:Write Model and Controller>,新创建的ODat ...