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/ 

有須要的朋友能夠自行下載。

相關文章
相關標籤/搜索