本篇文章是自己自學自定義view前的准備,具體參考資料來自
Android LayoutInflater原理分析,帶你一步步深入了解View(一)
Android視圖繪制流程完全解析,帶你一步步深入了解View(二)
Android視圖狀態及重繪流程分析,帶你一步步深入了解View(三)
Android自定義View的實現方法,帶你一步步深入了解View(四)
這位大哥的系列博文,相當於自己看這些的一個思考吧。
一、首先學layoutInflater。
相信接觸Android久一點的朋友對於LayoutInflater一定不會陌生,都會知道它主要是用於加載布局的。而剛接觸Android的朋友可能對LayoutInflater不怎么熟悉,因為加載布局的任務通常都是在Activity中調用setContentView()方法來完成的。其實setContentView()方法的內部也是使用LayoutInflater來加載布局的,只不過這部分源碼是internal的,不太容易查看到。那么今天我們就來把LayoutInflater的工作流程仔細地剖析一遍,也許還能解決掉某些困擾你心頭多年的疑惑。
先來看一下LayoutInflater的基本用法吧,它的用法非常簡單,首先需要獲取到LayoutInflater的實例,有兩種方法可以獲取到,第一種寫法如下:
LayoutInflater layoutInflater = LayoutInflater.from(context);
當然,還有另外一種寫法也可以完成同樣的效果:
LayoutInflater layoutInflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
其實第一種就是第二種的簡單寫法,只是Android給我們做了一下封裝而已。得到了LayoutInflater的實例之后就可以調用它的inflate()方法來加載布局了,如下所示:
layoutInflater.inflate(resourceId, root);
inflate()方法一般接收兩個參數,第一個參數就是要加載的布局id,第二個參數是指給該布局的外部再嵌套一層父布局,如果不需要就直接傳null。這樣就成功成功創建了一個布局的實例,之后再將它添加到指定的位置就可以顯示出來了。
下面我們就通過一個非常簡單的小例子,來更加直觀地看一下LayoutInflater的用法。比如說當前有一個項目,其中MainActivity對應的布局文件叫做activity_main.xml,代碼如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/main_layout" android:layout_width="match_parent" android:layout_height="match_parent" > </LinearLayout>
這個布局文件的內容非常簡單,只有一個空的LinearLayout,里面什么控件都沒有,因此界面上應該不會顯示任何東西。
那么接下來我們再定義一個布局文件,給它取名為button_layout.xml,代碼如下所示:
<Button xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Button" > </Button>
這個布局文件也非常簡單,只有一個Button按鈕而已。現在我們要想辦法,如何通過LayoutInflater來將button_layout這個布局添加到主布局文件的LinearLayout中。根據剛剛介紹的用法,修改MainActivity中的代碼,如下所示:
public class MainActivity extends Activity { private LinearLayout mainLayout; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mainLayout = (LinearLayout) findViewById(R.id.main_layout); LayoutInflater layoutInflater = LayoutInflater.from(this); View buttonLayout = layoutInflater.inflate(R.layout.button_layout, null); mainLayout.addView(buttonLayout); } }
可以看到,這里先是獲取到了LayoutInflater的實例,然后調用它的inflate()方法來加載button_layout這個布局,最后調用LinearLayout的addView()方法將它添加到LinearLayout中。
現在可以運行一下程序,結果如下圖所示:

Button在界面上顯示出來了!說明我們確實是借助LayoutInflater成功將button_layout這個布局添加到LinearLayout中了。LayoutInflater技術廣泛應用於需要動態添加View的時候,比如在ScrollView和ListView中,經常都可以看到LayoutInflater的身影。
<Button xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="300dp" android:layout_height="80dp" android:text="Button" > </Button>
這里我們將按鈕的寬度改成300dp,高度改成80dp,這樣夠大了吧?現在重新運行一下程序來觀察效果。咦?怎么按鈕還是原來的大小,沒有任何變化!是不是按鈕仍然不夠大,再改大一點呢?還是沒有用!
其實這里不管你將Button的layout_width和layout_height的值修改成多少,都不會有任何效果的,因為這兩個值現在已經完全失去了作用。平時我們經常使用layout_width和layout_height來設置View的大小,並且一直都能正常工作,就好像這兩個屬性確實是用於設置View的大小的。而實際上則不然,它們其實是用於設置View在布局中的大小的,也就是說,首先View必須存在於一個布局中,之后如果將layout_width設置成match_parent表示讓View的寬度填充滿布局,如果設置成wrap_content表示讓View的寬度剛好可以包含其內容,如果設置成具體的數值則View的寬度會變成相應的數值。這也是為什么這兩個屬性叫作layout_width和layout_height,而不是width和height。
再來看一下我們的button_layout.xml吧,很明顯Button這個控件目前不存在於任何布局當中,所以layout_width和layout_height這兩個屬性理所當然沒有任何作用。那么怎樣修改才能讓按鈕的大小改變呢?解決方法其實有很多種,最簡單的方式就是在Button的外面再嵌套一層布局,如下所示:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <Button android:layout_width="300dp" android:layout_height="80dp" android:text="Button" > </Button> </RelativeLayout>
可以看到,這里我們又加入了一個RelativeLayout,此時的Button存在與RelativeLayout之中,layout_width和layout_height屬性也就有作用了。當然,處於最外層的RelativeLayout,它的layout_width和layout_height則會失去作用。現在重新運行一下程序,結果如下圖所示:

K!按鈕的終於可以變大了,這下總算是滿足大家的要求了吧。
看到這里,也許有些朋友心中會有一個巨大的疑惑。不對呀!平時在Activity中指定布局文件的時候,最外層的那個布局是可以指定大小的呀,layout_width和layout_height都是有作用的。確實,這主要是因為,在setContentView()方法中,Android會自動在布局文件的最外層再嵌套一個FrameLayout,所以layout_width和layout_height屬性才會有效果。
說到這里,雖然setContentView()方法大家都會用,但實際上Android界面顯示的原理要比我們所看到的東西復雜得多。任何一個Activity中顯示的界面其實主要都由兩部分組成,標題欄和內容布局。標題欄就是在很多界面頂部顯示的那部分內容,比如剛剛我們的那個例子當中就有標題欄,可以在代碼中控制讓它是否顯示。而內容布局就是一個FrameLayout,這個布局的id叫作content,我們調用setContentView()方法時所傳入的布局其實就是放到這個FrameLayout中的,這也是為什么這個方法名叫作setContentView(),而不是叫setView()。
最后再附上一張Activity窗口的組成圖吧,以便於大家更加直觀地理解:

二、視圖繪制流程
相信每個Android程序員都知道,我們每天的開發工作當中都在不停地跟View打交道,Android中的任何一個布局、任何一個控件其實都是直接或間接繼承自View的,如TextView、Button、ImageView、ListView等。這些控件雖然是Android系統本身就提供好的,我們只需要拿過來使用就可以了,但你知道它們是怎樣被繪制到屏幕上的嗎?多知道一些總是沒有壞處的,那么我們趕快進入到本篇文章的正題內容吧。
要知道,任何一個視圖都不可能憑空突然出現在屏幕上,它們都是要經過非常科學的繪制流程后才能顯示出來的。每一個視圖的繪制過程都必須經歷三個最主要的階段,即onMeasure()、onLayout()和onDraw(),下面我們逐個對這三個階段展開進行探討。
1. onMeasure()
measure是測量的意思,那么onMeasure()方法顧名思義就是用於測量視圖的大小的。
一個界面的展示可能會涉及到很多次的measure,因為一個布局中一般都會包含多個子視圖,每個視圖都需要經歷一次measure過程。ViewGroup中定義了一個measureChildren()方法來去測量子視圖的大小
當然,onMeasure()方法是可以重寫的,也就是說,如果你不想使用系統默認的測量方式,可以按照自己的意願進行定制,比如:
public class MyView extends View { ...... @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(200, 200); } }
需要注意的是,在setMeasuredDimension()方法調用之后,我們才能使用getMeasuredWidth()和getMeasuredHeight()來獲取視圖測量出的寬高,以此之前調用這兩個方法得到的值都會是0。
由此可見,視圖大小的控制是由父視圖、布局文件、以及視圖本身共同完成的,父視圖會提供給子視圖參考的大小,而開發人員可以在XML文件中指定視圖的大小,然后視圖本身會對最終的大小進行拍板。
2、onLayout()
measure過程結束后,視圖的大小就已經測量好了,接下來就是layout的過程了。正如其名字所描述的一樣,這個方法是用於給視圖進行布局的,也就是確定視圖的位置。
View中的onLayout()方法就是一個空方法,因為onLayout()過程是為了確定視圖在布局中所在的位置,而這個操作應該是由布局來完成的,即父視圖決定子視圖的顯示位置。既然如此,我們來看下ViewGroup中的onLayout()方法是怎么寫的吧,代碼如下:
@Override protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
可以看到,ViewGroup中的onLayout()方法竟然是一個抽象方法,這就意味着所有ViewGroup的子類都必須重寫這個方法。沒錯,像LinearLayout、RelativeLayout等布局,都是重寫了這個方法,然后在內部按照各自的規則對子視圖進行布局的。由於LinearLayout和RelativeLayout的布局規則都比較復雜,就不單獨拿出來進行分析了,這里我們嘗試自定義一個布局,借此來更深刻地理解onLayout()的過程。
自定義的這個布局目標很簡單,只要能夠包含一個子視圖,並且讓子視圖正常顯示出來就可以了。那么就給這個布局起名叫做SimpleLayout吧,代碼如下所示:
public class SimpleLayout extends ViewGroup { public SimpleLayout(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (getChildCount() > 0) { View childView = getChildAt(0); measureChild(childView, widthMeasureSpec, heightMeasureSpec); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (getChildCount() > 0) { View childView = getChildAt(0); childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight()); } } }
代碼非常的簡單,我們來看下具體的邏輯吧。你已經知道,onMeasure()方法會在onLayout()方法之前調用,因此這里在onMeasure()方法中判斷SimpleLayout中是否有包含一個子視圖,如果有的話就調用measureChild()方法來測量出子視圖的大小。
接着在onLayout()方法中同樣判斷SimpleLayout是否有包含一個子視圖,然后調用這個子視圖的layout()方法來確定它在SimpleLayout布局中的位置,這里傳入的四個參數依次是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),分別代表着子視圖在SimpleLayout中左上右下四個點的坐標。其中,調用childView.getMeasuredWidth()和childView.getMeasuredHeight()方法得到的值就是在onMeasure()方法中測量出的寬和高。
這樣就已經把SimpleLayout這個布局定義好了,下面就是在XML文件中使用它了,如下所示:
<com.example.viewtest.SimpleLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_launcher" /> </com.example.viewtest.SimpleLayout>
可以看到,我們能夠像使用普通的布局文件一樣使用SimpleLayout,只是注意它只能包含一個子視圖,多余的子視圖會被舍棄掉。這里SimpleLayout中包含了一個ImageView,並且ImageView的寬高都是wrap_content。現在運行一下程序,結果如下圖所示:

