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

本文详细探讨了Android中View的测量机制,从自定义View的测量到ViewGroup的测量协议,深入剖析MeasureSpec的三种模式。通过实验展示了View尺寸设置的方法,并揭示了Activity中最顶层的DecorView的测量过程。文章强调了测量阶段的重要性,旨在帮助开发者理解Android界面布局的底层原理。
摘要由CSDN通过智能技术生成

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

首先声明,这一篇篇幅很长很长很长的文章。目的就是为了把 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&
评论 31
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

frank909

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值