Android自定义View(一)

对于很多Android入门程序猿来说自定义View,都是比较恐惧的,但是这又是高手进阶的必经之路。先总结下自定义View的步骤:
1、自定义View的属性
2、在View的构造方法中获得我们自定义的属性
[ 3、重写onMesure ]
4、重写onDraw
我把3用[]标出了,所以说3不一定是必须的,当然了大部分情况下还是需要重写的。
其中第1,第2点在前面的文章已经有详细的介绍Android自定义属性,不了解的童鞋可以去看看参考下,本文着重介绍第3和第4点。

onMeasure()
onMeasure()方法顾名思义就是用于测量视图的大小的。它接收两个参数,widthMeasureSpec和heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。
MeasureSpec的值由specSize和specMode共同组成的,其中specSize记录的是大小,specMode记录的是规格。specMode一共有三种类型,如下所示:

  • EXACTLY

表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。(一般是在布局中设置了明确的值或者是MATCH_PARENT)

  • AT_MOST

表示子视图最多只能是specSize中指定的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。系统默认会按照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。(一般为WARP_CONTENT)

  • UNSPECIFIED

表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到,一般都是父控件是AdapterView,通过measure方法传入的模式。
MeasureSpec是一个int型数字,可能有很多人想不通,一个int型整数怎么可以表示两个东西(大小模式和大小的值),一个int类型我们知道有32位。而模式有三种,要表示三种状 态,至少得2位二进制位。于是系统采用了最高的2位表示模式。如图:
Android自定义View(一)_第1张图片
最高两位是00–MeasureSpec.UNSPECIFIED
最高两位是01–MeasureSpec.EXACTLY
最高两位是11–MeasureSpec.AT_MOST
很多人一遇到位操作头就大了,为了操作简便,于是系统给我提供了一个MeasureSpec工具类。
这个工具类有四个方法和三个常量(上面所示)供我们使用:

//这个是由我们给出的尺寸大小和模式生成一个包含这两个信息的int变量,这里这个模式这个参数,传三个常量中的一个。
public static int makeMeasureSpec(int size, int mode)
//这个是得到这个变量中表示的模式信息,将得到的值与三个常量进行比较。
public static int getMode(int measureSpec) 
//这个是得到这个变量中表示的尺寸大小的值。
public static int getSize(int measureSpec) 
//把这个变量里面的模式和大小组成字符串返回来,方便打日志
 public static String toString(int measureSpec)

好了,说完MeasureSpec,我们继续来看onMeasure方法,形参中widthMeasureSpec和heightMeasureSpec这两个值又是从哪里得到的呢?通常情况下,这两个值都是由父视图经过计算后传递给子视图的,说明父视图会在一定程度上决定子视图的大小。
当然,onMeasure()方法是可以重写的,也就是说,如果你不想使用系统默认的测量方式,可以按照自己的意愿进行定制,比如:

public class MyView extends View {  

    ......  

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

这样的话就把View默认的测量流程覆盖掉了,不管在布局文件中定义MyView这个视图的大小是多少,最终在界面上显示的大小都将会是200*200。
需要注意的是,在setMeasuredDimension()方法调用之后,我们才能使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0(注意区别于而getWidth()和getHeight(),这两个方法要在ViewGroup的onLayout()过程结束后才能获取到)。
由此可见,视图大小的控制是由父视图、布局文件、以及视图本身共同完成的,父视图会提供给子视图参考的大小,而开发人员可以在XML文件中指定视图的大小,然后视图本身会对最终的大小进行拍板。

onDraw()
measure和layout的过程都结束后,接下来就进入到draw的过程了。同样,根据名字你就能够判断出,在这里才真正地开始对视图进行绘制。
这时就会用到Canvas、Paint画图了,具体见前面一篇博客Android的Paint、Canvas和Matrix讲解

好了,原理性东西我们讲过了,下面通过几个简单的例子来验证一下,同样按上面四步写法:

  • 1

自定义View的属性,首先在res/values/ 下建立一个attrs.xml , 在里面定义我们的属性和声明我们的整个样式。

<?xml version="1.0" encoding="utf-8"?>  
<resources>     
    <declare-styleable name="CustomTextView">  
        <attr name="titleText" format="string" />  
        <attr name="titleTextColor" format="color" />  
        <attr name="titleTextSize" format="dimension" />  
    </declare-styleable>   
</resources>  

然后在布局中声明我们的自定义View,一定要引入我们的命名空间xmlns:custom=”http://schemas.android.com/apk/res/com.hx.test”

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:custom="http://schemas.android.com/apk/res/com.hx.test"  
    android:layout_width="match_parent"  
    android:layout_height="match_parent" >  

    <com.hx.test.CustomTextView  
        android:layout_width="200dp"  
        android:layout_height="100dp"  
        custom:titleText="CustomTextView"  
        custom:titleTextColor="#ff0000"  
        custom:titleTextSize="20sp" />    
</RelativeLayout>  
  • 2

在View的构造方法中,获得我们的自定义的属性,我们重写了3个构造方法,默认的布局文件调用的是两个参数的构造方法,所以记得让所有的构造调用我们的三个参数的构造,我们在三个参数的构造中获得自定义属性。

    public CustomTextView(Context context, AttributeSet attrs)  
    {  
        this(context, attrs, 0);  
    }  

    public CustomTextView(Context context)  
    {  
        this(context, null);  
    }  

