《Android 群英传》读书笔记: View 的测量

版权声明:本文为 like_program 原创文章,未经博主允许不得转载。 https://blog.csdn.net/like_program/article/details/53135415

转载请注明出处: http://blog.csdn.net/like_program/article/details/53135415

在自定义 View 时,我们经常要测量 View 的尺寸。Android 给我们提供了一个设计短小精悍却功能强大的类 – MeasureSpec 类,通过它来帮助我们测量 View。

MeasureSpec 是一个 32 位的 int 值,其中高 2 位为测量的模式,低 30 位为测量的大小。

测量模式

系统会根据我们写的布局文件中的 layout_width 属性或 layout_height 属性值,来判断应该使用哪种测量模式。测量的模式可以为以下三种。

  • EXACTLY

    即精确值模式,当我们将控件的 layout_width 属性或 layout_height 属性指定为具体数值,或者是 match_parent 时,指定的很明确,我就是要控件这么大,所以此时系统的测量模式是 EXACTLY (精确值模式)。

  • AT_MOST

    即最大值模式,当我们将控件的 layout_width 属性或 layout_height 属性指定为 wrap_content 时,指定的就不明确了,到底是多大呢,不知道,只知道有个上限:不超过父控件的大小即可。所以此时系统的测量模式是 AT_MOST (最大值模式)

  • UNSPECIFIED

    不指定其大小测量模式,这种我还没遇见过,所以暂时解释不了。根据网上查的资料,这种模式一般出现在 AadapterView 的 item 的 heightMode 中、ScrollView 的 childView 的heightMode中

    扫描二维码关注公众号,回复: 3800164 查看本文章

View 默认的 onMeasure() 方法只支持 EXACTLY 模式,所以如果在自定义控件的时候不重写 onMeasure() 方法的话,就只能使用 EXACTLY 模式,此时控件可以响应你指定的具体宽高值或者是 match_parent 属性。

而如果要让自定义 View 支持 wrap_content 属性,就必须重写 onMeasure() 方法来指定 wrap_content 时的大小。

下面来看一个简单的实例,演示如何进行 View 的测量。

打开 Android Studio,新建 MeasureTest 项目。

新建 CustomView.java ,继承自 View,代码如下:

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

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

首先,要重写 onMeasure() 方法,该方法如下所示:

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

在 IDE 中按住 Ctrl 键查看 super.onMeasure() 方法。可以发现,系统最终会调用 setMeasuredDimension(widthMeasureSpec, heightMeasureSpec) 方法将测量后的宽高值设置进去,从而完成测量工作。

onMeasure方法

所以在重写 onMeasure() 方法后,最终要做的工作就是把测量后的宽度值作为参数设置给 setMeasuredDimension() 方法。

通过上面的分析,重写的 onMeasure() 方法代码如下所示:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(measuredWidth(widthMeasureSpec),
            measuredHeight(heightMeasureSpec));
}

在 onMeasure() 方法中,我们调用自定义的 measuredWidth() 方法和 measuredHeight() 方法,分别对宽高进行重新定义,参数则是宽和高的 MeasureSpec 对象,MeasureSpec 对象中包含了测量的模式和测量值的大小。

下面就以 measuredWidth() 方法为例,讲解如何自定义测量值。

第一步,从 MeasureSpec 对象中提取出具体的测量模式和大小。代码如下:

// 获取测量模式
int specMode = MeasureSpec.getMode(measureSpec);
// 获取测量值
int specSize = MeasureSpec.getSize(measureSpec);

接下来,通过判断测量的模式,给出不同的测量值。

  • 如果控件的 layout_width 属性指定为具体数值,或者指定为 match_parent 属性时,此时 specMode 为 EXACTLY ,直接使用指定的 specMode 即可;

  • 当 specMode 为其他两种模式时,需要给它一个默认的大小;

  • 特别地,如果指定 wrap_content 属性,即 AT_MOST 模式,则需要取出我们指定的大小与 specSize 中最小的一个来作为最后的测量值(这一点有点不好懂,等下会详细说明)

measuredWidth() 方法的代码如下所示。这段代码基本上也可以作为模板代码。

private int measuredWidth(int measureSpec) {
    int result = 0;
    // 获取测量模式
    int specMode = MeasureSpec.getMode(measureSpec);
    // 获取测量值
    int specSize = MeasureSpec.getSize(measureSpec);

    if (specMode == MeasureSpec.EXACTLY) {
        result = specSize;
    } else {
        result = 200;

        if (specMode == MeasureSpec.AT_MOST) {
            result = Math.min(result, specSize);
        }
    }
    return result;
}

measureHeight() 方法与 measureWidth() 基本一致。通过这两个方法,我们就完成了对宽高值的自定义。

接着我们重写下 onDraw() 方法,代码如下:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawColor(Color.GRAY);
}

canvas.drawColor(Color.GRAY)这句代码的意思是把画布的颜色设置为灰色,如果你不懂画布是什么意思也没事,你可以简单的理解为:把 CustomView 控件的颜色设置为灰色。

接着我们在 CustomView 上点击右键,选择 Copy Reference

复制全限定类名

复制 CustomView 的全限定类名,粘贴到 activity_main.xml,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.example.measuretest.MainActivity">

    <com.example.measuretest.CustomView
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</RelativeLayout>

这里我们把 CustomView 控件的尺寸设置为 match_parent。

运行一下程序:

