首页 > 代码库 > 长谈:关于 View Measure 测量机制,让我一次把话说完

长谈:关于 View Measure 测量机制,让我一次把话说完

《倚天屠龙记中》有这么一处:张三丰示范自创的太极剑演示给张无忌看,然后问他记住招式没有。张无忌说记住了一半。张三丰又慢吞吞使了一遍,问他记住多少,张无忌说只记得几招了。张三丰最后又示范了一遍,张无忌想了想说,这次全忘光了。张三丰很满意,于是放心让张无忌与八臂神剑去比试。

首先声明,这一篇篇幅很长很长很长的文章。目的就是为了把 Android 中关于 View 测量的机制一次性说清楚。算是自己对自己较真。写的时候花了好几天,几次想放弃,想放弃的原因不是我自己没有弄清楚,而是觉得自己叙事脉络已经紊乱了,感觉无法让读者整明白,怕把读者带到沟里面去,怕自己让人觉得罗嗦废话。但最后,我决定还是坚持下去,因为在反复纠结 –> 不甘 –> 探索 –> 论证 –> 质疑的过程循环中,我完成了对自己的升华,弄明白长久以来的一些困惑。所以,此文最大的目的是给自己作为一些学习记录,如果有幸帮助你解决一些困惑,那么我心宽慰。如果有错的地方,也欢迎指出批评。

如果你有这样的困扰:
1. 一个 View 的 parent 一定是 ViewGroup 吗?

  1. Android 自定义 View 的时候,经常对 onMeasure() 的理解不到位。有时感觉懂了,有时又有点懵。

  2. Android 自定义 View 的时候,经常对 onMeasure() 的理解不到位。有时感觉懂了,有时又有点懵。

  3. 在 xml 中设置一个 View 的属性 layout_width 为 wrap_content 或者 match_parent 而不是具体数值 50dp 时,为什么 view 也有正常的尺寸。

  4. 你或多或者知道 Android 测量时的 3 种布局模式:MeasureSpec.EXACTLY、Measure.AT_MOST、Measure.UNSPECIFIED。但你不大能够把握它们。

  5. 你不但对自定义 View 没有问题,对于自定义 ViewGroup 也不在话下,你明白 Android 给出的 3 种测量模式的含义,但是你还是没有来得及去思考,3 种测量模式本身是什么。

  6. 你也许没有想过 Activity 最外层的 View 是什么。

  7. 你也许知道 Activity 最外层的 View 叫做 DecorView。明白它与 PhoneWindow 及 Activity.setContentView() 的联系。但你不知道谁对 DecorView 进行了尺寸测量。

好了,文章正式开始。请深吸一口气,take it easy!

无法忽视的自定义 View 话题

Android 应用层开发绕不开自定义 View 这个话题,在 Android 中官方称之为 Widget,所以本文中的 View 其实与 Widget也就一个意思。虽然现在 Github 上有形形色色的开源库供大家使用,但是作为一名有想法的开发者而言,虽然不提倡重复造轮子,但是轮子都是造出来的。碰到一些新鲜的 UI 效果时,如果现有的 Widget 无法完成任务,那么我们就应该想到要自定义一个 View 了。
我们或多或少知道,在 Android 中 View 绘制流程有测量、布局、绘制三个步骤,它们分别对应 3 个 API :onMeasure()、onLayout()、onDraw()。
- 测量 onMeasure()
- 布局 onLayout()
- 绘制 onDraw()

没有办法说这三个阶段,那个阶段最重要,只是相对而言,测量阶段对于大多开发者而言难度相对其它两个要大,处理的细节也要多得多,自定义一个 View,正确的测量是第一步,正因为如此今天本文的主题就是讨论 View 中的测量机制和细节。

测量 View 就是测量一个矩形

得益于人们的想象力,Android 系统平台上出现了各种各样的 View。有 Button、TextView、ListView 等系统自带的组件,也有更多开发者自定义的 View。
技术分享

上面是 Android 系统自带的 Widget 表现,它们用来完成不同功能的交互与效果展示,但对于开发者而言,上面的界面还有这样的一面。
技术分享

透过另一个视角来观察,所有的 Widget

世界万物都有某些运行的规则,或者是突破不了的樊篱。对于一个 View 而言,它本质上就是一个矩形,一块四方的区域,铺开一张画布,然后利用所有的资源,在现有的规则之下天马行空。
技术分享

因此,自定义 View 的第一步,我们要在心里默念 – 我们现在要确定一个矩形了!

既然是矩形,那么它肯定有明确的宽高和位置坐标。宽高是在测量阶段得出,然后在布局阶段,根据实际需要确定好位置信息对矩形进行布局,之后的视觉效果就交给绘制流程了,它是画家,这个我们很放心。

打个比方,政府做城市规划时,房地产商们告诉政府他们希望的用地面积,政府综合政策和用地面积的实际情况,给地产商划分土地面积,地图上就是一个个圈圈。地产商们拿到明确的地域范围信息后,在规定好的区域建造自己的高楼或者大厦。而自定义 View 就是拿到这个类似政府规划的区域范围参数,只不过现实世界中,政府规划给地产商的土地不一定是四四方方的矩形,但是在 Android 中 View 拿到的区域一定是矩形。
技术分享

好了,我们知道了测量的就是长和宽,我们的目的也就是长和宽。

View 设置尺寸的基本方法

接下来的过程,我将会用一系列比较细致的实验来说明问题,觉得罗嗦无聊的同学可以直接跳过这一小节。
我们先看看在 Android 中使用 Widget 的时候,怎么定义大小。比如我们要在屏幕上使用一个 Button。

<Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="test"/>

这样屏幕上就出现了一个按钮。
技术分享
我们再把宽高固定。

<Button
        android:layout_width="200dp"
        android:layout_height="50dp"
        android:text="test"/>

技术分享
再换一种情况,将按钮的宽度由父容器决定

<Button
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:text="test"/>

技术分享

上面就是我们日常开发中使用的步骤,通过 layout_width 和 layout_height 属性来设置一个 View 的大小。而在 xml 中,这两个属性有 3 种取值可能。

  1. match_parent 代表这个维度上的值与父窗口一样
  2. wrap_content 表示这个维度上的值由 View 本身的内容所决定
  3. 具体数值如 5dp 表示这个维度上 View 给出了精确的值。

实验1

我们再进一步,现在给 Button 找一个父容器进行观察。父容器背景由特定颜色标识。

<RelativeLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="#ff0000">
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="test"/>
</RelativeLayout>

技术分享
可以看到 RelativeLayout 包裹着 Button。我们再换一种情况。

实验2

<RelativeLayout
    android:layout_width="150dp"
    android:layout_height="wrap_content"
    android:background="#ff0000">
    <Button
        android:layout_width="120dp"
        android:layout_height="wrap_content"
        android:text="test"/>
</RelativeLayout>

技术分享

实验3

<RelativeLayout
    android:layout_width="150dp"
    android:layout_height="wrap_content"
    android:background="#ff0000">
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="test"/>
</RelativeLayout>

