前言

Android开发中我们平时接触最多的是各种View, View是一个比较大的体系,包含了绘制流程事件分发各种动画自定义View 等等。前几天我写了一篇事件分发源码解析的文章, 今天我们来探索一下绘制流程中有点难懂的测量流程

基础知识准备

测量涉及到的相关类和方法:

  • MeasureSpec: 中文翻译测量规格,包含了两部分:SpecMode(测量模式)、SpecSize(对应测量模式下的规格大小)。它是由32位int值表示的,高两位代表SpecMode,低30位代表SpecSize
  • measure(): 测量的入口方法,里面调用了onMeasure方法
  • onMeasure(): 暴露给开发者可以自定义测量规则,不重写的话默认走的都是View的onMeasure方法

现在我们从View的onMeasure方法聊起来吧~~~

View的onMeasure源码解析

为了方便大家理解先给大家上三段常见的代码:

①View的width和height都是wrap_content

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"> <View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@color/red" /> </FrameLayout>

②View的width和height都是match_parent

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"> <View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@color/red" /> </FrameLayout>

③View的width和height都是100dp

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"> <View
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center"
android:background="@color/red" /> </FrameLayout>

上面三段代码展示效果如下:

场景①和②:

场景③:

简单描述下上面出现的情况:

  • 当view的宽高设置成match_parentwrap_content的时候,显示的大小和父view的大小一致
  • 当view的宽高设置成固定值(xxdp)的时候,显示的大小就是我们设置的固定值大小

如果你已经对上面结果感觉毫无疑问而且心里已经知其所以然了,后面的内容可以不用看了。点个赞就可以走了。。。

等等。。开个玩笑。。怎么就当真了呢。。哈哈哈。。废话不多说,走,我们点进去源码瞅瞅。

View.onMeasure

//View.java
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
...
//1
onMeasure(widthMeasureSpec, heightMeasureSpec);
...
}

上述代码①处代码调用了view的onMeasure方法,先不聊widthMeasureSpec和heightMeasureSpec的由来,这个后面补上。接着我们看View的onMeasure方法:

//View.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//1
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),//4
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)//4
);
} //2
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
...
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
} //3
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight; mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
} //返回background(Drawable)的minWidth、mMinWidth(android_minWitdh)较大值
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth,mBackground.getMinimumWidth());
} //返回background(Drawable)的minHeight、mMinHeight(android_minHeight)较大值
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight :
max(mMinHeight, mBackground.getMinimumHeight()); } //4
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) {
//5
case MeasureSpec.UNSPECIFIED:
result = size;
break;
//6
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

上述代码略微多一点,但是方法都比较简单,下面我们一一介绍下:

  • 标注①②③处的调用链:onMeasure-->setMeasuredDimension-->setMeasuredDimensionRaw 走到方法③setMeasuredDimensionRaw处,view的测量流程就结束了,同时赋值了mMeasuredWidthmMeasuredHeight,这个时候我们就可以通过getMeasuredWidth()getMeasuredHeight() 拿到宽高值了
  • 方法②setMeasuredDimension处又调用了方法④getDefaultSize
  • 方法④getDefaultSize里面大致逻辑是:
    • 如果传入的measureSpecMeasureSpec.Mode的值是MeasureSpec.AT_MOST或者MeasureSpec.EXACTLY 则返回specSize。
    • 如果传入的measureSpecMeasureSpec.Mode的值是MeasureSpec.UNSPECIFIED 则返回size(getSuggestedMinimumWidth()、getSuggestedMinimumHeight())。

现在我们的疑惑点就在于方法①onMeasure的参数是从哪里来的

那么下面我们就瞅瞅FrameLayout相关测量源码

FrameLayout的onMeasure源码解析

在讲FrameLayout的onMeasure前,我们先默认一个事实,这个FrameLayout是Activity的根View,它会被添加到DecorView上,DecorView实际上也是一个FramLayout,下面的源码解释了DecorView的MeasureSpec的由来:

//ViewRootImpl.java

private void performTraversals() {
...
//获取DecorView的MeasureSpec
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
//测量入口
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); ...
//layout入口
performLayout(lp, mWidth, mHeight);
...
//draw入口
performDraw();
...
} private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) { case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
//默认DecorView的measureSpec是:size=windowSize mode是EXACTLY
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension,MeasureSpec.EXACTLY);
break;
}
return measureSpec;
} private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
if (mView == null) {
return;
}
...
//mView即为DecorView
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}

从上面的代码得知:DecorView的measureSpec是size=windowSize比如(1920px 1080px) mode=EXACTLY, 然后由于Activity根FramLayout的widthheight都是match_parent, 所以 FrameLayout measureSpec和DecorView的measureSpec是一致的, 接下来我们看FrameLayout的onMeasure源码:

//FrameLayout.java

