第三章終于進入開發的正題了!本章主要介紹自定義控件:
#### **一、Android 控件架構**
Android中的每個控件都會在界面中占得一塊矩形的區域,而在Android中,控件大致被分為兩類,即ViewGroup控件與View控件。ViewGroup控件作為父控件可以包含多個View控件,并管理其包含的View控件。通過ViewGroup,整個界面上的控件形成了一個樹形結構,這也就是我們常說的控件樹,**上層控件負責下層子控件的測量與繪制,井傳遞交互事件**。通常在Activity中使用的findViewById()方法,就是在控件樹中以樹的深度優先遍歷來查找對應元素。**在每個控件樹的頂部,都有一個ViewParent對象,這就是整棵樹的控制核心,所有的交互管理事件都由它來統一調度和分配,從而可以對整個視圖進行整體控制**。下圖展示一個View視圖樹。
:-: 
圖1View樹結構
Activity包含一個Window對象,通常是由PhoneWindow類來實現的,PhoneWindow對象又將一個DecorView設置為整個應用的根View。DecorView作為窗口界面的頂層視圖,封裝了一些窗口操作的通用方法,DecorView將要顯示的內容呈現在了PhoneWindow上,這里所有View的監聽事件都通過WindowManagerService來接收,并通過Activity對象來回調onClickListener。DecorView在顯示上分為TitleView和ContentView兩部分,如下圖所示。可以通過如下代碼獲得ContentView:
~~~
ViewGroup content=(ViewGroup)findViewById(android.R.id.content);
~~~
:-: 
圖2 UI界面架構圖
:-: 
圖3 標準視圖樹
Activity的setContentView()方法,其實最終調用PhoneWindow的setContentView()方法,從該方法可以看出**先得到當前的一個窗體**(通過getWindow()方法)即**一個Activity一定會有一個當前的window**,當Activity實例化時,一定會實例化一個并且僅有一個window,**window本身不是顯示的視圖,只是一個窗戶玻璃,窗戶玻璃上的窗花(實際上是view)才是真正的視圖**,而且window實際上是其唯一的一個實例化的子類PhoneWindow,之后調用phoneWindow的setContentView方法,該方法內部會有一個mContentParent.addView(view, params)方法,來添加view視圖,而**這個窗花怎么裁剪和貼到玻璃呢,是通過LayoutInflater()和addView()。**
>[info] 補充:
> LayoutInflater是一個用來實例化XML布局文件為View對象的類
> LayoutInflater.infalte(R.layout.test,null)用來從指定的XML資源中填充一個新的View
**Activity相當于一個工人,該工人來建造一個窗戶phoneWindow**,這個窗戶phoneWindow有一個viewRoot根視圖(view、viewGroup),在根視圖上面就要添加一個一個的view,通過 mContentParent.addView(view, params);來達到我們的想要的效果,mContentParent是一個viewGroup(),**當我們點擊界面的某一個控件時,實際上windowManagerService接收到這個訊息,來回調Activity的方法,比如onKeyDown()方法**。**Activity是控制單元,window是承載模型,view才是真正的顯示視圖。**
為什么調用requestWindowFeature()方法一定要在setContentView()方法調用之前?**通過設置`requestWindowFeature(Window.FEATURE_NO_TITLE)`來設置全屏顯示,視圖樹中的布局就只有Content了。**
當程序在onCreate()方法中調用setContentView()方法后,ActivityManagerService會回調onResume()方法,此時系統才會將整個DecorView添加到PhoneWindow中,并讓其顯示出來,從而完成界面的繪制。
[Android 中window 、view、 Activity的關系](http://www.hmoore.net/alex_wsc/android/344868)
#### **二、View的測量**
在現實生活中,如果我們要去畫一個圖形,就必須知道它的大小和位置。同樣, Android系統在繪制View前,也必須對View進行測量,即告訴系統該畫一個多大的View。這個過程在onMeasure()方法中進行。
View的測量在onMeasure中進行,系統提供了MeasureSpec類,是一個32位的int值,其高2位為測量模式,低30位為測量的大小。測量模式有以下三種:
* EXACTLY:精確模式,當控件指定精確值(例如android:layout_width="50dp")或者指定為match_parent屬性時系統使用該模式。
* AT_MOST:最大值模式,指定wrap_content時系統使用該屬性,View類默認只支持EXACTLY,如果想使用wrap_content需自己在onMeasure中實現。控件大小一般隨著控件的子控件或內容的變化而變化,此時控件的尺寸只要不超過父控件允許的最大尺寸即可。
* UNSPECIFIED:自定義模式,View想多大就多大,**通常在繪制自定義View的時候才使用**。
View類默認的onMeasure()方法只支持EXACTLY模式,所以如果在自定義控件的時候不重寫onMeasure()方法的話,就只能使用EXACTLY模式。控件可以響應你指定的具體寬高值或者是match_parent屬性。而如果要讓自定義View支持wrap_content屬性,那么就必須在寫onMeasure()方法來指定wrap_content時的大小。
**通過MeasureSpec這個類,可以獲取View的測量模式和View想要繪制的大小**。有了這些信息就可以控制View最后顯示的大小。
①、首先重寫onMeasure()方法
~~~
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure (widthMeasureSpec, heightMeasureSpec);
}
~~~
點擊super.onMeasure()方法,進入到View.onMeasure()方法,發現系統最終會調用setMeasuredDimension()方法將測量后的寬高值設置進去,從而完成測量工作。所以在重寫onMeasure()方法后,最重要的工作就是把測量后的寬高值作為參數設置給setMeasuredDimension()方法。
**View.onMeasure()**
~~~
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
~~~
源碼中對于這個方法這樣描述:
1. 測量視圖View及其內容以確定測量的寬度和測量的高度。 這個方法由measure(int,int)調用,應該被子類覆蓋,以提供精確和高效的內容測量。
2. 覆蓋此方法時,您必須調用setMeasuredDimension(int,int)來存儲此視圖的測量的寬度和高度,如果不這樣做,則會觸發由measure(int,int)引發的IllegalStateException異常, 調用super.onMeasure (widthMeasureSpec, heightMeasureSpec);是一個有效的用法。
3. 度量的基類實現默認為背景大小,除非MeasureSpec允許更大的大小,子類應該覆蓋onMeasure(int,int)來提供對其內容的更好的度量。
4. 如果這個方法被覆蓋,那么這個子類的責任是確保測量的高度和寬度至少是視圖的最小高度和寬度——getSuggestedMinimumHeight()和getSuggestedMinimumWidth()。
>[info] 參數
> widthMeasureSpec:父控件強加的橫向空間要求,需求用android.view.View.MeasureSpec進行編碼。
> heightMeasureSpec:父控件強加的垂直空間要求,需求用android.view.View.MeasureSpec進行編碼。
示例效果如下:

