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. Java基础01-集合1、泛型

    集合.泛型 第一章:集合1 1. 什么是集合 定义:在Java中,集合是一种可以存储多个数据的容器. 代码: ArrayList<String> list = new ArrayList& ...

  2. 关于RocketMQ消息消费与重平衡的一些问题探讨

    其实最好的学习方式就是互相交流,最近也有跟网友讨论了一些关于 RocketMQ 消息拉取与重平衡的问题,我姑且在这里写下我的一些总结. ## 关于 push 模式下的消息循环拉取问题 之前发表了一篇关 ...

  3. 定制linux镜像并自动化安装

    最近碰到个需求:要在内网环境安装centos6.5系统并搭建服务,但由于自动部署脚本里安装依赖包使用的是yum安装,而服务器无法连接外网,实施人员也不会本地yum源搭建….. 本来想法是打算把需要的依 ...

  4. 还看不懂同事的代码?Lambda 表达式、函数接口了解一下

    当前时间:2019年 11月 11日,距离 JDK 14 发布时间(2020年3月17日)还有多少天? // 距离JDK 14 发布还有多少天? LocalDate jdk14 = LocalDate ...

  5. 同余类BFS的一些瞎吹

    同余类BFS的题,是个OIer基本上都会见过一些,最好的例子就是NOIP 2018 day1  T2---货币系统 虽然这题其实是什么背包就能解决的题目,但数据一变大,出题人坏一点,就没了.... 同 ...

  6. 『题解』LibreOJ6277 数列分块入门 1

    更好的阅读体验 Portal Portal1: LibreOJ Description 给出一个长为\(n\)的数列,以及\(n\)个操作,操作涉及区间加法,单点查值. Input 第一行输入一个数字 ...

  7. [转载]2.3 UiPath循环活动For Each的介绍和使用

    一.For Each的介绍 For Each:循环迭代一个列表.数组.或其他类型的集合, 可以遍历并分别处理每条信息 二.For Each在UiPath中的使用 1.打开设计器,在设计库中新建一个Fl ...

  8. 开始逆向objc基础准备(一)简单认识一下arm32,以及与x86汇编指令类比

    ARM32体系中有31或33个通用寄存器,没有特定的某种态下有r0-r15一共16个寄存器,快速中断态下有另一组r8-r12备份寄存器,在用户态和系统态之外其它态下都各自有一组r13-r14备份寄存器 ...

  9. ZeroC ICE的远程调用框架 代理引用地址

    在官方文档中称为Binding,协议-地址对的绑定.在Proxy模式中,一般地有三个参与者,Proxy,Subject以及RealSubject.Subject定义了Proxy(代理)和RealSub ...

  10. React传值,验证值的类型和默认值

    const ele = <Ff const={'哈哈'} index={55}></Ff> let box = document.querySelector('#app') / ...