前言

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. Android实现三角形气泡效果方式汇总

    在开发过程中,我们可能会经常遇到这样的需求样式: 这张图是截取京东消息通知的弹出框,我们可以看到右上方有个三角形的气泡效果,这只是其中一种,三角形的方向还可以是上.下.左.右. 通过截图可以发现,气泡 ...

  2. python基础学习之元组和字典的功能方法

    什么是元组?(tuple) emmmmmm,这个没必要深究吧,就是一排'元素',一行 格式: a = (1,2,3,4,5,6,7,8,9)用小括号表示的,极为元组. 其有序,且不可更改,可以对比st ...

  3. Java8的新特性--并行流与串行流

    目录 写在前面 Fork/Join框架 Fork/Join框架与传统线程池的区别 传统的线程池 Fork/Join框架 Fork/Join框架的使用 Java8中的并行流 写在前面 我们都知道,在开发 ...

  4. BIMFACE二次开发SDK 开源C#版

    [ BIMFace.SDK.CSharp ] 是基于微软.NET 技术封装的用于 BIMFACE 二次开发的通用类库.其中封装了BIMFace服务端API,包含基础API.文件上传API.文件转换AP ...

  5. P1426 小鱼会有危险吗(JAVA语言)

    题目描述 有一次,小鱼要从A处沿直线往右边游,小鱼第一秒可以游7米,从第二秒开始每秒游的距离只有前一秒的98%.有个极其邪恶的猎人在距离A处右边s米的地方,安装了一个隐蔽的探测器,探测器左右x米之内是 ...

  6. Java 读取Word文本/段落格式属性

    本文介绍通过Java后端程序代码来读取Word文本和段落格式的方法. 本次测试环境如下: Word版本:2013 编译环境:IntelliJ IDEA2018 Work库:free spire.doc ...

  7. SqlServer触发器的创建与使用

    前言 上期我们介绍了SqlServer的视图和存储过程创建与使用,这期我们介绍一下触发器. 有需要回顾的可以电梯直达看一下: SqlServer视图的创建与使用 SqlServer存储过程的创建与使用 ...

  8. 系统编程-信号-总体概述和signal基本使用

    信号章节 -- 信号章节总体概要 信号基本概念 信号是异步事件,发送信号的线程可以继续向下执行而不阻塞. 信号无优先级. 1到31号信号是非实时信号,发送的信号可能会丢失,不支持信号排队. 31号信号 ...

  9. [Fundamental of Power Electronics]-PART I-2.稳态变换器原理分析-2.3 Boost 变换器实例

    2.3 Boost 变换器实例 图2.13(a)所示的Boost变换器器是另一个众所周知的开关模式变换器,其能够产生幅值大于直流输入电压的直流输出电压.图2.13(b)给出了使用MOSFET和二极管的 ...

  10. 消息中间件-ActiveMQ支持的消息协议

    package com.study.mq.a1_example.helloworld.queue; import org.apache.activemq.ActiveMQConnectionFacto ...