OK!ImageView成功已經顯示出來了,並且顯示的位置也正是我們所期望的。如果你想改變ImageView顯示的位置,只需要改變childView.layout()方法的四個參數就行了。
3、onDraw()
measure和layout的過程都結束后,接下來就進入到draw的過程了。同樣,根據名字你就能夠判斷出,在這里才真正地開始對視圖進行繪制。ViewRoot中的代碼會繼續執行並創建出一個Canvas對象,然后調用View的draw()方法來執行具體的繪制工作。draw()方法內部的繪制過程總共可以分為六步,其中第二步和第五步在一般情況下很少用到,因此這里我們只分析簡化后的繪制過程。代碼如下所示:
public void draw(Canvas canvas) { if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW); } final int privateFlags = mPrivateFlags; final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN; // Step 1, draw the background, if needed int saveCount; if (!dirtyOpaque) { final Drawable background = mBGDrawable; if (background != null) { final int scrollX = mScrollX; final int scrollY = mScrollY; if (mBackgroundSizeChanged) { background.setBounds(0, 0, mRight - mLeft, mBottom - mTop); mBackgroundSizeChanged = false; } if ((scrollX | scrollY) == 0) { background.draw(canvas); } else { canvas.translate(scrollX, scrollY); background.draw(canvas); canvas.translate(-scrollX, -scrollY); } } } final int viewFlags = mViewFlags; boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; if (!verticalEdges && !horizontalEdges) { // Step 3, draw the content if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); // Step 6, draw decorations (scrollbars) onDrawScrollBars(canvas); // we're done... return; } }
可以看到,第一步是從第9行代碼開始的,這一步的作用是對視圖的背景進行繪制。這里會先得到一個mBGDrawable對象,然后根據layout過程確定的視圖位置來設置背景的繪制區域,之后再調用Drawable的draw()方法來完成背景的繪制工作。那么這個mBGDrawable對象是從哪里來的呢?其實就是在XML中通過android:background屬性設置的圖片或顏色。當然你也可以在代碼中通過setBackgroundColor()、setBackgroundResource()等方法進行賦值。
接下來的第三步是在第34行執行的,這一步的作用是對視圖的內容進行繪制。可以看到,這里去調用了一下onDraw()方法,那么onDraw()方法里又寫了什么代碼呢?進去一看你會發現,原來又是個空方法啊。其實也可以理解,因為每個視圖的內容部分肯定都是各不相同的,這部分的功能交給子類來去實現也是理所當然的。
第三步完成之后緊接着會執行第四步,這一步的作用是對當前視圖的所有子視圖進行繪制。但如果當前的視圖沒有子視圖,那么也就不需要進行繪制了。因此你會發現View中的dispatchDraw()方法又是一個空方法,而ViewGroup的dispatchDraw()方法中就會有具體的繪制代碼。
以上都執行完后就會進入到第六步,也是最后一步,這一步的作用是對視圖的滾動條進行繪制。那么你可能會奇怪,當前的視圖又不一定是ListView或者ScrollView,為什么要繪制滾動條呢?其實不管是Button也好,TextView也好,任何一個視圖都是有滾動條的,只是一般情況下我們都沒有讓它顯示出來而已。繪制滾動條的代碼邏輯也比較復雜,這里就不再貼出來了,因為我們的重點是第三步過程。
通過以上流程分析,相信大家已經知道,View是不會幫我們繪制內容部分的,因此需要每個視圖根據想要展示的內容來自行繪制。如果你去觀察TextView、ImageView等類的源碼,你會發現它們都有重寫onDraw()這個方法,並且在里面執行了相當不少的繪制邏輯。繪制的方式主要是借助Canvas這個類,它會作為參數傳入到onDraw()方法中,供給每個視圖使用。Canvas這個類的用法非常豐富,基本可以把它當成一塊畫布,在上面繪制任意的東西,那么我們就來嘗試一下吧。
public class MyView extends View { private Paint mPaint; public MyView(Context context, AttributeSet attrs) { super(context, attrs); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); } @Override protected void onDraw(Canvas canvas) { mPaint.setColor(Color.YELLOW); canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint); mPaint.setColor(Color.BLUE); mPaint.setTextSize(20); String text = "Hello View"; canvas.drawText(text, 0, getHeight() / 2, mPaint); } }
可以看到,我們創建了一個自定義的MyView繼承自View,並在MyView的構造函數中創建了一個Paint對象。Paint就像是一個畫筆一樣,配合着Canvas就可以進行繪制了。這里我們的繪制邏輯比較簡單,在onDraw()方法中先是把畫筆設置成黃色,然后調用Canvas的drawRect()方法繪制一個矩形。然后在把畫筆設置成藍色,並調整了一下文字的大小,然后調用drawText()方法繪制了一段文字。
就這么簡單,一個自定義的視圖就已經寫好了,現在可以在XML中加入這個視圖,如下所示:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <com.example.viewtest.MyView android:layout_width="200dp" android:layout_height="100dp" /> </LinearLayout>
將MyView的寬度設置成200dp,高度設置成100dp,然后運行一下程序,結果如下圖所示:

相信大家在平時使用View的時候都會發現它是有狀態的,比如說有一個按鈕,普通狀態下是一種效果,但是當手指按下的時候就會變成另外一種效果,這樣才會給人產生一種點擊了按鈕的感覺。當然了,這種效果相信幾乎所有的Android程序員都知道該如何實現,但是我們既然是深入了解View,那么自然也應該知道它背后的實現原理應該是什么樣的,今天就讓我們來一起探究一下吧。
1、視圖狀態
視圖狀態的種類非常多,一共有十幾種類型,不過多數情況下我們只會使用到其中的幾種,因此這里我們也就只去分析最常用的幾種視圖狀態。(1
(1). enabled
表示當前視圖是否可用。可以調用setEnable()方法來改變視圖的可用狀態,傳入true表示可用,傳入false表示不可用。它們之間最大的區別在於,不可用的視圖是無法響應onTouch事件的。
(2). focused
表示當前視圖是否獲得到焦點。通常情況下有兩種方法可以讓視圖獲得焦點,即通過鍵盤的上下左右鍵切換視圖,以及調用requestFocus()方法。而現在的Android手機幾乎都沒有鍵盤了,因此基本上只可以使用requestFocus()這個辦法來讓視圖獲得焦點了。而requestFocus()方法也不能保證一定可以讓視圖獲得焦點,它會有一個布爾值的返回值,如果返回true說明獲得焦點成功,返回false說明獲得焦點失敗。一般只有視圖在focusable和focusable in touch mode同時成立的情況下才能成功獲取焦點,比如說EditText。
(3). window_focused
表示當前視圖是否處於正在交互的窗口中,這個值由系統自動決定,應用程序不能進行改變。
(4). selected
表示當前視圖是否處於選中狀態。一個界面當中可以有多個視圖處於選中狀態,調用setSelected()方法能夠改變視圖的選中狀態,傳入true表示選中,傳入false表示未選中。
(5). pressed
表示當前視圖是否處於按下狀態。可以調用setPressed()方法來對這一狀態進行改變,傳入true表示按下,傳入false表示未按下。通常情況下這個狀態都是由系統自動賦值的,但開發者也可以自己調用這個方法來進行改變。
我們可以在項目的drawable目錄下創建一個selector文件,在這里配置每種狀態下視圖對應的背景圖片。比如創建一個compose_bg.xml文件,在里面編寫如下代碼:
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/compose_pressed" android:state_pressed="true"></item>
<item android:drawable="@drawable/compose_pressed" android:state_focused="true"></item>
<item android:drawable="@drawable/compose_normal"></item>
</selector>
這段代碼就表示,當視圖處於正常狀態的時候就顯示compose_normal這張背景圖,當視圖獲得到焦點或者被按下的時候就顯示compose_pressed這張背景圖。
2、視圖重繪
雖然視圖會在Activity加載完成之后自動繪制到屏幕上,但是我們完全有理由在與Activity進行交互的時候要求動態更新視圖,比如改變視圖的狀態、以及顯示或隱藏某個控件等。那在這個時候,之前繪制出的視圖其實就已經過期了,此時我們就應該對視圖進行重繪。
調用視圖的setVisibility()、setEnabled()、setSelected()等方法時都會導致視圖重繪,而如果我們想要手動地強制讓視圖進行重繪,可以調用invalidate()方法來實現。當然了,setVisibility()、setEnabled()、setSelected()等方法的內部其實也是通過調用invalidate()方法來實現的,那么就讓我們來看一看invalidate()方法的代碼是什么樣的吧。invalidate實際上調用了視圖繪制的入口函數:performTraversals()方法。
另外需要注意的是,invalidate()方法雖然最終會調用到performTraversals()方法中,但這時measure和layout流程是不會重新執行的,因為視圖沒有強制重新測量的標志位,而且大小也沒有發生過變化,所以這時只有draw流程可以得到執行。而如果你希望視圖的繪制流程可以完完整整地重新走一遍,就不能使用invalidate()方法,而應該調用requestLayout()了。這個方法中的流程比invalidate()方法要簡單一些,但中心思想是差不多的,這里也就不再詳細進行分析了。
四、Android自定義view的實現方法
回顧一下,我們一共學習了LayoutInflater的原理分析、視圖的繪制流程、視圖的狀態及重繪等知識,算是把View中很多重要的知識點都涉及到了。
一些接觸Android不久的朋友對自定義View都有一絲畏懼感,總感覺這是一個比較高級的技術,但其實自定義View並不復雜,有時候只需要簡單幾行代碼就可以完成了。
如果說要按類型來划分的話,自定義View的實現方式大概可以分為三種,自繪控件、組合控件、以及繼承控件。那么下面我們就來依次學習一下,每種方式分別是如何自定義View的。
一、自繪控件
自繪控件的意思就是,這個View上所展現的內容全部都是我們自己繪制出來的。繪制的代碼是寫在onDraw()方法中的。
下面我們准備來自定義一個計數器View,這個View可以響應用戶的點擊事件,並自動記錄一共點擊了多少次。新建一個CounterView繼承自View,代碼如下所示:
public class CounterView extends View implements OnClickListener { private Paint mPaint; private Rect mBounds; private int mCount; public CounterView(Context context, AttributeSet attrs) { super(context, attrs); mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mBounds = new Rect(); setOnClickListener(this); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPaint.setColor(Color.BLUE); canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint); mPaint.setColor(Color.YELLOW); mPaint.setTextSize(30); String text = String.valueOf(mCount); mPaint.getTextBounds(text, 0, text.length(), mBounds); float textWidth = mBounds.width(); float textHeight = mBounds.height(); canvas.drawText(text, getWidth() / 2 - textWidth / 2, getHeight() / 2 + textHeight / 2, mPaint); } @Override public void onClick(View v) { mCount++; invalidate(); } }
可以看到,首先我們在CounterView的構造函數中初始化了一些數據,並給這個View的本身注冊了點擊事件,這樣當CounterView被點擊的時候,onClick()方法就會得到調用。而onClick()方法中的邏輯就更加簡單了,只是對mCount這個計數器加1,然后調用invalidate()方法。
調用invalidate()方法會導致視圖進行重繪,因此onDraw()方法在稍后就將會得到調用。
既然CounterView是一個自繪視圖,那么最主要的邏輯當然就是寫在onDraw()方法里的了,下面我們就來仔細看一下。這里首先是將Paint畫筆設置為藍色,然后調用Canvas的drawRect()方法繪制了一個矩形,這個矩形也就可以當作是CounterView的背景圖吧。接着將畫筆設置為黃色,准備在背景上面繪制當前的計數,注意這里先是調用了getTextBounds()方法來獲取到文字的寬度和高度,然后調用了drawText()方法去進行繪制就可以了。
這樣,一個自定義的View就已經完成了,並且目前這個CounterView是具備自動計數功能的。那么剩下的問題就是如何讓這個View在界面上顯示出來了,其實這也非常簡單,我們只需要像使用普通的控件一樣來使用CounterView就可以了。比如在布局文件中加入如下代碼:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <com.example.customview.CounterView android:layout_width="100dp" android:layout_height="100dp" android:layout_centerInParent="true" /> </RelativeLayout>
可以看到,這里我們將CounterView放入了一個RelativeLayout中,然后可以像使用普通控件來給CounterView指定各種屬性,比如通過layout_width和layout_height來指定CounterView的寬高,通過android:layout_centerInParent來指定它在布局里居中顯示。只不過需要注意,自定義的View在使用的時候一定要寫出完整的包名,不然系統將無法找到這個View。
好了,就是這么簡單,接下來我們可以運行一下程序,並不停地點擊CounterView,效果如下圖所示。

