【Android - 自定义View】之View的measure过程解析
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过程解析的更多相关文章
- Android自定义View4——统计图View
1.介绍 周末在逛慕课网的时候,看到了一张学习计划报告图,详细记录了自己一周的学习情况,天天都是0节课啊!正好在学习Android自定义View,于是就想着自己去写了一个,这里先给出一张慕课网的图,和 ...
- 自定义View Measure过程(2)
目录 目录 1. 作用 测量View的宽/高 在某些情况下,需要多次测量(measure)才能确定View最终的宽/高: 在这种情况下measure过程后得到的宽/高可能是不准确的: 建议在layou ...
- 【Android - 自定义View】之View的工作过程简介
View的工作过程分为三个过程: View的measure过程: View的layout过程: View的draw过程. 我们知道,一个Activity就是一个窗口,这个窗口中包含一个Window.一 ...
- Android View 测量流程(Measure)完全解析
前言 上一篇文章,笔者主要讲述了DecorView以及ViewRootImpl相关的作用,这里回顾一下上一章所说的内容:DecorView是视图的顶级View,我们添加的布局文件是它的一个子布局,而V ...
- Android应用层View绘制流程之measure,layout,draw三步曲
概述 上一篇博文对DecorView和ViewRootImpl的关系进行了剖析,这篇文章主要是来剖析View绘制的三个基本流程:measure,layout,draw.仅仅有把这三个基本流程搞清楚了, ...
- 源码分析篇 - Android绘制流程(二)measure、layout、draw流程
performTraversals方法会经过measure.layout和draw三个流程才能将一帧View需要显示的内容绘制到屏幕上,用最简化的方式看ViewRootImpl.performTrav ...
- Android 自定义View及其在布局文件中的使用示例(三):结合Android 4.4.2_r1源码分析onMeasure过程
转载请注明出处 http://www.cnblogs.com/crashmaker/p/3549365.html From crash_coder linguowu linguowu0622@gami ...
- [Android学习笔记]View的measure过程学习
View从创建到显示到屏幕需要经历几个过程: measure -> layout -> draw measure过程:计算view所占屏幕大小layout过程:设置view在屏幕的位置dr ...
- Android自定义View前传-View的三大流程-Measure
Android自定义View前传-View的三大流程-Measure 参考 <Android开发艺术探索> https://developer.android.google.cn/refe ...
随机推荐
- (IDEA) 搭建Maven并使用Maven打包部署
1.配置Maven的环境变量 a.首先我们去maven官网下载Maven程序,解压到安装目录,如图所示: b.配置M2_HOME的环境变量,然后将该变量添加到Path中 备注:必须要有JAVA_HOM ...
- MySQL:数据库基本认识
1.什么是数据库 通俗来讲,数据库就是用于存储数据的仓库.很多人可能会问,存储数据用文件不就行了吗?为什么还要弄数据库? 文件存储数据具有以下几个缺点: 文件的安全性问题 文件不利于查询和数据库管理 ...
- [考试反思]1014csp-s模拟测试73:侵蚀
嗯...还是没有改变那个现状 依旧只是打满了暴力,虽说T2打的的确比暴力好很多,但是因为出题人没有设分所以和暴力等同. 离上面的分差还是大的很,下面还是追的很紧 而且进几场的排名也是连续下滑... 虽 ...
- Linux下安装jdk8步骤
作为Java开发人员,在Linux下安装一些开发工具是必备技能,本文以安装jdk为例,详细记录了每一步的操作命令,以供参考. 下载jdk8 登录网址:http://www.oracle.com/tec ...
- Kubernetes5-集群上搭建基于redis和docker的留言薄
一.简介 1.环境依旧是kubernetes之前文章的架构 2.需要docker的镜像 1)php-forntend web 前端镜像 docker.io-kubeguide-guestbook-ph ...
- 2. 彤哥说netty系列之IO的五种模型
你好,我是彤哥,本篇是netty系列的第二篇. 欢迎来我的公从号彤哥读源码系统地学习源码&架构的知识. 简介 本文将介绍linux中的五种IO模型,同时也会介绍阻塞/非阻塞与同步/异步的区别. ...
- 数据可视化:绘图库-Matplotlib
为什么要绘图? 一个图表数据的直观分析,下面先看一组北京和上海上午十一点到十二点的气温变化数据: 数据: 这里我用一段代码生成北京和上海的一个小时内每分钟的温度如下: import random co ...
- win7设置docker默认服务端地址
目录 win7设置docker默认服务端地址 1.开启docker远程访问 2.本地调整 2.1 docker.exe重命名 2.2 添加docker.bat 2.3 添加快速切换功能 3.使用验证 ...
- 【Swift】UNNotificationServiceExtension
一.简介 An object that modifies the content of a remote notification before it's delivered to the user. ...
- 树莓派debian配置lamp[解决Apache不显示php网页]
Apache + MySql + Php. 1.安装Apache Apache可以用下面的命令来安装 sudo apt-get install apache2 Apache默认路径是/var/www/ ...