//这里的widthMeasureSpec的size 手机屏幕宽度 mode是EXACTLY
//heightMeasureSpec的size是手机屏幕高度-状态栏高度 mode是EXACTLY
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
int maxHeight = 0;
int maxWidth = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//1
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
...
}
} ...
//2
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
} //ViewGroup.java
//3
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec,
int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); //4
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
//5
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height); //6
child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } //7
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//measureSpec的mode
int specMode = MeasureSpec.getMode(spec);
//measureSpec的size
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) {//child的宽高值是固定值比如40dp
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//child的lp.width是match_parent
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//child的lp.width是wrap_content
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break; case MeasureSpec.AT_MOST:
if (childDimension >= 0) {//child的宽高值是固定值比如40dp
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//此时 child的match_parent 和 wrap_content 返回的MeasureSpec 一模一样
//mode都是AT_MOST
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break; case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
//child的宽高值是固定值比如40dp
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//此时 child的match_parent 和 wrap_content 返回的MeasureSpec 一模一样
//mode都是AT_MOST
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
} return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
} //View.java //8
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState){
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST://对应了wrap_content
//如果子view的size大于measureSpec的size返回specSize 不能大于父类传递进来的specSize
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {//如果子view的size小于specSize 则返回子view的size
result = size;
}
break;
//EXACTLY模式(一般对应固定值或match_parent) 返回measureSpec的specSize
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED://未设置特定测量模式 返回传入的size
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}

上面的源代码有点多,不着急,我们一个一个来介绍:

  • 注释①处我们遍历child,挨个调用方法③measureChildWithMargins()

  • 方法③measureChildWithMargins内部 注释④⑤都是调用了方法⑦getChildMeasureSpec

  • 方法⑦getChildMeasureSpec比较核心我们详细介绍:

    • 首先我们看方法⑦getChildMeasureSpec的参数是measureSpec(即刚才我们提到的decorView的measureSpec)paddingchildDimension(child在xml里面写的width和height), childDimension的取值有三个:固定值xxdp match_parent wrap_content
    • 方法⑦getChildMeasureSpec返回了child的measureSpec其计算规则如下图:

  • 注释⑥处调用了child.measure 走入了我们前面讲的view的测量流程, 到这文章前面我们提到三种情景就可以解释通了:

    1. view的宽高为match_parent或者wrap_content, FrameLayout计算得出子view的MeasureSpec(size=specSize mode=EXATLY或者AT_MOST),但是走到子View里面的测量流程的时候对于EXACTLY和AT_MOST一视同仁取得都是specSize
    2. view的值为固定值时,FrameLayout计算得出子view的MeasureSpec(size=childDimension mode=EXATLY)注意这里measureSpec的specSize是childDimension
  • 所有子view测量完后,我们计算并保存了maxWidth 和maxHeight, 然后就到了注释②处的setMeasuredDimension方法,然后通过注释⑧resolveSizeAndState方法及参数measureSpec和childMaxSize计算出了FramLayout的width和height,FramLayout测量也就算结束了。注释⑧resolveSizeAndState方法里面的注释我已经标注的很明白了,就不详细介绍了

通过上面的流程分析,我们基本把测量流程梳理了一遍,其流程图如下:

总结

  1. 如果View/ViewGroup并没有重写onMeasure的话,match_parent 和wrap_content表现出来的效果是一样的,都是取得parentMeasureSpec的size ,所以如果想表现成wrap_content的效果,我们需要重写onMeasure方法来定义自己的测量规则,比如:①如果TextView的wrap_content需要生效的话我们可以在AT_MOST模式下,将TextView的宽高设置成文字的矩形大小②如果ImageView的wrap_content需要生效的话我们可以在AT_MOST模式下,将Image的宽高设置成图片的原始大小

  2. 常规测量模式下,我们的子view是不可能比父View还大,这个时候我们会产生一个疑问:如果我们希望子view/ViewGroup可以超出父view该如何做,答案是UNSPECIFIED,推荐阅读类是NestedScrollView

  3. setMeasuredDimension方法是一个view测量的终点方法,其赋值了mMeasureWidth和mMeasureHeight

  4. 自定义ViewGroup并继承自ViewGroup的时候,如果不重写onMeasure就会导致子view根本没测量,一般我们可以通过调用measureChildren()来测量子view或者自己定义测量规则

推荐阅读

架构师学习笔记

耗时268天,7大模块、2983页58万字,Android开发核心知识笔记!

Java基础、计算机网络、系统

史上最全!押题率90%的 Android 中高级工程师面试复习大纲及真题答案整理(上篇)

Android基础夯实99题,点这里

史上最全!押题率90%的 Android 中高级工程师面试复习大纲及真题答案整理(中篇)

Android高级面试题(上)

史上最全!押题率90%的 Android 中高级工程师面试复习大纲及真题答案整理(下篇)

Android高级面试题(下)

史上最全!押题率90%的 Android 中高级工程师面试复习大纲及真题答案整理(终章)

