转载请注明出处: 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中
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() 方法后,最终要做的工作就是把测量后的宽度值作为参数设置给 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。
运行一下程序:
我们可以看到,屏幕全部变成了灰色,这块灰色的区域就是我们刚刚自定义的 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"/>
运行下程序:
我们可以看到,屏幕依然全部变成了灰色,所以 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()
这段代码可能有的同学会看不懂,如果看不懂的话可以看下我的这篇博客,里面对这段代码作了很详细的解释:
运行下程序:
我们可以看到,这次屏幕只有部分是灰色,再看下日志:
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)小,子控件当然放不进父控件了,所以只好委屈委屈,尺寸和父控件一样大。