圖4:TeachingView-wrap_content
代碼如下所示
[TeachingView](https://github.com/xuyisheng/AndroidHeroes/blob/master/3.Android控件架構/SystemWidget/app/src/main/java/com/imooc/systemwidget/TeachingView.java).onMeasure()
~~~
@Override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {
setMeasuredDimension(
measureWidth(widthMeasureSpec),
measureHeight(heightMeasureSpec));
}
~~~
在onMeasure(int,int)方法中,我們調用自定義的measureWidth()和mcasureHeight()方法,分別對寬高進行重新定義,參數則是寬和高的MeasureSpec對象,MeasureSpec對象中包含了測量的模式和測量值的大小。
**TeachingView.measureWidth()**
~~~
private int measureWidth(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = 200;
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
//確保view的水平像素大小小于手機分辨率的水平像素大小
}
}
return result;
}
~~~
下面以measureWidth()方法為例,講解如何自定義測量值。
* 第一步,從MeasureSpec對象中提取出具體的測量模式和大小
* 接著,通過判斷測量的模式(通過view布局文件中view的寬高屬性來判定測量模式),給出不同的測量值。當specMode是EXACTLY時,直接使用指定的specSize即可,當specMode為其他兩種模式時,需要給它-個默認的大小。特別地,如果指定wrap_content屬性,即AT_MOST模式,則需要取出我們指定的大小與specSize中最小的一個來作為最后的測量值,measureWidth()方法的代碼如上所示
下面是onMeasure的示例代碼:
~~~
//參數widthMeasureSpec和heightMeasureSpec包含了測量值的模式和大小
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);// 獲取寬度模式,參數是寬和高的MeasureSpec對象
int widthSize = MeasureSpec.getSize(widthMeasureSpec);// 獲取寬度值
int width = 0;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else {
width = 200;// 自定義的默認wrap_content值
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(widthSize, width);
}
}
int heightMode = MeasureSpec.getMode(heightMeasureSpec);// 獲取高度模式
int heightSize = MeasureSpec.getSize(heightMeasureSpec);// 獲取高度值
int height = 0;
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
height = 200;// 自定義的默認wrap_content值
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(heightSize, height);
}
}
setMeasuredDimension(width, height);// 最終將測量的值傳入該方法完成測量
}
~~~
當指定寬高屬性為wrap_content屬性時, 如果不重寫onMeasure()方法,那么系統就不知道該使用默認多大的尺寸。因此,它就會默認填充整個父布局,所以重寫onMeasure()方法的目的,就是為了能夠給View一個wrap_content屬性下的默認大小,程序運行效果如圖4所示。
在布局文件中,先指定確定的寬高值400dp,程序運行效果如圖5所示。
當指定寬高屬性為match_parent屬性時,程序運行效果如圖6所示。
:-: 
圖5 TeachingView-400dp
:-: 
圖6 TeachingView-match_parent
可以發現,當指定wrap_content屬性時,View就獲得了一個默認值200dp(由上面代碼可得知該結果), 而不是再填充父布局。
#### **三、View的繪制**
當測量好了一個View之后,我們就可以簡單地重寫onDraw()方法,并在Canvas 對象上來繪制所需要的圖形。
要想在Android的界而中繪制相應的圖像,就必須在Canvas上進行繪制。Canvas就像是一個畫板,使用Paint就可以在上面作畫了。通常需要通過繼承View并重寫它的onDraw()方法來完成繪圖。
View的繪制是通過onDraw方法實現的,具體是通過對onDraw方法中canvas參數操作執行繪圖。在其他地方,則需要自己創建canvas對象,創建時需傳入一個bitmap對象,`Canvas canvas = new Canvas(bitmap); `bitmap是用來保存Canvas.drawXXX繪制的像素信息的,通過這些繪圖操作改變的實際上就是bitmap對象而不是canvas。
**onDraw方法**
~~~
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.RED);
}
~~~
#### **四、ViewGroup的測量與繪制**
* 1、在前面的分析中說了,ViewGroup會去管理其子View,其中一個管理項目就是負責子View的顯示大小。當ViewGroup的大小為wrap_content時,它就會遍歷所有子View,并調用其Measure方法獲得其大小,來決定自身的大小,而在其他模式下則通過指定值來設置自身的大小。
* 2、然后當View測量完畢以后,ViewGroup會執行它的Layout方法,同樣是遍歷子View并調用其Layout方法來確定子View的布局位置。在自定義ViewGroup時,通常會重寫onLayout()方法來控制子View顯示位置,同樣,若需支持wrap_content還需重寫onMeasure()方法。
* 3、**ViewGroup通常不需要繪制,因為它本身沒有需要繪制的東西,如果不指定ViewGroup的背景顏色,那么ViewGroup的onDraw方法都不會被調用**。但是,ViewGroup會調用dispatchDraw方法來繪制其子view,其過程同樣是通過遍歷所有子view并調用子view的繪制方法來完成繪制工作的。
8.本章較為淺顯的分析了下事件傳遞的機制。當ViewGroup接收到事件,通過調用dispatchTouchEvent(),由這個方法再調用onInterceptTouchEvent()方法來判斷是否要攔截事件,如果返回true則攔截將事件交給onTouchEvent處理,返回false則繼續向下傳遞。當View在接受到事件時,通過調用dispatchTouchEvent(),由此方法再調用onTouchEvent方法,如果返回true則攔截事件自己處理,如果返回false則將事件向上傳遞回ViewGroup并且調用其onTouchEvent方法繼續做判斷。