Android学习——自定义控件(一)

因为以前在实习生面试的时候,被面试官问到有关自定义控件的问题,但没有回答上来,因而回来后便学习了关于自定义控件的相关知识。android

 

自定义控件介绍


自定义控件,按个人理解,大致上分为两种。一种是本身绘图或者加入动画,产生的单一的自定义控件。一种是利用已有的控件进行组合,产生的组合控件。这篇博文主要介绍第一种。git

在进行单一的自定义控件编写时,主要须要重写三个方法:onMeasure(),onLayout()和onDraw(),在介绍这三个方法以前,先来展现一下我本身设计的一个简单的自定义控件,而后根据图片,依次对这三个方法进行讲解github

image

这个自定义控件是一个2048方块的模型,对于一个2048方块来讲,咱们须要可以设置他的大小、方块上的数字以及方块的颜色。对于Android的自带控件来讲,咱们能够经过XML文件来静态设置这些属性,那么对于自定义控件来讲,固然也能够这样作,接下来先来介绍自定义控件如何设置自定义属性面试

 

自定义属性


1.建立自定义属性

在values文件中,建立attrs.xml(若是有多个自定义控件,能够建立多个XML文件来定义自定义属性),在XML中按以下格式,声明自定义属性:canvas

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="Code"><!--自定义控件的类名-->
        <attr name="size" format="dimension"/> <!--属性名以及属性的类型-->
        <attr name="text" format="string"/>
        <attr name="codecolor" format="color"/>
        <attr name="textcolor" format="color"/>
        <attr name="textsize" format="dimension"/>
        <attr name="gravity">
            <flag name="left" value="0"/>
            <flag name="top" value="1"/>
            <flag name="right" value="2"/>
            <flag name="bottom" value="3"/>
            <flag name="center" value="4"/>
        </attr>
    </declare-styleable>
</resources>

 

2.使用自定义属性

在使用普通控件的属性时,咱们的格式为android:*****=*****,在自定义控件中,咱们须要人为地定义一个名称,来表示咱们的自定义属性,在XML文件的头部进行声明,代码以下:less

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:code="http://schemas.android.com/apk/res-auto"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
                android:gravity="center"
    tools:context="com.example.administrator.service.MainActivity">

    <com.example.administrator.service.Code
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        code:gravity="center"
        code:text="2048"
        code:textsize="20sp"
        code:size="100dp"
        code:codecolor="@color/colorPrimaryDark"
        />
</RelativeLayout>

其中RelativeLayout下的xmlns:code=http://schemas.android.com/apk/res-auto,即为声明的自定义属性的名称布局

 

3.在Java文件中获取自定义属性

属性在存储过程当中,实际上利用的是键值对存储方式,所以,在Java文件中获取相关属性时,只须要根据声明的属性名称做为key值,获取对应的values值便可。其中,用户获取键值对的类为TypedArray,代码以下:学习

private void initParams(Context context, AttributeSet attrs) {
        TypedArray typedArray =context.obtainStyledAttributes(attrs,R.styleable.Code);
        if(typedArray!=null)
        {
            codecolor=typedArray.getColor(R.styleable.Code_codecolor,Color.YELLOW);
            text=typedArray.getString(R.styleable.Code_text);
            textsize=typedArray.getDimension(R.styleable.Code_textsize,20);
            length=typedArray.getDimension(R.styleable.Code_size,100);
            textcolor=typedArray.getColor(R.styleable.Code_textcolor,Color.GRAY);
            position=typedArray.getInt(R.styleable.Code_gravity,LEFT);

            typedArray.recycle();
        }
    }

在获取的属性中,有些须要设置默认值,有些则不须要,这点须要注意。其次,在使用完TypedArray后,不要忘记回收。动画

 

onMeasure()


接下来开始介绍文章开头提到的三个方法,先来介绍onMeasure()方法。咱们知道,在声明一个控件时,咱们须要声明该控件的宽、高,而onMeasure()方法的做用,即是根据用户声明的宽高,计算出相应的宽高,并把该数值传递给View。spa

 

1.MeasureSpec类

 

 

 

 

在介绍具体的计算方法前,咱们先了解一下MeasureSpec类,找到其源代码,提取出咱们须要的部分:

public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;


        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;

        public static int makeMeasureSpec(int size,
                                          int mode) {
                return size + mode;

        }



        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }

在这个代码中,咱们能够看到,一个View的尺寸值是一个32位的二进制数,并由两部分组成。其中,高两位,表示的是尺寸值的模式,分为三种:EXACTLY、UNSPECIFIED和AT_MOST,而低30位,则表示这个View的大小。因此,当咱们获取一个View的尺寸值时,即可以利用MeasureSpec类的getMode方法和getSize方法,获取对应的模式和大小。那么这三种模式又分别表明了什么呢,继续查看以下源码:

