【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 ...
 
随机推荐
- Ubuntu16.04下升级Python到3.6
			
转: 这里 有一篇帖子是说从源代码开始安装,这种方式原来尝试过,需要删除系统默认的软链命令,感觉比较粗暴,现在在想有没有更好的方式呢? 找到一个帖子:http://ubuntuhandbook.org ...
 - 你遇到了吗?Caused by: org.apache.hadoop.ipc.RemoteException(org.apache.hadoop.fs.FileAlreadyExistsException)
			
我在使用 Structured Streaming 的 ForeachWriter,写 HDFS 文件时,出现了这个异常 这个异常出现的原因是HDFS作为一个分布式文件系统,支持多线程读,但是不支持多 ...
 - 2019.11.11 洛谷月赛t3
			
题目背景 由于Y校的老师非常毒瘤,要求\(zhouwc\)在\(csp\)考前最后\(3\)天参加期中考,\(zhouwc\)非常生气,决定消极考试,以涂完卡但全错为目标.现在\(retcarizy\ ...
 - Ubuntu13.10编译android源码中遇到的问题
			
1. jdk的版本不对 我开始安装的是最新的jdk7,但是编译时会出现jdk的版本
 - C# WebApi 根据实体类检查传参或字典检查参数
			
根据实体类或字典检查传参,是否缺少参数并返回缺少参数 值类型必须声明可空 /// <summary> /// 根据 Dictionary<string, string> 得到实 ...
 - 学习笔记 : python 文件操作
			
1.如果文件路径带有 \ 比如 open('c:\python\test.txt') 会报:SyntaxError: (unicode error) 'unicodeescape' codec ...
 - JSP——底层原理
			
都知道jsp就是在HTML文件中写java代码,以实现动态页面的效果,但是这种动态是如何实现的呢?今天就在研究一下. 首先,我写了一个简单的jsp文件: <%@page import=" ...
 - PHP根据ip获取地理位置(通过高德地图接口)
			
PHP根据ip获取地理位置(通过高德地图接口)<pre>//restapi.amap.com/v3/ip?key=2004f145cf3a39a72e9ca70ca4b2a1dc& ...
 - 项目——基于httpd镜像演示Dockerfile所有的指令
			
基于httpd镜像演示Dockerfile所有的指令: 第一步:创建Dockerfile工作目录 [root@localhost harbor]# mkdir /test [root@localhos ...
 - Unity 简记(2)--2D移动
			
目录 1.输入 1.1直接检测按下哪个按键 1.2.检测水平输入和垂直输入 2.移动 2.1.Transform组件 2.2.RigidBody组件 2.3.NavMeshAgent组件 2.4.Ch ...