技术分享

实验4

<RelativeLayout
    android:layout_width="150dp"
    android:layout_height="wrap_content"
    android:background="#ff0000">
    <Button
        android:layout_width="1000dp"
        android:layout_height="wrap_content"
        android:text="test"/>
</RelativeLayout>

技术分享

似乎发生了不怎么愉快的事情,Button 想要的长度是 1000 dp,而 RelativeLayout 最终给予的却仍旧是在自己的有限范围参数内。就好比山水庄园问光明开发区政府要地 1 万亩,政府说没有这么多,最多 2000 亩。

Button 是一个 View,RelativeLayout 是一个 ViewGroup。那么对于一个 View 而言,它相当于山水庄园,而 ViewGroup 类似于政府的角色。View 芸芸众生,它们的多姿多彩构成了美丽的 Android 世界,ViewGroup 却有自己的规划,所谓规划也就是以大局为重嘛,尽可能协调管辖区域内各个成员的位置关系。

山水庄园拿地盖楼需要同政府协商沟通,自定义一个 View 也需要同它所处的 ViewGroup 进行协商。

那么,它们的协议是什么?

View 和 ViewGroup 之间的测量协议 MeasureSpec

我们自定义一个 View,onMeasure()是一个关键方法。也是本文重点研究内容。

public class TestView extends View {
    public TestView(Context context) {
        super(context);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }
}

onMeasure() 中有两个参数 widthMeasureSpec、heightMeasureSpec。它们是什么?看起来和宽高有关。

它们确实和宽高有关,了解它们需要从一个类说起。MeasureSpec。

MeasureSpec

MeasureSpec 是 View.java 中一个静态类

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;


    public static final int UNSPECIFIED = 0 << MODE_SHIFT;


    public static final int EXACTLY     = 1 << MODE_SHIFT;


    public static final int AT_MOST     = 2 << MODE_SHIFT;


    public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                      @MeasureSpecMode int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }


  ......
    @MeasureSpecMode
    public static int getMode(int measureSpec) {
        //noinspection ResourceType
        return (measureSpec & MODE_MASK);
    }


    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }

    ......
}

MeasureSpec 的代码并不是很多,它有最重要的三个静态常量和三个最重要的静态方法。

MeasureSpec.UNSPECIFIED
MeasureSpec.EXACTLY
MeasureSpec.AT_MOST

MeasureSpec.makeMeasureSpec()
MeasureSpec.getMode()
MeasureSpec.getSize()

MeasureSpec 代表测量规则,而它的手段则是用一个 int 数值来实现。我们知道一个 int 数值有 32 bit。MeasureSpec 将它的高 2 位用来代表测量模式 Mode,低 30 位用来代表数值大小 Size。

技术分享

通过 makeMeasureSpec() 方法将 Mode 和 Size 组合成一个 measureSpec 数值。
而通过 getMode() 和 getSize() 却可以逆向地将一个 measureSpec 数值解析出它的 Mode 和 Size。

下面讲解 MeasureSpec 的 3 种测量模式。

MeasureSpec.UNSPECIFIED

此种模式表示无限制,子元素告诉父容器它希望它的宽高想要多大就要多大,你不要限制我。一般开发者几乎不需要处理这种情况,在 ScrollView 或者是 AdapterView 中都会处理这样的情况。所以我们可以忽视它。本文中的示例,基本上会跳过它。

MeasureSpec.EXACTLY

此模式说明可以给子元素一个精确的数值。


<Button
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:text="test"/>

当 layout_width 或者 layout_height 的取值为 match_parent 或者 明确的数值如 100dp 时,表明这个维度上的测量模式就是 MeasureSpec.EXACTLY。为什么 match_parent 也有精确的值呢?我们可以合理推断一下,子 View 希望和 父 ViewGroup 一样的宽或者高,对于一个 ViewGroup 而言它显然是可以决定自己的宽高的,所以当它的子 View 提出 match_parent 的要求时,它就可以将自己的宽高值设置下去。

MeasureSpec.AT_MOST

此模式下,子 View 希望它的宽或者高由自己决定。ViewGroup 当然要尊重它的要求,但是也有个前提,那就是你不能超过我能提供的最大值,也就是它期望宽高不能超过父类提供的建议宽高。
当一个 View 的 layout_width 或者 layout_height 的取值为 wrap_content 时,它的测量模式就是 MeasureSpec.AT_MOST。

了解上面的测量模式后,我们就要动手编写实例来验证一些想法了。

自定义 View

我的目标是定义一个文本框,中间显示黑色文字,背景色为红色。
技术分享

我们可以轻松地进行编码。首先,我们定义好它需要的属性,然后编写它的 java 代码。
attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="TestView">
        <attr name="android:text" />
        <attr name="android:textSize" />
    </declare-styleable>
</resources>

TestView.java

public class TestView extends View {

    private  int mTextSize;
    TextPaint mPaint;
    private String mText;

    public TestView(Context context) {
        this(context,null);
    }

    public TestView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public TestView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        TypedArray ta = context.obtainStyledAttributes(attrs,R.styleable.TestView);
        mText = ta.getString(R.styleable.TestView_android_text);
        mTextSize = ta.getDimensionPixelSize(R.styleable.TestView_android_textSize,24);

        ta.recycle();

        mPaint = new TextPaint();
        mPaint.setColor(Color.BLACK);
        mPaint.setTextSize(mTextSize);
        mPaint.setTextAlign(Paint.Align.CENTER);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int cx = (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
        int cy = (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;

        canvas.drawColor(Color.RED);
        if (TextUtils.isEmpty(mText)) {
            return;
        }
        canvas.drawText(mText,cx,cy,mPaint);

    }
}

布局文件

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.frank.measuredemo.MainActivity">

    <com.frank.measuredemo.TestView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:text="test"/>

</RelativeLayout>

效果
技术分享

我们可以看到在自定义 View 的 TestView 代码中,我们并没有做测量有关的工作,因为我们根本就没有复写它的 onMeasure() 方法。但它却完成了任务,给定 layout_width 和 layout_height 两个属性明确的值之后,它就能够正常显示了。我们再改变一下数值。

<com.frank.measuredemo.TestView
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:text="test"/>

将 layout_width 的值改为 match_parent,所以它的宽是由父类决定,但同样它也正常。
技术分享

我们已经知道,上面的两种情况其实就是对应 MeasureSpec.EXACTLY 这种测量模式,在这种模式下 TestView 本身不需要进行处理。

那么有人会问,如果 layout_width 或者 layout_height 的值为 wrap_content 的话,那么会怎么样呢?

我们继续测试观察。

<com.frank.measuredemo.TestView
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        android:text="test"/>

技术分享
效果和前面的一样,宽度和它的 ViewGroup 同样了。我们再看。

<com.frank.measuredemo.TestView
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:text="test"/>

技术分享

宽度正常,高度却和 ViewGroup 一样了。
再看一种情况

<com.frank.measuredemo.TestView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="test"/>

技术分享

这次可以看到,宽高都和 ViewGroup 一致了。

但是,这不是我想要的啊!

wrap_content 对应的测量模式是 MeasureSpec.AT_MOST,所以它的第一要求就是 size 是由 View 本身决定,最大不超过 ViewGroup 能给予的建议数值。

TestView 如果在宽高上设置 wrap_content 属性,也就代表着,它的大小由它的内容决定,在这里它的内容其实就是它中间位置的字符串。显然上面的不符合要求,那么就显然需要我们自己对测量进行处理。
我们的思路可以如下:
1. 对于 MeasureSpec.EXACTLY 模式,我们不做处理,将 ViewGroup 的建议数值作为最终的宽高。
2. 对于 MeasureSpec.AT_MOST 模式,我们要根据自己的内容计算宽高,但是数值不得超过 ViewGroup 给出的建议值。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        /**resultW 代表最终设置的宽,resultH 代表最终设置的高*/
        int resultW = widthSize;
        int resultH = heightSize;

