美文网首页androidAndroid自定义View
[Android] View 的测量及绘制

[Android] View 的测量及绘制

作者: 希灵丶 | 来源:发表于2016-01-28 16:32 被阅读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

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

    相关文章

      网友评论

        本文标题:[Android] View 的测量及绘制

        本文链接:https://www.haomeiwen.com/subject/qjadkttx.html