androidAndroid自定义View

[Android] View 的测量及绘制

2016-01-28  本文已影响421人  希灵丶

View 的显示过程


一个 View 经过三步重点流程,最终才能显示到屏幕上。分别是:测量,布局,绘制。

其实很容易理解,一个图形要想显示在界面上,首先要进行测量决定大小。然后要进行布局,决定摆放的位置。最后就是进行绘制,用线条和图形描述出来。

如果要进行自定义 View 的学习,那么了解这些流程是必须的。

在 Android 中,View 同样要经过以上三个步骤,只是其中的布局通常是由父布局,一个 ViewGroup 来决定,我们主要先来了解一下测量以及绘制的过程。

测量


MeasureSpec

View 的测量是在 onMeasure() 方法中进行。其中 Android 设计了一个类用来进行测量 ---- MeasureSpec 类,这个类的值是一个 32 位的 int 值,高 2 位表示测量的模式,低 30 位表示测量的大小。

MeasureSpec 的三种模式

其中测量的模式分为以下三种:

  1. EXACTLY
    精确值模式,在手动指定了控件的 layout_width 或 layout_heigth 属性为具体数值的时候,或者指定为 match_parent 时,系统使用的是 EXACTLY 模式。

  2. AT_MOST
    最大值模式,在手动指定了控件的 layout_width 或 layout_heigth 属性为 wrap_content 的时候,控件大小一般随着控件的子控件或内容变化而变化,只要不超过父控件允许的最大尺寸即可。

  3. 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

这只是简单的一个例子,一般来说一个控件的绘制过程是相当复杂的,这个就要根据自己的情况来自定义了。

上一篇下一篇

猜你喜欢

热点阅读