        int contentW = 0;
        int contentH = 0;

        /**重点处理 AT_MOST 模式,TestView 自主决定数值大小,但不能超过 ViewGroup 给出的
         * 建议数值
         * */
        if ( widthMode == MeasureSpec.AT_MOST ) {

            if (!TextUtils.isEmpty(mText)){
                contentW = (int) mPaint.measureText(mText);
                contentW += getPaddingLeft() + getPaddingRight();
                resultW = contentW < widthSize ? contentW : widthSize;
            }

        }

        if ( heightMode == MeasureSpec.AT_MOST ) {
            if (!TextUtils.isEmpty(mText)){
                contentH = mTextSize;
                contentH += getPaddingTop() + getPaddingBottom();
                resultH = contentH < widthSize ? contentH : heightSize;
            }
        }

        //一定要设置这个函数,不然会报错
        setMeasuredDimension(resultW,resultH);

}

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    int cx = getPaddingLeft() + (getWidth() - getPaddingLeft() - getPaddingRight()) / 2;
    int cy = getPaddingTop() + (getHeight() - getPaddingTop() - getPaddingBottom()) / 2;

    metrics = mPaint.getFontMetrics();
    cy += metrics.descent;

    canvas.drawColor(Color.RED);
    if (TextUtils.isEmpty(mText)) {
        return;
    }
    canvas.drawText(mText,cx,cy,mPaint);

}

代码并不难,我们可以做验证。

<com.frank.measuredemo.TestView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:paddingLeft="2dp"
    android:paddingRight="2dp"
    android:paddingTop="2dp"
    android:textSize="24sp"
    android:text="test"/>

技术分享
可以看到这才是我们想要的效果。它现在完成了对 MeasureSpec.AT_MOST 模式的适配。我们再验证一下另外一种情况

<com.frank.measuredemo.TestView
    android:layout_width="300dp"
    android:layout_height="wrap_content"
    android:paddingLeft="2dp"
    android:paddingRight="2dp"
    android:paddingTop="2dp"
    android:textSize="48sp"
    android:text="test"/>

技术分享
在 MeasureSpec.EXACTLY 模式下同样没有问题。

现在,我们已经掌握了自定义 View 的测量方法,其实也很简单的嘛。

但是,还没有完。我们验证的刚刚是自定义 View,对于 ViewGroup 的情况是有些许不同的。

View 和 ViewGroup,鸡生蛋,蛋生鸡的关系

ViewGroup 是 View 的子类,但是 ViewGroup 的使命却是装载和组织 View。这好比是母鸡是鸡,母鸡下蛋是为了孵化小鸡,小鸡长大后如果是母鸡又下蛋,那么到底是蛋生鸡还是鸡生蛋?

技术分享

自定义 View 的测量,我们已经掌握了,那现在我们编码来测试自定义 ViewGroup 时的测量变现。
假设我们要制定一个 ViewGroup,我们就给它起一个名字叫 TestViewGroup 好了,它里面的子元素按照对角线铺设,如下图:
技术分享

前面说过 ViewGroup 本质上也是一个 View,只不过它多了布局子元素的义务。既然是 View 的话,那么自定义一个 ViewGroup 也需要从测量开始,问题的关键是如何准确地得到这个 ViewGroup 尺寸信息?

我们还是需要仔细讨论。

  1. 当 TestViewGroup 某一维度上的测量模式为 MeasureSpec.EXACTLY 时,这时候的尺寸就可以按照父容器传递过来的建议尺寸。要知道 ViewGroup 也有自己的 parent,在它的父容器中,它也只是一个 View。
  2. 当 TestViewGroup 某一维度上的测量模式为 MeasureSpec.AT_MOST 时,这就需要 TestViewGroup 自己计算这个维度上的尺寸数值。就上面给出的信息而言,TestViewGroup 的尺寸非常简单,那就是在一个维度上用自身 padding + 各个子元素的尺寸(包含子元素的宽高+子元素设置的 marging )得到一个可能的尺寸数值。然后用这个尺寸数值与 TestViewGroup 的父容器给出的建议 Size 进行比较,最终结果取最较小值。
  3. 当 TestViewGroup 某一维度上的测量模式为 MeasureSpec.AT_MOST 时,因为要计算子元素的尺寸,所以如何准确得到子元素的尺寸也是至关重要的事情。好在 Android 提供了现成的 API。
  4. 当 TestViewGroup 测量成功后,就需要布局了。自定义 View 基本上不要处理这一块,但是自定义 ViewGroup,这一部分却不可缺少。但本篇文章不是讨论布局技巧的,只是告诉大家布局其实相对而言更简单一点,无非是确定好子元素的坐标然后进行布局。

接下来,我们就可以具体编码了。

public class TestViewGroup extends ViewGroup {


    public TestViewGroup(Context context) {
        this(context,null);
    }

