[Android] View 的测量及绘制
View 的显示过程
一个 View 经过三步重点流程,最终才能显示到屏幕上。分别是:测量,布局,绘制。
其实很容易理解,一个图形要想显示在界面上,首先要进行测量决定大小。然后要进行布局,决定摆放的位置。最后就是进行绘制,用线条和图形描述出来。
如果要进行自定义 View 的学习,那么了解这些流程是必须的。
在 Android 中,View 同样要经过以上三个步骤,只是其中的布局通常是由父布局,一个 ViewGroup 来决定,我们主要先来了解一下测量以及绘制的过程。
测量
MeasureSpec
View 的测量是在 onMeasure() 方法中进行。其中 Android 设计了一个类用来进行测量 ---- MeasureSpec 类,这个类的值是一个 32 位的 int 值,高 2 位表示测量的模式,低 30 位表示测量的大小。
MeasureSpec 的三种模式
其中测量的模式分为以下三种:
-
EXACTLY
精确值模式,在手动指定了控件的 layout_width 或 layout_heigth 属性为具体数值的时候,或者指定为 match_parent 时,系统使用的是 EXACTLY 模式。 -
AT_MOST
最大值模式,在手动指定了控件的 layout_width 或 layout_heigth 属性为 wrap_content 的时候,控件大小一般随着控件的子控件或内容变化而变化,只要不超过父控件允许的最大尺寸即可。 -
UNSPECIFIED
表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。
View 类模式的 onMeasure() 方法只支持 EXACTLY 模式,所以如果自定义控件的时候不重写 onMeasure() 方法的话,就只能使用 EXACTLY 模式。控件可以响应你指定的具体宽高值或者是 match_parent 属性。但是如果需要 View 支持 wrap_content 属性,就必须重写 onMeasure() 方法来指定 wrap_content 模式时的大小。
MeasureSpec 是怎么来的
关于这个 MeasureSpec 是由父布局传递给子布局的布局要求,我通过代码调试得到一些信息,我们来看一下。
在一个 1080 x 1920 的手机上,最外层使用一个 LinearLayout , width 和 height 都使用 match_parent,然后包裹了一个自定义的 View 。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.shire.myapplication.MyView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#f2f2" />
</LinearLayout>
此时在 MyView 的 onMeasure() 中我获取了 widthMeasureSpec 和 heightMeasureSpec ,提取的结果为:
widthMeasureSpec 的 size 为:1080
heightMeasureSpec 的 size 为:1557
这里的 1080 就是顶层 LinearLayout 充满屏幕的宽度,而 1557 就是顶层的 LinearLayout 除去状态栏高度之后充满屏幕的高度,由此可以得到 MeasureSpec 是传过来的是父布局的大小。
但是!如果你对 View 进行了自定义的大小,传过来的就是你定义的大小。比如上面的 MyView 的 layout_width 更改为 100dp ,那么获取到的结果就是:
widthMeasureSpec 的 size 为:300
heightMeasureSpec 的 size 为:1557
至于为什么 100dp 变成 300 这是 dp 转 px 的一个过程导致的,详细的可以看我另一篇文章: Android开发中dip,dpi,density,px等详解
分析 onMeasure
通过 MeasureSpec 我们可以获得测量模式以及大小,我们来看看部分源码是如何进行测量的。
我们先看 View 中的 onMeasure() 方法:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
在这里通过 getDefaultSize() 来从 MeasureSpec 中获取相应的大小以及模式,最后转换为一个 int 类型给 setMeasuredDimension() 作为参数进行最后测量的结果,我们看看这个 getDefaultSize()。
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
可以看到,首先通过 MeasureSpec.getMode() 和 MeasureSpec.getSize() 取出模式和大小,然后判断模式。
可以看到在 AT_MOST 或 EXACTLY 模式下都是同样的处理方式,这也说明了上面所说的,View 在默认情况下只支持 EXACTLY 模式,但是如果需要 View 支持 wrap_content 属性,也就是 AT_MOST,就必须重写 onMeasure() 方法来指定 AT_MOST 模式时的大小。
重写 onMeasure
下面我们自定义一个 View 来试试吧,我们先新建一个空的自定义 View 。
package com.shire.myapplication;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
public class MyView extends View {
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
我们并没有修改任何东西。接下来看看 XML 文件,我们添加了一个 myView 并设置了宽高为 wrap_content 背景是绿色便于观察控件大小,那么在这个情况下的显示效果会是怎么样?
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.shire.myapplication.MyView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#f2f2" />
</LinearLayout>
QQ截图20160128142829.png
如图,虽然我们设置了 wrap_content 属性,当时控件依然充满了父控件,这就是我们上面说的,View 在默认的情况下,是不支持 wrap_content 的,而且在不设置指定的宽高的情况下会把父控件的宽高传过来,所以必须要重写 onMeasure() 方法。
接下来看看应该如何重写 onMeasure() 方法 。根据源码的方案,是由 getDefaultSize() 方法来进行测量,然后将结果给 setMeasuredDimension() 所以我们主要就是自定义一个 “getDefaultSize()” 方法。我们看下具体的代码。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getSize(widthMeasureSpec),getSize(heightMeasureSpec));
}
我们重写了 onMeasure() 方法,并自定义了一个测量方法 getSize() ,接下来就是看看 getSize() 中是如何写的。
private int getSize(int MeasureSpec) {
//初始化一个返回值变量
int result;
//获得测量模式
int specMode = View.MeasureSpec.getMode(MeasureSpec);
//获得测量大小
int specSize = View.MeasureSpec.getSize(MeasureSpec);
//判断模式是否是 EXACTLY
if (specMode == View.MeasureSpec.EXACTLY) {
//如果模式是 EXACTLY 则直接使用specSize的测量大小
result = specSize;
}else{
//如果是其他两个模式,先设置一个默认大小值 200
result = 200;
//如果是 AT_MOST 也就是 wrap_content 的话,就取默认值 200 和 specSize 中小的一个为准。
if (specMode == View.MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
在上面的代码中,我们对 AT_MOST 模式时的测量方式进行了处理,接下来看看效果如何。还是同样的 XML 文件。
QQ截图20160128150118.png可以看到,现在 AT_MOST 模式已经生效了,当我们在设置 wrap_content 的时候不会再填充父布局,而是根据我们自定义的测量代码进行测量,用了 200 这个默认值。
至此,对 View 的测量算是简单的讲解完成了,其实总结一句话,如果的你自定义 View 不需要使用 wrap_content,就不用管 onMeasure() 方法。不然的话,就需要重写。
绘制
View 的绘制过程是在 onDraw() 方法中,如果你去看源代码,会发现这个方法是空的,但是子类可以继承。想来也正常,每个控件都有自己的表现方式,绘制方法,自然要自己来写这部分绘制的代码。接下来我们继续使用上面的 MyView 自定义绘制部分的代码。
在 onDraw() 方法中,传进来了一个 Canvas 对象,这个对象等于一块画布,我们可以在上面作画,那现在有了画布,我们还需要一支笔,那就是 Paint 对象。
public class MyView extends View {
//创建一个画笔对象
private Paint mPaint;
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
//初始化画笔对象
mPaint = new Paint();
}
@Override
protected void onDraw(Canvas canvas) {
//设置画笔的颜色为蓝色
mPaint.setColor(Color.BLUE);
//使用画笔画一个矩形
canvas.drawRect(0,0,50,50,mPaint);
//设置画笔的颜色的黄色
mPaint.setColor(Color.YELLOW);
//设置画笔的字体大小为40
mPaint.setTextSize(40);
//使用画笔写出一行字
canvas.drawText("我可是用笔写的", 0, 80, mPaint);
}
}
最后的效果
QQ截图20160128161413.png这只是简单的一个例子,一般来说一个控件的绘制过程是相当复杂的,这个就要根据自己的情况来自定义了。