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

Posted frank909

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了长谈:关于 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 一样

以上是关于长谈:关于 View Measure 测量机制,让我一次把话说完的主要内容,如果未能解决你的问题,请参考以下文章

Android View框架的measure机制

View的layout机制

Android View的绘制流程三部曲 —— Measure

反思|Android View机制设计与实现:测量流程

React Native学习-measure测量view的宽高值

Android View 测量流程(Measure)完全解析