    public TestViewGroup(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public TestViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

    }


    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        //只关心子元素的 margin 信息,所以这里用 MarginLayoutParams
        return new MarginLayoutParams(getContext(),attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        /**resultW 代表最终设置的宽,resultH 代表最终设置的高*/
        int resultW = widthSize;
        int resultH = heightSize;

        /**计算尺寸的时候要将自身的 padding 考虑进去*/
        int contentW = getPaddingLeft() + getPaddingRight();
        int contentH = getPaddingTop() + getPaddingBottom();

        /**对子元素进行尺寸的测量,这一步必不可少*/
        measureChildren(widthMeasureSpec,heightMeasureSpec);

        MarginLayoutParams layoutParams = null;

        for ( int i = 0;i < getChildCount();i++ ) {
            View child = getChildAt(i);
            layoutParams = (MarginLayoutParams) child.getLayoutParams();

            //子元素不可见时,不参与布局,因此不需要将其尺寸计算在内
            if ( child.getVisibility() == View.GONE ) {
                continue;
            }

            contentW += child.getMeasuredWidth()
                    + layoutParams.leftMargin + layoutParams.rightMargin;

            contentH += child.getMeasuredHeight()
                    + layoutParams.topMargin + layoutParams.bottomMargin;
        }

        /**重点处理 AT_MOST 模式,TestViewGroup 通过子元素的尺寸自主决定数值大小,但不能超过
         *  ViewGroup 给出的建议数值
         * */
        if ( widthMode == MeasureSpec.AT_MOST ) {
            resultW = contentW < widthSize ? contentW : widthSize;
        }

        if ( heightMode == MeasureSpec.AT_MOST ) {
            resultH = contentH < heightSize ? contentH : heightSize;
        }

        //一定要设置这个函数,不然会报错
        setMeasuredDimension(resultW,resultH);

    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int topStart = getPaddingTop();
        int leftStart = getPaddingLeft();
        int childW = 0;
        int childH = 0;
        MarginLayoutParams layoutParams = null;
        for ( int i = 0;i < getChildCount();i++ ) {
            View child = getChildAt(i);
            layoutParams = (MarginLayoutParams) child.getLayoutParams();

            //子元素不可见时,不参与布局,因此不需要将其尺寸计算在内
            if ( child.getVisibility() == View.GONE ) {
                continue;
            }

            childW = child.getMeasuredWidth();
            childH = child.getMeasuredHeight();

            leftStart += layoutParams.leftMargin;
            topStart += layoutParams.topMargin;


            child.layout(leftStart,topStart, leftStart + childW, topStart + childH);

            leftStart += childW + layoutParams.rightMargin;
            topStart += childH + layoutParams.bottomMargin;
        }

    }

}

然后我们将之添加进 xml 布局文件中进行测试。

<com.frank.measuredemo.TestViewGroup
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">
    <com.frank.measuredemo.TestView
        android:layout_width="120dp"
        android:layout_height="wrap_content"
        android:paddingLeft="2dp"
        android:paddingRight="2dp"
        android:paddingTop="2dp"
        android:textSize="24sp"
        android:text="test"/>
    <TextView
        android:layout_width="120dp"
        android:layout_height="50dp"
        android:paddingLeft="2dp"
        android:paddingRight="2dp"
        android:paddingTop="2dp"
        android:textSize="24sp"
        android:background="#00ff40"
        android:text="test"/>
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"
        android:text="test"/>
</com.frank.measuredemo.TestViewGroup>

那么实际效果如何呢?
技术分享

再试验一下给 TestViewGroup 加上固定宽高。

<com.frank.measuredemo.TestViewGroup
    android:layout_width="350dp"
    android:layout_height="600dp"
    android:background="#c3c3c3">
    <com.frank.measuredemo.TestView
        android:layout_width="120dp"
        android:layout_height="wrap_content"
        android:paddingLeft="2dp"
        android:paddingRight="2dp"
        android:paddingTop="2dp"
        android:textSize="24sp"
        android:text="test"/>
    <TextView
        android:layout_width="120dp"
        android:layout_height="50dp"
        android:paddingLeft="2dp"
        android:paddingRight="2dp"
        android:paddingTop="2dp"
        android:textSize="24sp"
        android:background="#00ff40"
        android:text="test"/>
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"
        android:text="test"/>
</com.frank.measuredemo.TestViewGroup>

结果如下:
技术分享

自此,我们也知道了自定义 ViewGroup 的基本步骤,并且能够处理 ViewGroup 的各种测量模式。

但是,在现实工作开发过程中,需求是不定的,我上面讲的内容只是基本的规则,大家熟练于心的时候才能从容应对各种状况。

TestViewGroup 作为一个演示用的例子,只为了说明测量规则和基本的自定义套路。对于 Android 开发初学者而言,还是要多阅读代码,关键是要多临摹别人的优秀的自定义 View 或者 ViewGroup。

我个人觉得,尝试自己动手去实现一个流式标签控件,对于提高自定义 ViewGroup 的能力是有很大的提高,因为只有在自己实践的时候,你都会思考,在思考和实验的过程你才会深刻的理解测量机制的用途。

不过自定义一个流式标签控件是另外一个话题了,也许我会另外开一篇来讲解,不过我希望大家亲自动手去实现它。下面是我自定义 ViewGroup 的一个实例截图。
技术分享


洋洋洒洒写了这么多的内容,其实基本上已经完结了,已经不耐烦的同学可以直接跳转到后面的总结。但是,对于有钻研精神的同学来讲,其实还不够。还没有完。

问题1:到底是谁在测量 View ?

问题2:到底是什么时候需要测量 View ?

针对问题 1:
我们在自定义 TestViewGroup 的时候,在 onMeasure() 方法中,通过了一个 API 对子元素进行了测量,这个 API 就是 measureChildren()。这个方法进行了什么样的处理呢?我们可以去看看。

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);
        }
    }
}

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
    final 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);
}

代码简短易懂,分别调用 child 的 measure() 方法。值得注意的是,传递给 child 的测量规格已经发生了变化,比如 widthMeasureSpec 变成了 childWidthMeasureSpec。原因是这两行代码:

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
        mPaddingTop + mPaddingBottom, lp.height);

硬着头皮再去看 getChildMeasureSpec() 方法的实现。

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        // 父类本身就是 EXACTLY 模式下
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                // 如果 child 希望有自己的尺寸,那么满足它,并把它的测量模式设置为 EXACTLY
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                // child 希望尺寸和 parent 一样大,那么满足它
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can‘t be
                // bigger than us.
                // child 希望由自己决定自己的尺寸,但是有个前提是它不能大于 parent 本身给的建议值。
                // 并且需要设置它的测量模式为 AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        // 父类本身就是 AT_MOST 模式下,对于 parent 本身而言,它也有个最大的尺寸约束
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                // 如果 child 希望有自己的尺寸,那么满足它,并把它的测量模式设置为 EXACTLY
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                // child 希望尺寸和 parent 一样大,那么满足它,但是 parent 本身有限制,所以也
                // 需要给 child 加一个限制,这个限制就是将 child 模式设置为 AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can‘t be
                // bigger than us.
                // child 希望由自己决定自己的尺寸,但是有个前提是它不能大于 parent 本身给的建议值。
                // 并且需要设置它的测量模式为 AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        // 父类本身就是 UNSPECIFIED 模式下,对于 parent 本身而言,它的期望值是想要多大就多大,让 parent 的 parent不要限制它。
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                //如果 child 希望有自己的尺寸,那么满足它,并把它的测量模式设置为 EXACTLY
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                // child 希望尺寸和 parent 一样大,那么满足它,但是 parent 本身也需要计算,所以只能设置为0,并且
                // 将child的测量模式设置为 UNSPECIFIED
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                // child 希望尺寸由自己决定,一般这个时候,parent会给它一个 Size 作为最大值限制,
                // 但是 parent 本身也需要计算,所以只能设置为0,并且没有给child最大值的设定
                // 将child的测量模式设置为 UNSPECIFIED
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