什么?这么精髓的View的Measure流程源码全解析,你确定不看看?的更多相关文章

  1. Android Activity启动流程源码全解析(1)

    前言 Activity是Android四大组件的老大,我们对它的生命周期方法调用顺序都烂熟于心了,可是这些生命周期方法到底是怎么调用的呢?在启动它的时候会用到startActivty这个方法,但是这个 ...

  2. Android Activity启动流程源码全解析(2)

    接上之前的分析 ++Android Activity启动流程源码全解析(1)++ 1.正在运行的Activity调用startPausingLocked 一个一个分析,先来看看startPausing ...

  3. 自定义控件(View的绘制流程源码解析)

    参考声明:这里的一些流程图援引自http://a.codekk.com/detail/Android/lightSky/%E5%85%AC%E5%85%B1%E6%8A%80%E6%9C%AF%E7% ...

  4. Android笔记--View绘制流程源码分析(二)

    Android笔记--View绘制流程源码分析二 通过上一篇View绘制流程源码分析一可以知晓整个绘制流程之前,在activity启动过程中: Window的建立(activit.attach生成), ...

  5. Android笔记--View绘制流程源码分析(一)

    Android笔记--View绘制流程源码分析 View绘制之前框架流程分析 View绘制的分析始终是离不开Activity及其内部的Window的.在Activity的源码启动流程中,一并包含 着A ...

  6. [Android]从Launcher开始启动App流程源码分析

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5017056.html 从Launcher开始启动App流程源码 ...

  7. [Android]Android系统启动流程源码分析

    以下内容为原创,欢迎转载,转载请注明 来自天天博客:http://www.cnblogs.com/tiantianbyconan/p/5013863.html Android系统启动流程源码分析 首先 ...

  8. Spring加载流程源码分析03【refresh】

      前面两篇文章分析了super(this)和setConfigLocations(configLocations)的源代码,本文来分析下refresh的源码, Spring加载流程源码分析01[su ...

  9. Spring IOC容器启动流程源码解析(四)——初始化单实例bean阶段

    目录 1. 引言 2. 初始化bean的入口 3 尝试从当前容器及其父容器的缓存中获取bean 3.1 获取真正的beanName 3.2 尝试从当前容器的缓存中获取bean 3.3 从父容器中查找b ...

随机推荐

  1. windows电脑上传ipa到appstore的详细流程

    在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布. 但是在苹果开发者中心无法直接上传ipa文件,它要求我们使用xcode或transpo ...

  2. 为什么要从 Linux 迁移到 BSD 4

    为什么要从 Linux 迁移到 BSD 4 许可证问题 Linux GPL 许可证对开发者的要求比较严格,它是一种开源的反模式,因为它强制发布所有修改过的源代码,并且阻止其他开源项目的集成,例如 GP ...

  3. 你想知道的 std::vector::push_back 和 std::vector::emplace_back

    引言 C++ 11 后,标准库容器 std::vector 包含了成员函数 emplace 和 emplace_back.emplace 在容器指定位置插入元素,emplace_back 在容器末尾添 ...

  4. 「UOJ 514」通用测评号(生成函数)

    首先,题目中的过程可以看作:每次选择任意一个燃料仓,给它装填 \(1\) 单位的燃料,如果此时恰好 "填满" 了它,就给答案 \(+1\). 考虑 \(n\) 号燃料仓填满的概率, ...

  5. 安装RPM包或者源码包

    RPM工具 RPM他是以一种数据库记录的方式将我们所需要的套件安装到linux主机的一套管理程序关于RPM各个选项的含义如下-i:表示安装-v:表示可视化-h:表示安装进度在安装RPM包时,常用的附带 ...

  6. 【java框架】SpringBoot(3) -- SpringBoot集成Swagger2

    1.SpringBoot web项目集成Swagger2 1.1.认识Swagger2 Swagger 是一个规范和完整的框架,用于生成.描述.调用和可视化 RESTful 风格的 Web 服务.总体 ...

  7. 【linux】驱动-6-总线-设备-驱动

    目录 前言 6. 总线-设备-驱动 6.1 概念 6.2 工作原理 6.3 总线 6.3.1 总线介绍 6.3.2 注册总线 6.4 设备 6.4.1 设备介绍 6.4.2 设备注册.注销 6.5 驱 ...

  8. Redis 超详细自动管理Cluster集群工具上手 redis-trib.rb (多图,手把手)

    安装介绍 ​ redis-trib.rb是一款由Redis官方提供的集群管理工具,能够大量减少集群搭建的时间. ​ 除此之外,还能够简化集群的检查.槽迁徙.负载均衡等常见的运维操作,但是使用前必须要安 ...

  9. [Azure Devops] 获取单元测试的代码覆盖率

    1. 获取代码覆盖率 上一篇文章里,我们在 Pipeline 中插入一个单元测试并把所有单元测试都通过作为 Pipeline 通过的硬性要求.除此以外,我们还可以获取单元测试的代码覆盖率,用作衡量代码 ...

  10. [搜索]P1088 火星人

    火星人 题目描述 人类终于登上了火星的土地并且见到了神秘的火星人.人类和火星人都无法理解对方的语言,但是我们的科学家发明了一种用数字交流的方法.这种交流方法是这样的,首先,火星人把一个非常大的数字告诉 ...