privatestaticintgetRootMeasureSpec(intwindowSize,introotDimension){
    int measureSpec;
    switch (rootDimension) {

    case ViewGroup.LayoutParams.MATCH_PARENT:
        // Window can't resize. Force root view to be windowSize.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        // Window can resize. Set max size for root view.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        // Window wants to be an exact size. Force root view to be that size.
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

由这段代码能够看到,当咱们定义的宽高为MATCH_PARENT时,模式为EXACTLY,即根据父布局剩余的可填充大小,返回一个准确值。当宽高为WRAP_CONTENT时,模式为AT_MOST,即最大限度地填充父布局。而当为其余值时,如100dp,则为EXACTLY,为用户自定义的准确值。而UNSPECIFIED,则是当该控件大小不肯定时(如在ScrollView的子控件),返回的模式值。

 

2.onMeasure方法的编写

在看了前一段介绍后,你们可能会有疑问,既然系统已经根据XML中定义的属性,给出了相应的尺寸值,那么onMeasure又有什么做用呢。其实是这样,因为自定义控件使咱们本身定义的控件,所以咱们想让他多大就多大,有时候系统给出的尺寸值,并非咱们实际想要的尺寸值。如当宽高为WRAP_CONTENT时,系统让咱们的模式为AT_MOST,即彻底填充父布局,但显然,咱们并不必定但愿是这样。甚至,咱们有时候会但愿,不管用户定义的宽高为多少,咱们都但愿咱们的View只会显示出一种给定好的宽高。这个时候,onMeasure的做用就显示出来了。换言之,系统给出的尺寸值,只是系统推荐的值,而咱们真正但愿这个View的尺寸值为多少,则是咱们在onMeasure方法中,本身实现的。下面,以个人这个自定义控件举例。代码以下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    {
        int widthMode=MeasureSpec.getMode(widthMeasureSpec);
        int heightMode=MeasureSpec.getMode(heightMeasureSpec);
        //////////设定控件的长宽
        switch (widthMode)
        {
            case MeasureSpec.EXACTLY:
                break;
            case MeasureSpec.UNSPECIFIED:
                widthMeasureSpec=MeasureSpec.makeMeasureSpec((int)length,MeasureSpec.EXACTLY);
                break;
            case MeasureSpec.AT_MOST:
                widthMeasureSpec=MeasureSpec.makeMeasureSpec((int)length,MeasureSpec.EXACTLY);
                break;
        }
        switch (heightMode)
        {
            case MeasureSpec.EXACTLY:
                break;
            case MeasureSpec.UNSPECIFIED:
                heightMeasureSpec=MeasureSpec.makeMeasureSpec((int)length,MeasureSpec.EXACTLY);
                break;
            case MeasureSpec.AT_MOST:
                heightMeasureSpec=MeasureSpec.makeMeasureSpec((int)length,MeasureSpec.EXACTLY);
                break;
        }


        super.onMeasure(widthMeasureSpec, heightMeasureSpec);   //传输长宽数据

这段代码很容易读懂,咱们首先利用MeasureSpec的getMode方法,获取到相应的模式值。我但愿,当宽高为WRAP_CONTENT时,View的大小和个人方块大小同样,而其余时刻,则和用户定义的宽高相同,所以,即可以在CASE语句中,对尺寸值进行修改,并用makeMeasureSpec从新生成32位数字,最后,经过super.onMeasure方法,传输相应的尺寸值。

 

3.在onMeasure进行相应的初始化操做

固然,onMeasure方法,除了能够传输尺寸值之外,还能够进行相应的初始化操做,如在个人控件中,有gravity属性,那么在设置方块中心的位置时,便天然须要用到整个控件的宽高属性,这个操做天然也就须要在onMeasure中实现,具体代码以下:

int width=MeasureSpec.getSize(widthMeasureSpec);
        int height=MeasureSpec.getSize(heightMeasureSpec);
        X=width/2;
        Y=height/2;
        Log.i("X1",X+"");
        switch (position)
        {
            case LEFT:
                X=length/2+getPaddingLeft();
                Log.i("X2",X+"");
                break;
            case RIGHT:
                X=width-getPaddingLeft()-length/2;
                break;
            case TOP:
                Y=length/2+getPaddingTop();
                break;
            case BOTTOM:
                Y=height-getPaddingBottom()-length/2;
                break;
            case CENTER:
                break;
        }
        float left=X-length/2;
        float right=X+length/2;
        float top=Y-length/2;
        float bottom=Y+length/2;
        rectf.set(left,top,right,bottom);   //获取绘图区域
    }

 

onLayout()


onlayout方法,主要做用是设置该View在父布局中的位置,好比你在该View控件中声明了自定义属性layout_gravity,那么你便须要在onLayout中指定该控件的位置。因为这个自定义控件中,未涉及相关属性,故在这个很少介绍这个方法。

 

onDraw()


onDraw方法,主要是用Canvas和Paint类,来绘制相关的View图像。其中,Paint类至关因而画笔,当你每次进行绘制前,须要先对画笔进行修改和描述。而Canvas类,至关于你对画笔进行的动做,如画圆、画矩形、书写文字等等。而这相配合,即可以绘制出想要的图形。具体代码以下:

protected void onDraw(Canvas canvas)
    {
        super.onDraw(canvas);
        mPaint.setColor(codecolor);
        canvas.drawRoundRect(rectf,length/10,length/10,mPaint);
        mPaint.setColor(textcolor);
        mPaint.setTextSize(textsize);
        Log.d("Jinx",text);
        canvas.drawText(text,X-length/5*2,Y-length/2+textsize,mPaint);
    }

相关的Canvas的绘制方法,在网上能够搜索到相关数据。

 

GitHub地址


https://github.com/gtxjinx/Custom-View/ 

有须要的朋友能够自行下载。

相关文章
相关标签/搜索