代码很清楚,很多事情真相大白了。解释了很多东西,包括:

  1. 为什么 layout_width 或者 layout_height 设置了精确的数值时,它的测量模式为 MeasureSpec.EXACTLY。

  2. 为什么 layout_width 或者 layout_height 设置为 match_parent 时,不一定保证它的测量模式为 MeasureSpec.EXACTLY。它也有可能是 MeasureSpec.AT_MOST,也有可能是 MeasureSpec.UNSPECIFIED。

  3. 为什么 layout_width 或者 layout_height 设置为 wrap_content 时,它最可能的测量模式为 MeasureSpec.AT_MOST,但也有可能是 MeasureSpec.UNSPECIFIED。

我们继续向前,ViewGroup 的 measureChild() 方法最终会调用 View.measure() 方法。我们进一步跟踪。


public final void measure(int widthMeasureSpec, int heightMeasureSpec) {


        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

        // Optimize layout by avoiding an extra EXACTLY pass when the view is
        // already measured as the correct size. In API 23 and below, this
        // extra pass is required to make LinearLayout re-distribute weight.
        final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                || heightMeasureSpec != mOldHeightMeasureSpec;
        final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
        final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
        final boolean needsLayout = specChanged
                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

        if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;



            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            if (cacheIndex < 0 || sIgnoreMeasureCache) {


                // measure ourselves, this should set the measured dimension flag back
                // onMeasure 在此调用
                onMeasure(widthMeasureSpec, heightMeasureSpec);


                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } 

            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("View with id " + getId() + ": "
                        + getClass().getName() + "#onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }

            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;


    }

代码比较长,删掉了一些与主题无关的代码,我们可以看到,当一个 view 的 measure() 方法调用时,如果它此次的 measureSpec 与 旧的不相同或者是它需要重新布局也就是 requestLayout() 方法被调用时,它会触发 onMeasure() 方法。值得注意的是最后它会检查变量 mPrivateFlags 的一个位 PFLAG_MEASURED_DIMENSION_SET,如果这一位没有置为 1 的话,会抛出异常。而这一位在 onMeasure() 方法中一定要调用。

好了,鸡生蛋,蛋生鸡结论似乎有个结果了。是 ViewGroup 中的 onMeasure() 调用了 View.measure() 而 View.measure() 调用了 View.onMeasure()。

技术分享

于是,我们终于明白了。

但是呢,还能更深入一点吗?

Activity 中的道,最顶层的那个 View?

道生一,一生二,二生三,三生万物,万物负阴而抱阳,冲气以为和。– 《道德经》

我们已经知道,不管是对于 View 还是 ViewGroup 而言,测量的起始是 measure() 方法,沿着控件树一路遍历下去。那么,对于 Android 一个 Activity 而言,它的顶级 View 或者顶级 ViewGroup 是哪一个呢?

从 setContentView 说起

我们知道给 Activity 布局的时候,在 onCreate() 中设置 setContentView() 的资源文件就是我们普通开发者所能想到的比较顶层的 View 了。比如在 activity_main.xml 中设置一个 RelativeLayout,那么这个 RelativeLayout 就是 Activity 最顶层的 View 吗?谁调用它的 measure() 方法触发整个控件树的测量?


public void setContentView(int layoutResID) {
    getWindow().setContentView(layoutResID);
    initActionBar();
}

public Window getWindow() {
    return mWindow;
}

/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.policy.PhoneWindow, which you should instantiate when needing a
 * Window.  Eventually that class will be refactored and a factory method
 * added for creating Window instances without knowing about a particular
 * implementation.
 */
public abstract class Window {

}

public class PhoneWindow extends Window implements MenuBuilder.Callback {

}

可以看到,调用 Activity.setContentView() 其实就是调用 PhoneWindow.setContentView()。

PhoneWindow.java

@Override
public void setContentView(View view) {
    setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}

@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
    if (mContentParent == null) {
        installDecor();
    } else {
        mContentParent.removeAllViews();
    }
    mContentParent.addView(view, params);
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
}

注意,在上面代码中显示,通过 setContentView 传递进来的 view 被添加到了一个 mContentParent 变量上了,所以可以回答上面的问题,通过 setContentView() 中传递的 View 并不是 Activity 最顶层的 View。我们再来看看 mContentParent。

它只是一个 ViewGroup。我们再把焦点聚集到 installDecor() 这个函数上面。

private void installDecor() {
    if (mDecor == null) {
        mDecor = generateDecor();

    }
    if (mContentParent == null) {
        mContentParent = generateLayout(mDecor);

        // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
        mDecor.makeOptionalFitsSystemWindows();

        mTitleView = (TextView)findViewById(com.android.internal.R.id.title);
        if (mTitleView != null) {
            mTitleView.setLayoutDirection(mDecor.getLayoutDirection());
            if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
                View titleContainer = findViewById(com.android.internal.R.id.title_container);
                if (titleContainer != null) {
                    titleContainer.setVisibility(View.GONE);
                } else {
                    mTitleView.setVisibility(View.GONE);
                }
                if (mContentParent instanceof FrameLayout) {
                    ((FrameLayout)mContentParent).setForeground(null);
                }
            } else {
                mTitleView.setText(mTitle);
            }
        } else {
            mActionBar = (ActionBarView) findViewById(com.android.internal.R.id.action_bar);


        }
    }
}

代码很长,我删除了一些与主题无关的代码。这个方法体内引出了一个 mDecor 变量,它通过 generateDecor() 方法创建。DecorView 是 PhoneWindow 定义的一个内部类,实际上是一个 FrameLayout。

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {

}

我们回到 generate() 方法

protected DecorView generateDecor() {
    return new DecorView(getContext(), -1);
}

DecorView 怎么创建的我们已经知晓,现在看看 mContentParent 创建方法 generateLayout()。它传递进了一个 DecorView,所以它与 mDecorView 肯定有某种关系。

protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.




    WindowManager.LayoutParams params = getAttributes();



    // Inflate the window decor.



    // Embedded, so no decoration is needed.
    layoutResource = com.android.internal.R.layout.screen_simple;
    // System.out.println("Simple!");


    mDecor.startChanging();

    View in = mLayoutInflater.inflate(layoutResource, null);

    decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));

    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    if (contentParent == null) {
        throw new RuntimeException("Window couldn‘t find content container view");
    }


    mDecor.finishChanging();

    return contentParent;
}

原代码很长,我删除了一些繁琐的代码,整个流程变得很清晰,这个方法内 inflate 了一个 xml 文件,然后被添加到了 mDecorView。而 mContentParent 就是这个被添加进去的 view 中。
这个 xml 文件是 com.android.internal.R.layout.screen_simple,我们可以从 SDK 包中找出它来。
技术分享

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

就是一个 LinearLayout ,方向垂直。2 个元素,一个是 actionbar,一个是 content。并且 ViewStub 导致 actionbar 需要的时候才会进行加载。

总之由以上信息,我们可以得到 Activity 有一个 PhoneWindow 对象,PhoneWindow 中有一个 DecorView,DecorView 内部有一个 LinearLayout,LinearLayout 中存在 id 为 android:id/content 的布局 mContentParent。mContentParent 用来加载 Activity 通过 setContentView 传递进来的 View。所以整个结构呼之欲出。

