因爲以前在實習生面試的時候,被面試官問到有關自定義控件的問題,但沒有回答上來,因而回來後便學習了關於自定義控件的相關知識。android
自定義控件,按個人理解,大致上分爲兩種。一種是本身繪圖或者加入動畫,產生的單一的自定義控件。一種是利用已有的控件進行組合,產生的組合控件。這篇博文主要介紹第一種。git
在進行單一的自定義控件編寫時,主要須要重寫三個方法:onMeasure(),onLayout()和onDraw(),在介紹這三個方法以前,先來展現一下我本身設計的一個簡單的自定義控件,而後根據圖片,依次對這三個方法進行講解github
這個自定義控件是一個2048方塊的模型,對於一個2048方塊來講,咱們須要可以設置他的大小、方塊上的數字以及方塊的顏色。對於Android的自帶控件來講,咱們能夠經過XML文件來靜態設置這些屬性,那麼對於自定義控件來講,固然也能夠這樣作,接下來先來介紹自定義控件如何設置自定義屬性面試
在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>
在使用普通控件的屬性時,咱們的格式爲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,即爲聲明的自定義屬性的名稱佈局
屬性在存儲過程當中,實際上利用的是鍵值對存儲方式,所以,在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()方法的做用,即是根據用戶聲明的寬高,計算出相應的寬高,並把該數值傳遞給View。spa
在介紹具體的計算方法前,咱們先了解一下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的子控件),返回的模式值。
在看了前一段介紹後,你們可能會有疑問,既然系統已經根據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方法,傳輸相應的尺寸值。
固然,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方法,主要做用是設置該View在父佈局中的位置,好比你在該View控件中聲明瞭自定義屬性layout_gravity,那麼你便須要在onLayout中指定該控件的位置。因爲這個自定義控件中,未涉及相關屬性,故在這個很少介紹這個方法。
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的繪製方法,在網上能夠搜索到相關數據。
https://github.com/gtxjinx/Custom-View/
有須要的朋友能夠自行下載。