match_parent

我们可以看到,屏幕全部变成了灰色,这块灰色的区域就是我们刚刚自定义的 CustomView。再回想一下,我们刚刚定义 CustomView 的尺寸是 match_parent,所以此时系统的测量模式是 EXACTLY,所以会执行 measuredWidth() 的以下逻辑:

if (specMode == MeasureSpec.EXACTLY) {
    result = specSize;
}
return result;

此时 CustomView 控件的尺寸也就是父控件的尺寸,所以 CustomView 占满了全屏。

通过这个小例子,相信大家已经对 View 的测量有了一定的了解。

接下来,我们再看下刚才那个模板方法,书上对这个模板方法并没有详细解释,所以我们来仔细分析一下:

// 获取测量模式
int specMode = MeasureSpec.getMode(measureSpec);
// 获取测量值
int specSize = MeasureSpec.getSize(measureSpec);

之前已经说了,系统的测量模式是根据我们写的布局文件中的 layout_width 属性或 layout_height 属性值来判断的:

  • 如果属性值是一个具体数值,比如 100px,或者 match_parent,那么测量模式就是 EXACTLY,测量值就是 100px,或者是父控件尺寸;

  • 如果属性值是 wrap_content,那么测量模式就是 AT_MOST,但是测量值是多少呢?

嗯,这是个问题,我们只知道不能超过父控件允许的尺寸。那么到底是多少呢?答案是:父控件的尺寸大小。

为什么是父控件的尺寸大小呢?

因为此时系统只知道子控件尺寸不能超过父控件的尺寸,但是不知道子控件的具体尺寸是多少,那么子控件就会默认填充整个父布局。

我们用代码来证实一下:

修改下 CustomView.java 中的 measuredWidth() 和 measuredHeight() 方法,代码如下:

private int measuredWidth(int measureSpec) {
    int result = 0;
    // 获取测量模式
    int specMode = MeasureSpec.getMode(measureSpec);
    // 获取测量值
    int specSize = MeasureSpec.getSize(measureSpec);

    if (specMode == MeasureSpec.AT_MOST) {
        result = specSize;
    } 
    return result;
}
private int measuredHeight(int measureSpec) {
    int result = 0;
    // 获取测量模式
    int specMode = MeasureSpec.getMode(measureSpec);
    // 获取测量值
    int specSize = MeasureSpec.getSize(measureSpec);

    if (specMode == MeasureSpec.AT_MOST) {
        result = specSize;
    } 
    return result;
}

修改 activity.main.xml,修改下 CustomView 的尺寸,修改为 wrap_content:

<com.example.measuretest.CustomView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

运行下程序:

wrap_content

我们可以看到,屏幕依然全部变成了灰色,所以 CustomView 的尺寸修改为 wrap_content 后,测量值就是父控件的宽度和高度。

为了进一步证实我们的想法,我们再修改下父控件的尺寸,看 CustomView 的尺寸会不会有变化。

修改 activity_main.xml,修改父控件的尺寸为 500px,给 CustomView 增加一个 id,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="500px"
    android:layout_height="500px"
    tools:context="com.example.measuretest.MainActivity">

    <com.example.measuretest.CustomView
        android:id="@+id/custom_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</RelativeLayout>

我们再修改下 MainActivity.java 代码,以打印下 CustomView 控件的尺寸。MainActivity.java 代码如下:

package com.example.measuretest;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.ViewTreeObserver;

public class MainActivity extends AppCompatActivity {

    public static final String TAG = "MainActivity";

    private CustomView customView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        customView = (CustomView) findViewById(R.id.custom_view);

        // 获取控件树,对 onLayout 结束事件进行监听
        customView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver
                .OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                // OnGlobalLayoutListener 可能会被多次触发,因此在得到了高度之后,要将 OnGlobalLayoutListener 注销掉
                customView.getViewTreeObserver()
                        .removeGlobalOnLayoutListener(this);
                Log.d(TAG, "宽度是:" + customView.getWidth());
                Log.d(TAG, "高度是:" + customView.getHeight());
            }
        });
    }
}
customView.getViewTreeObserver().addOnGlobalLayoutListener()

这段代码可能有的同学会看不懂,如果看不懂的话可以看下我的这篇博客,里面对这段代码作了很详细的解释:

Android 实现闪屏页+功能引导页

运行下程序:

500运行图

我们可以看到,这次屏幕只有部分是灰色,再看下日志:

打印500

CustomView 的宽度和高度都是 500,所以我们的推论是正确的。

接下来再看下这段代码,

result = 200;

if (specMode == MeasureSpec.AT_MOST) {
    // 最小的一个来作为最后的测量值
    result = Math.min(result, specSize);
}

刚刚我们分析了,测量模式为 AT_MOST 时,specSize 为父控件的尺寸,知道了这一点,这段代码也就好理解了:

result 是我们想让子控件显示的尺寸。测量模式为 AT_MOST 时,specSize 就是父控件的尺寸,

  • 要是 specSize 大,说明子控件尺寸比父控件小,子控件可以放进父控件,所以我们前面给 result 赋值 200,也就可以使用。

  • 要是 result 大,说明父控件尺寸比我们想让子控件显示的尺寸(200)小,子控件当然放不进父控件了,所以只好委屈委屈,尺寸和父控件一样大。

源码下载

猜你喜欢

转载自blog.csdn.net/like_program/article/details/53135415