注意:因为代码有删简,实际上 LinearLayout 由两部分组成,下面的是 Content 无疑,上面的部分不一定是 ActionBar,也可能是 title,不过这不影响我们,我们只需要记住 content 就好了。

技术分享

DecorView 才是 Activity 中整个控件树的根。

谁测绘了顶级 View ?

既然 DecorView 是整个测绘的发起点,那么谁对它进行了测绘?谁调用了它的 measure() 方法,从而导致整个控件树自上至下的尺寸测量?

我们平常开发知道调用一个 View.requestLayout() 方法,可以引起界面的重新布局,那么 requestLayout() 干了什么?

我们再回到 PhoneWindow 的 setContentView() 中来。

public void setContentView(View view, ViewGroup.LayoutParams params) {
    if (mContentParent == null) {
        installDecor();
    } else {
        mContentParent.removeAllViews();
    }
    mContentParent.addView(view, params);
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
}

我们看看 mContentParent.addView(view, params) 的时候发生了什么。

public void addView(View child, int index, LayoutParams params) {
    if (DBG) {
        System.out.println(this + " addView");
    }

    // addViewInner() will call child.requestLayout() when setting the new LayoutParams
    // therefore, we call requestLayout() on ourselves before, so that the child‘s request
    // will be blocked at our level
    requestLayout();
    invalidate(true);
    addViewInner(child, index, params, false);
}

它调用了 requestLayout(),正好要探索它,所以说 setContentView 之后,控件树会进行一次测绘。不过这里是结论,其过程需要我们来验证。

public void requestLayout() {
    if (mMeasureCache != null) mMeasureCache.clear();

    if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
        // Only trigger request-during-layout logic if this is the view requesting it,
        // not the views in its parent hierarchy
        ViewRootImpl viewRoot = getViewRootImpl();
        if (viewRoot != null && viewRoot.isInLayout()) {
            if (!viewRoot.requestLayoutDuringLayout(this)) {
                return;
            }
        }
        mAttachInfo.mViewRequestingLayout = this;
    }

    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    mPrivateFlags |= PFLAG_INVALIDATED;

    if (mParent != null && !mParent.isLayoutRequested()) {
        mParent.requestLayout();
    }
    if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
        mAttachInfo.mViewRequestingLayout = null;
    }
}

View 在 requestLayout() 中给 mPrivateFlags 添加了 2 个标记 PFLAG_FORCE_LAYOUT 和 PFLAG_INVALIDATED。还引出了另外一个对象 ViewRootImpl。然后调用 parent 的 requestLayout()。

我们在想最顶层的 View 是 DecorView,但是它有 parent 吗?如果有的话,那是什么?我在此困扰了好久,觉得 DecorView 没有父容器,所以我把自己追踪的线索弄丢了,好在后来找到了。现在可以提示大家一下。
View 中的 mParent 其实是一个接口:ViewParent。

/**
 * Defines the responsibilities for a class that will be a parent of a View.
 * This is the API that a view sees when it wants to interact with its parent.
 * 
 */
public interface ViewParent {
    /**
     * Called when something has changed which has invalidated the layout of a
     * child of this view parent. This will schedule a layout pass of the view
     * tree.
     */
    public void requestLayout();

   ......

}

我之前犯糊涂了,以为一个 View 的 parent 一定是 ViewGroup。其实 ViewGroup 只是 ViewParent 接口的实现类之一。

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
}

我们再看看 ViewRootImpl 的源码。


/**
 * The top of a view hierarchy, implementing the needed protocol between View
 * and the WindowManager.  This is for the most part an internal implementation
 * detail of {@link WindowManagerGlobal}.
 *
 * {@hide}
 */
@SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"})
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {

}

可以看到 ViewRootImpl 也是 ViewParent 的实现类,所以说理论上它也可以成为 DecorView 的 parent。我们再注意到注释解释说 ViewRootImpl 是控件树的最顶端。更多的细节要我去查找 WindowManagerGlobal。好吧,那就找吧。


/**
 * Provides low-level communication with the system window manager for
 * operations that are not associated with any particular context.
 *
 * This class is only used internally to implement global functions where
 * the caller already knows the display and relevant compatibility information
 * for the operation.  For most purposes, you should use {@link WindowManager} instead
 * since it is bound to a context.
 *
 * @see WindowManagerImpl
 * @hide
 */
public final class WindowManagerGlobal {

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        if (view == null) {
            throw new IllegalArgumentException("view must not be null");
        }
        if (display == null) {
            throw new IllegalArgumentException("display must not be null");
        }
        if (!(params instanceof WindowManager.LayoutParams)) {
            throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
        }

        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
        if (parentWindow != null) {
            parentWindow.adjustLayoutParamsForSubWindow(wparams);
        }

        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {


            root = new ViewRootImpl(view.getContext(), display);

            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
        }

        // do this last because it fires off messages to start doing things
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            synchronized (mLock) {
                final int index = findViewLocked(view, false);
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
            }
            throw e;
        }
    }


}

代码不是很多,其中 addView() 方法引起了我的兴趣。
在此,创建了 ViewRootImpl 对象。并且这个类的注释叫我去查看 WindowManagerImpl。

感觉越陷越深了,没有办法,深吸一口气,继续向前。

public final class WindowManagerImpl implements WindowManager {
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    private final Display mDisplay;
    private final Window mParentWindow;

    public WindowManagerImpl(Display display) {
        this(display, null);
    }


    @Override
    public void addView(View view, ViewGroup.LayoutParams params) {
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }


}

原来 WindowManagerImpl 的内部有一个 WindowManagerGlobal 对象,WindowManagerGlobal 代理 WindowManagerImpl ,并且 WindowManagerImpl 是 WindowManager 的实现类。
也就是我们平常编写悬浮窗经常用到的。

WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);

已经走到这一步了,貌似线索已经断了,不过,需要明白一点是 Activity 也是一个 Window,所以创建 Activity 的时候一定会通过 WindowManager addView。

把焦点回到 Activity,里面还有个 PhoneWindow 值得我们挖掘。在 Activity 源码中搜索 window 被赋值的地方。结果找到了。

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config) {
        attachBaseContext(context);

        mFragments.attachActivity(this, mContainer, null);

        mWindow = PolicyManager.makeNewWindow(this);
        mWindow.setCallback(this);
        mWindow.getLayoutInflater().setPrivateFactory(this);

        mUiThread = Thread.currentThread();

        mMainThread = aThread;
        mInstrumentation = instr;
        mToken = token;
        mIdent = ident;
        mApplication = application;
        mIntent = intent;
        mComponent = intent.getComponent();
        mActivityInfo = info;
        mTitle = title;
        mParent = parent;
        mEmbeddedID = id;
        mLastNonConfigurationInstances = lastNonConfigurationInstances;

        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        if (mParent != null) {
            mWindow.setContainer(mParent.getWindow());
        }
        mWindowManager = mWindow.getWindowManager();
        mCurrentConfig = config;
 }