二、組合控件
組合控件的意思就是,我們並不需要自己去繪制視圖上顯示的內容,而只是用系統原生的控件就好了,但我們可以將幾個系統原生的控件組合到一起,這樣創建出的控件就被稱為組合控件。
舉個例子來說,標題欄就是個很常見的組合控件,很多界面的頭部都會放置一個標題欄,標題欄上會有個返回按鈕和標題,點擊按鈕后就可以返回到上一個界面。那么下面我們就來嘗試去實現這樣一個標題欄控件。
新建一個title.xml布局文件,代碼如下所示:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="50dp" android:background="#ffcb05" > <Button android:id="@+id/button_left" android:layout_width="60dp" android:layout_height="40dp" android:layout_centerVertical="true" android:layout_marginLeft="5dp" android:background="@drawable/back_button" android:text="Back" android:textColor="#fff" /> <TextView android:id="@+id/title_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="This is Title" android:textColor="#fff" android:textSize="20sp" /> </RelativeLayout>
TitleView中的代碼非常簡單,在TitleView的構建方法中,我們調用了LayoutInflater的inflate()方法來加載剛剛定義的title.xml布局,這部分內容我們已經在 Android LayoutInflater原理分析,帶你一步步深入了解View(一) 這篇文章中學習過了。
接下來調用findViewById()方法獲取到了返回按鈕的實例,然后在它的onClick事件中調用finish()方法來關閉當前的Activity,也就相當於實現返回功能了。
另外,為了讓TitleView有更強地擴展性,我們還提供了setTitleText()、setLeftButtonText()、setLeftButtonListener()等方法,分別用於設置標題欄上的文字、返回按鈕上的文字、以及返回按鈕的點擊事件。
到了這里,一個自定義的標題欄就完成了,那么下面又到了如何引用這個自定義View的部分,其實方法基本都是相同的,在布局文件中添加如下代碼:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <com.example.customview.TitleView android:id="@+id/title_view" android:layout_width="match_parent" android:layout_height="wrap_content" > </com.example.customview.TitleView> </RelativeLayout>
這樣就成功將一個標題欄控件引入到布局文件中了,運行一下程序,效果如下圖所示:

現在點擊一下Back按鈕,就可以關閉當前的Activity了。如果你想要修改標題欄上顯示的內容,或者返回按鈕的默認事件,只需要在Activity中通過findViewById()方法得到TitleView的實例,然后調用setTitleText()、setLeftButtonText()、setLeftButtonListener()等方法進行設置就OK了。
三、繼承控件
繼承控件的意思就是,我們並不需要自己重頭去實現一個控件,只需要去繼承一個現有的控件,然后在這個控件上增加一些新的功能,就可以形成一個自定義的控件了。這種自定義控件的特點就是不僅能夠按照我們的需求加入相應的功能,還可以保留原生控件的所有功能。 關於繼承控件這點略去。