    /** * 获得我自定义的样式属性 * * @param context * @param attrs * @param defStyle */  
    public CustomTextView(Context context, AttributeSet attrs, int defStyle)  
    {  
        super(context, attrs, defStyle);  
        /** * 获得我们所定义的自定义样式属性 */  
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomTextView, defStyle, 0);  
        int n = a.getIndexCount();  
        for (int i = 0; i < n; i++)  
        {  
            int attr = a.getIndex(i);  
            switch (attr)  
            {  
            case R.styleable.CustomTextView_titleText:  
                mTitleText = a.getString(attr);  
                break;  
            case R.styleable.CustomTextView_titleTextColor:  
                mTitleTextColor = a.getColor(attr, Color.BLACK);  
                break;  
            case R.styleable.CustomTextView_titleTextSize:  
                // 默认设置为16sp,TypeValue也可以把sp转化为px 
                mTitleTextSize = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(  
                        TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));  
                break;  

            }  

        }  
        a.recycle();  

        /** * 获得绘制文本的宽和高 */  
        mPaint = new Paint();  
        mPaint.setTextSize(mTitleTextSize);  
        mPaint.setColor(mTitleTextColor);  
        mBound = new Rect();  
        mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);  
    }  
  • 3

我们重写onDraw,onMesure调用系统提供的:

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

    @Override
    protected void onDraw(Canvas canvas) {
        mPaint.setColor(Color.GREEN);
        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
        mPaint.setColor(mTitleTextColor);
        canvas.drawText(mTitleText, getWidth() / 2 - mBound.width() / 2,
                getHeight() / 2 + mBound.height() / 2, mPaint);
    }

运行效果图:

这已经基本已经实现了自定义View。但是此时如果我们把布局文件的宽和高写成wrap_content,会发现效果并不是我们的预期:
Android自定义View(一)_第2张图片
这是因为MATCH_PARENT 时系统返回的specMode的值为AT_MOST,而specSize为全屏,子视图默认占据了specSize中指定大小的范围。这时需要我们重写onDraw来实现自己的逻辑。

    @Override  
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)  
    {  
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);  
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);  
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);  
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);  
        int width;  
        int height ;  
        if (widthMode == MeasureSpec.EXACTLY) {  
            width = widthSize;  
        } else {
            mPaint.setTextSize(mTitleTextSize);
            mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);   
            float textWidth = mBound.width();  
            int desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());  
            width = desired;  
        }       
        if (heightMode == MeasureSpec.EXACTLY) {  
            height = heightSize;  
        } else {
            mPaint.setTextSize(mTitleTextSize);
            mPaint.getTextBounds(mTitleText, 0, mTitleText.length(), mBound);  
            float textHeight = mBound.height();  
            int desired = (int) (getPaddingTop() + textHeight + getPaddingBottom());  
            height = desired;  
        }  
        setMeasuredDimension(width, height);  
    } 

我们再来修改下布局文件

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:custom="http://schemas.android.com/apk/res/com.hx.test"  
    android:layout_width="match_parent"  
    android:layout_height="match_parent" >  

    <com.hx.test.CustomTextView  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        custom:titleText="CustomTextView"  
        android:padding="10dp"  
        custom:titleTextColor="#ff0000"  
        android:layout_centerInParent="true"  
        custom:titleTextSize="20sp" />    
</RelativeLayout>

效果如下:
Android自定义View(一)_第3张图片

完全复合我们的预期,现在我们可以对高度、宽度进行随便的设置了,基本可以满足我们的需求。我们还可以在构造函数中对它的点击事件做一些处理:

this.setOnClickListener(new OnClickListener() {   
      @Override  
      public void onClick(View v) {  
            mTitleText = randomText();  
            postInvalidate();
            //invalidate(); 
      } 
});  
    private String randomText() {  
        Random random = new Random();  
        int randomInt = random.nextInt(5);
        return Str[randomInt];
    }

其中

private String[] Str = {"Text0", "Text1", "Text2", "Text3", "Text4"};

Android自定义View(一)_第4张图片
可以看到点击事件已经生效了,每次当我们点击自定义View时显示的文字内容都发生变化。调用postInvalidate()就是让View重新执行onDraw,其实这里也可以使用invalidate(),都能生效。他们区别在于:
invalidate()得在UI线程中被调动,在工作者线程中可以通过Handler来通知UI线程进行界面更新。而postInvalidate()可以在工作者线程中被调用。

但是还有一个问题,我们看到点击后自定义View的文字变短了,但自定义View的宽却没有相应变窄,这不符合我们写的WRAP_CONTENT的思想。其实这是由于postInvalidate()只是强制View重新走onDraw,而View的宽高是在onMesure中定义的,所以文字改变后需要强制自定义View也要走onMesure,重新测量,继续修改代码:

this.setOnClickListener(new OnClickListener() {   
        @Override  
        public void onClick(View v){  
            mTitleText = randomText();
            requestLayout();  
         } 
  }); 

Android自定义View(一)_第5张图片
完美实现我们的构想。总结一下RequestLayout用法:
RequestLayout:
当view确定自身已经不再适合现有的区域时,该view本身调用这个方法要求parent view重新调用他的onMeasure onLayout来对重新设置自己位置。
特别的当view的layoutparameter发生改变,并且它的值还没能应用到view上,这时候适合调用这个方法。也就是当通过getLayoutParrms().width = XXX的时候,我们需要重新调用RequestLayout。
invalidate:
View类调用迫使view重画(onDraw)。

Demo下载地址

你可能感兴趣的:(Android自定义View(一))