mWindow 通过 PolicyManager 创建。并且与 WindowManager建立了联系。

public final class PolicyManager {
    private static final String POLICY_IMPL_CLASS_NAME =
        "com.android.internal.policy.impl.Policy";

    private static final IPolicy sPolicy;

    static {
        // Pull in the actual implementation of the policy at run-time
        try {
            Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME);
            sPolicy = (IPolicy)policyClass.newInstance();
        } catch (ClassNotFoundException ex) {
            throw new RuntimeException(
                    POLICY_IMPL_CLASS_NAME + " could not be loaded", ex);
        } catch (InstantiationException ex) {
            throw new RuntimeException(
                    POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
        } catch (IllegalAccessException ex) {
            throw new RuntimeException(
                    POLICY_IMPL_CLASS_NAME + " could not be instantiated", ex);
        }
    }

    // Cannot instantiate this class
    private PolicyManager() {}

    // The static methods to spawn new policy-specific objects
    public static Window makeNewWindow(Context context) {
        return sPolicy.makeNewWindow(context);
    }

}

通过反射创建了一个 Policy 对象,然后调用它的 makeNewWindow() 方法得到了一个 window() 对象。

public class Policy implements IPolicy {
    private static final String TAG = "PhonePolicy";

    private static final String[] preload_classes = {
        "com.android.internal.policy.impl.PhoneLayoutInflater",
        "com.android.internal.policy.impl.PhoneWindow",
        "com.android.internal.policy.impl.PhoneWindow$1",
        "com.android.internal.policy.impl.PhoneWindow$DialogMenuCallback",
        "com.android.internal.policy.impl.PhoneWindow$DecorView",
        "com.android.internal.policy.impl.PhoneWindow$PanelFeatureState",
        "com.android.internal.policy.impl.PhoneWindow$PanelFeatureState$SavedState",
    };

    static {
        // For performance reasons, preload some policy specific classes when
        // the policy gets loaded.
        for (String s : preload_classes) {
            try {
                Class.forName(s);
            } catch (ClassNotFoundException ex) {
                Log.e(TAG, "Could not preload class for phone policy: " + s);
            }
        }
    }

    public Window makeNewWindow(Context context) {
        return new PhoneWindow(context);
    }


}

到此,我们终于知道了 Activity 中的 mWindow 就是一个 PhoneWindow。

兜兜转转,我们似乎回到了原点,不过值得庆幸的是我们知道了 Activity 中 PhoneWindow 的由来。

顶层的 View,由谁测绘?

为了避免大家遗忘,我把问题又抛出来。这是我们的终极目标,明确了这个答案之后,本篇文章就结束了。

不过,对于现在而言,目前掌握的信息,我们似乎无从得知,依照上面的思路步骤,我们无法找到这个问题的答案。此路不通,我们就需要想想其它办法了。我们要寻找的是,最开始的时候是谁调用了 DecorView.measure() 方法最终导致整个控件树的重绘。现在线索继了,我们只得再从 Activity 身上挖掘。

说说 Activity 相关

我之前阅读过 ActivityManager、WindowManager、PackageManager 的源码。我们大多最关心的是 Activity 的生命周期流程。

是的,我们在 Activity 中的 onCreate() 方法中加载布局文件,但是却要在 onResume() 之后,图像才能显示出来。所以,我们可以从这里入手。

Activity 的流程我只简单过一篇,因为是另外的主题了,并且这个主题很庞大繁琐,所以很多的细节我会选择忽略。不熟悉的同学可以自己翻阅资料。

Activity 的创建

Java 学习的时候,大家都知道要运行起来就要写一个类,然后包含一个 main() 方法,但是 Activity 没有让我们实现 main(),程序也照样跑起来了,这个估计是很多 Android 初学者比较困扰的地方。其实,Activity 是运行在 Android Framework 之上的,我们编写的程序其实是运行在 Google 工程师们弄好的程序之上,可以说是别人程序中的片断,这就是框架。Android 的应用运行的时候会创建一个包含 main() 方法的类,这个类叫 ActivityThread。一看名字就知道与 Activity 有关。
我们再来引出另一个类 ActivityManagerService,它是系统最重要的服务之一,用来调度和处理应用层客户端传递过来的相关的请求。

Activity 的 onResume() 由来

Activity 第一次启动时,肯定是先执行 onCreate() 然后才执行 onResume() 方法的。

 private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {


       ·
    Activity a = performLaunchActivity(r, customIntent);

    if (a != null) {
        r.createdConfig = new Configuration(mConfiguration);
        Bundle oldState = r.state;

        handleResumeActivity(r.token, false, r.isForward,
                !r.activity.mFinished && !r.startsNotResumed);

    } else {
        // If there was an error, for any reason, tell the activity
        // manager to stop us.
        try {
            ActivityManagerNative.getDefault()
                .finishActivity(r.token, Activity.RESULT_CANCELED, null);
        } catch (RemoteException ex) {
            // Ignore
        }
    }
}

可以看到是先执行 performLaunchActivity() 再执行 handleResumeActivity() 的。performLauncherActivity() 中是创建 Activity 并调用 onCreate() 方法。感兴趣的同学自己去查阅代码。

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward,
            boolean reallyResume) {


    ActivityClientRecord r = performResumeActivity(token, clearHide);

    if (r != null) {
        final Activity a = r.activity;


        if (r.window == null && !a.mFinished && willBeVisible) {
            // 重点在这一段代码
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;

            if (a.mVisibleFromClient) {
                a.mWindowAdded = true;

                wm.addView(decor, l);

            }
        }
    }   
}

可能看到 ActivityThread 在 handleResumeActivity() 方法中调用了 WindowManager.addView() 方法,这个方法最终调用的就是本文前面部分已经讲到过的 WindowManagerGlobal.addView()。

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {


    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;

    ViewRootImpl root;
    View panelParentView = null;

    synchronized (mLock) {

        root = new ViewRootImpl(view.getContext(), display);

        view.setLayoutParams(wparams);

        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
    }

    // do this last because it fires off messages to start doing things
    try {
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
        // BadTokenException or InvalidDisplayException, clean up.
        synchronized (mLock) {
            final int index = findViewLocked(view, false);
            if (index >= 0) {
                removeViewLocked(index, true);
            }
        }
        throw e;
    }
}

之前我们看到这一段代码的时候,不明白它的用途。现在可以算是明白了。Activity 创建后,onResume() 执行的时候,要将 DecorView 保存进 ViewRootImpl。

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
            if (mView == null) {
                mView = view;

                mAdded = true;

                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.
                requestLayout();


                view.assignParent(this);

            }
        }

}

我精简了代码,注意 ViewRootImpl 将自己设置成为了 DecorView 的 parent,这说明了一个问题:

控件树中任何一个 View 调用 requestLayout() 方法,最终会调用 Activity 相关联的 ViewRootImpl 的 requestLayout() 方法,可以说 ViewRootImpl 操纵了整棵控件树,它的名字名符其实

通过 setView() 方法 ViewRootImpl 将 DecorView 保存到 mView 变量中,并执行了 ViewRootImpl.requestLayout() 方法。

public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().postSyncBarrier();
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        scheduleConsumeBatchedInput();
    }
}

mChoreographer 是一个 Choreographer 对象,你可以简单把它当作一个 Handler 就好,用来编排 Android 界面的绘制。它最终会调用 doTraversal() 方法。

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().removeSyncBarrier(mTraversalBarrier);

        try {
            performTraversals();
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }

    }
}

最终调用的是ViewRootImpl 的 performTraversals() 方法。这个代码非常非常非常的长。我只节选我们需要的部分,完整信息请参照源码。

private void performTraversals() {
        // cache mView since it is used so much below...
        final View host = mView;


        if (host == null || !mAdded)
            return;

        mIsInTraversal = true;


        WindowManager.LayoutParams lp = mWindowAttributes;

        int desiredWindowWidth;
        int desiredWindowHeight;



        WindowManager.LayoutParams params = null;
        if (mWindowAttributesChanged) {
            mWindowAttributesChanged = false;
            surfaceChanged = true;
            params = lp;
        }



        if (mFirst) {


            if (lp.type == WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL
                    || lp.type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) {

            } else {
                DisplayMetrics packageMetrics =
                    mView.getContext().getResources().getDisplayMetrics();
                desiredWindowWidth = packageMetrics.widthPixels;
                desiredWindowHeight = packageMetrics.heightPixels;
            }


        } else {

        }



        boolean layoutRequested = mLayoutRequested && !mStopped;
        if (layoutRequested) {

            final Resources res = mView.getContext().getResources();


            // Ask host how big it wants to be
            windowSizeMayChange |= measureHierarchy(host, lp, res,
                    desiredWindowWidth, desiredWindowHeight);
        }   

    }

上面是删除了很多代码的结果,主要为了说明 performTraversals() 在第一次测量的时候,将建议尺寸填写成了屏幕的宽高,然后调用了 measureHierarchy() 方法。

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
        int childWidthMeasureSpec;
        int childHeightMeasureSpec;
        boolean windowSizeMayChange = false;

        if (DEBUG_ORIENTATION || DEBUG_LAYOUT) Log.v(TAG,
                "Measuring " + host + " in display " + desiredWindowWidth
                + "x" + desiredWindowHeight + "...");

        boolean goodMeasure = false;
        if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
            // On large screens, we don‘t want to allow dialogs to just
            // stretch to fill the entire width of the screen to display
            // one line of text.  First try doing the layout at a smaller
            // size to see if it will fit.
            final DisplayMetrics packageMetrics = res.getDisplayMetrics();
            res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true);
            int baseSize = 0;
            if (mTmpValue.type == TypedValue.TYPE_DIMENSION) {
                baseSize = (int)mTmpValue.getDimension(packageMetrics);
            }
            if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": baseSize=" + baseSize);
            if (baseSize != 0 && desiredWindowWidth > baseSize) {
                childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": measured ("
                        + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
                if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                    goodMeasure = true;
                } else {
                    // Didn‘t fit in that size... try expanding a bit.
                    baseSize = (baseSize+desiredWindowWidth)/2;
                    if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": next baseSize="
                            + baseSize);
                    childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    if (DEBUG_DIALOG) Log.v(TAG, "Window " + mView + ": measured ("
                            + host.getMeasuredWidth() + "," + host.getMeasuredHeight() + ")");
                    if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) {
                        if (DEBUG_DIALOG) Log.v(TAG, "Good!");
                        goodMeasure = true;
                    }
                }
            }
        }

        if (!goodMeasure) {
            childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
                windowSizeMayChange = true;
            }
        }



        return windowSizeMayChange;
    }

根据 LayoutParam 取值不同,设置不同的测量模式。主要调用了 getRootMeasureSpec() 方法,然后调用 performMeasure() 方法。

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.
        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;
}

可以看到当给 DecorView 设置的 LayoutParam 属性为 wrap_content 时,它的测量模式为 MeasureSpec.AT_MOST,其它的则为 MeasureSpec.EXACTLY,建议尺寸就是该方向上的屏幕宽高对应值。我们再看看 performMeasure() 方法。

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");

    try {

        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }

}

一切水落日出,mView 就是 DecorView。ViewRootImpl 调用了 DecorView 的 measure() 方法,启动了整个控件树的测量。

不过还可以细挖,那就是传递给 DecorView 的 LayoutParams 属性是什么,Activity 是全屏的,所以可以推断它是全屏的。但实际情况是不是这样呢?

DecorView 的 LayoutParams 实际上是 ViewRootImpl 中的 mWindowAttributes
ViewRootImpl.java

final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();

WindowManager.java

 public static class LayoutParams extends ViewGroup.LayoutParams
            implements Parcelable {

    public LayoutParams() {
        super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        type = TYPE_APPLICATION;
        format = PixelFormat.OPAQUE;
    }
}

mWindowAttributes 宽高测量模式都是 match_parent。所以 DecorView 最终测量的建议尺寸就是屏幕的宽高。

好了,回顾一下,从我们知道 DecorView 是 Activity 最外层的 View 开始,一直在寻找是谁在测绘它,调用它的 measure() 方法。千回百转,一层层追踪下去,都把 Activity、ActivityThread 牵扯出来了。最终找出了源头 ViewRootImpl。

总结

如果你一直顺序阅读到这个地方,我要对你说声感谢,谢谢你的捧场。

此文最终的目的不是为了让你困惑,如果因为篇幅过长,影响了你们的思维,我最后再做一点总结。

  1. View 的测量是从 measure() 方法开始到 onMeasure()。

  2. View 的 onMeasure() 中会接收到 parent 给出的测量规格建议。测量规格包含测量模式和建议尺寸。

  3. 测量模式包含 MeasureSpec.EXACTLY、MeasureSpec.AT_MOST、和 MeasureSpec.UNSPECIFIED。

  4. View 复写 onMeasure() 方法时,一般不处理 MeasureSpec.UNSPECIFIED 测量模式,对于 MeasureSpec.EXACTLY 模式,直接设定 parent 给出的测试尺寸。需要特别处理的是 MeasureSpec.AT_MOST,它需要的是自主决定尺寸数值,但是不得大于 parent 给出的建议尺寸。

  5. View 复写 onMeasure() 方法最后要通过 setMeasuredDimension() 设置尺寸,不然会报错异常。

  6. ViewGroup 自定义比 View 稍难,因为 onMeasure() 方法中要根据子元素来决定自己的宽高,所以要测量子元素的尺寸,通过 measureChildren() 或者根据 measureChild() 方法,然后通过 getMeasureWidth() / getMeasureHeight() 获取子元素期望的尺寸。

  7. Activity 有一棵控件树,DecorView 是它最顶层的 View,但 Activity 相关联的 ViewRootImpl 对象操纵着它们。

以上。

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    长谈:关于 View Measure 测量机制,让我一次把话说完