導語
控件是每個Android App都必不可少的一部分,無論是使用系統控件,還是使用自定義控件。
主要內容
- Android控件架構
- View的測量與繪制
- ViewGroup的測量與繪制
具體內容
了解Android控件架構以及View與ViewGroup的測量繪制,方便去自定義控件。
Android控件架構
Android中的每一個控件都會在界面中占得一塊矩形區域,而在Android中控件大致被分為兩類,即ViewGroup控件與View控件。ViewGroup控件作為父控件可以包含多個View控件,并管理其包含的View控件。
通過ViewGroup,整個界面上的控件形成了一個樹形結構,這也就是我們常說的控件樹,上層控件負責下層子控件的測量與繪制,并傳遞交互事件。
通常在Activity中使用findViewById()方法在控件樹中以樹的深度優先遍歷來查找對應元素。在每棵控件樹的頂部,都有一個ViewParent對象,這就是整棵樹的控件核心,所有的交互管理事件都由它來統一調度和分配,從而可以對整個視圖進行整體控制。下圖展示了一個View視圖樹。
通常情況下,在Activity中使用setContentView()方法來設置一個布局,在調用該方法后,布局內容才真正地顯示出來。Android界面的架構圖如下圖所示。
在每個Activity中,都包含一個Window對象,在Android中Window對象通常由PhoneWindow來實現。PhoneWindow將一個DecorView設置為整個應用的窗口的根View。
DecorView作為窗口界面的頂層視圖,封裝了封裝了一些窗口操作的通用方法。這里面所有View的監聽事件,都通過WindowManagerService來進行接收,并通過Activity對象來回調相應的onClickListener。在顯示上它將屏幕分為兩部分,一個是TitleView,另一個是ContentView。如下圖所示。
視圖樹的第二層裝載了一個LinearLayout,作為ViewGroup,這一層的布局結構會根據對應的參數設置不同的布局,如最常用的布局——上面顯示TitleBar下面是Content這樣的布局。
用戶可以通過設置requestWindowFeature(Window.FEATURE_NO_TITLE)來設置全屏顯示,視圖樹中的布局就就只有Content了,這就解釋了為什么調用requestWindowFeature()方法一定要在調用setContentView()方法之前才能生效的原因了。
在代碼中,當程序在onCreate()方法中調用setContentView()方法后,ActivityManagerService會回調onResume()方法,此時系統才會把整個DecorView添加到PhoneWindow中,并讓其顯示出來,從而最終完成界面的繪制。
View的測量
Android系統在繪制View前,也必須對View進行測量,告訴系統該畫一個多大的View。這個過程在onMeasure()方法中進行。
Android系統提供了一個類——MeasureSpec類,通過它來幫助我們測量View。MeasureSpec是一個32位的int值,其中高2位為測量模式,低30位為測量的大小。
測量模式可以為以下三種:
- EXACTLY
即精確模式,將控件的layout_width屬性或layout_height屬性指定為具體數值時,比如android:layout_width = "100dp",或者指定為match_parent屬性時(占據父View的大?。?,系統使用EXACTLY模式。 - AT_MOST
即最大值模式,當控件的layout_width屬性或layout_height屬性指定為wrap_content時,控件大小一般隨著控件的子控件或內容的變化而變化,此時控件的尺寸只要不超過父控件允許的最大尺寸即可。 - UNSPECIFIED
這個屬性比較奇怪——它不指定其大小測量模式,View想多大就多大,通常情況下在繪制自定義View時才會使用。
通過MeasureSpec這一個類,我們就獲取了View的測量模式和View想要繪制的大小。有了這些信息,我們就可以控制View最后顯示的大小。
實例:重寫onMeasure()方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
在IDE中按住Ctrl鍵查看super.onMeasure()方法,可以發現,系統最終會調用setMeasuredDimension(int widthMeasureSpec, int widthMeasureSpec)方法將測量后的寬高值設置進去,從而完成測量工作。所以重寫onMeasure()方法后,最終要做的就是把測量后的寬高值作為參數設置給setMeasuredDimension()方法。
通過上面的分析,重寫的onMeasure()方法代碼如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
measureWidth(widthMeasureSpec),
measureHeight(heightMeasureSpec));
}
在onMeasure()方法中,我們調用自定義的measureWidth()方法和measureHeight()方法分別對寬高重新定義,參數則是寬和高的MeasureSpec對象,MeasureSpec對象中包含了測量模式和測量值的大小。
- 第一步,從MeasureSpec對象中提取出具體的測量模式和大小,代碼如下:
int specMode = MeasureSpec.getMode(measureSpec); // 取出測量模式
int specSize = MeasureSpec.getSize(measureSpec); // 取出測量大小
- 第二步通過判斷測量模式,給出不同的測量值。
當specMode為EXACTLY時,直接使用指定的specSize即可;當specMode為其它兩種模式時,需要給一個默認的大小。如果指定wrap_content屬性,即AT_MOST模式,則需要用我們指定大小與specSize較小值做為最后的測量值。
measureWidth()方法如下所示,這段代碼基本可以作為模板代碼:
private int measureWidth(int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if(specMode == MeasureSpec.EXACTLY) {
result = specSize;
} else {
result = 200; // 不是精確值模式時默認值為200
if(specMode == MeasureSpec.AT_MOST) {
result = Math.min(result, specSize);
}
}
return result;
}
measureHeight()方法與masureWidth()基本一致。程序效果如下圖所示。
View的繪制
測量好了一個View之后,我們就可以簡單地重寫onDraw()方法,并在Canvas對象上來繪制所需要的圖形。Canvas就像是一個畫板,使用Paint就可以在上面作畫了。
觀察:重寫onDraw()方法:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
我們發現在onDraw()中有一個參數,就是Canvas canvas對象。使用這個Canvas對象就可以進行繪圖了,而在其它地方,通常需要使用代碼創建一個Canvas對象,代碼如下:
Canvas canvas = new Canvas(bitmap);
當創建一個Canvas對象時,我們通常會傳入一個Bitmap對象,傳入的Bitmap對象,會與通過這個Bitmap對象的Canvas畫面緊緊聯系在一起,這個過程我們稱之為裝載畫布。這個Bitmap對象用來存儲所有繪制在Canvas上的像素信息。所以當你通過這種方式創建了Canvas對象后,后有調用所有的Canvas.drawXXX()方法都發生在這個bitmap上。
在View類的onDraw()方法中,通過下面代碼,我們可以了解到canvas與bitmap直接的關系。首先在onDraw方法中繪制兩個bitmap,代碼如下所示:
canvas.drawBitmap(bitmap1, 0, 0, null);
canvas.drawBitmap(bitmap2, 0, 0, null);
而對于bitmap2,我們將它裝載到另一個Canvas對象中,代碼如下所示:
Canvas mCanvas = new Canvas(bitmap2);
在其它地方使用Canvas對象的繪圖方法在裝載bitmap2的Canvas對象上進行繪畫,代碼如下所示:
mCanvas.drawXXX
通過mCanvas將繪制效果作用在了bitmap2上,再刷新View的時候,就會發現通過onDraw()方法畫出來的bitmap2已經發生了改變,這就是因為bitma2承載了在mCanvas上所進行的繪圖操作。雖然我們也使用了Canvas的繪制API,但其實并沒有將圖形直接繪制在onDraw()方法指定的那塊畫面上,而是通過改變bitmap,然后讓View重繪,從而顯示改變之后的bitmap。
ViewGroup的測量
在前面的分析中說了,ViewGroup會去管理其子View,其中一個管理項目就是負責子View的顯示大小。當ViewGroup的大小為wrap_content時,ViewGroup就需要對子View進行遍歷,以便獲得所有子View的大小,從而來設定自己的大小。而在其它模式下則會通過具體的指定值來設置自身大小。
ViewGroup在測量時通過遍歷所有子View,從而調用子View的Measure方法來獲得每一個子View的測量結果。當子View測量完畢后,就需要將子View放到合適的位置,這個過程就是View的Layout過程。ViewGroup在執行Layout過程時,同樣是使用遍歷來調用子View的Layout方法,并指定其具體顯示的位置,從而來決定其布局位置。
在自定義ViewGroup時,通常會去重寫onLayout()方法來控制其子View顯示位置的邏輯。同樣,如果需要支持wrap_content屬性,那么它還必須重寫onMeasure()方法,這點與View是相同的。
ViewGroup的繪制
ViewGroup通常情況下不需要繪制,因為它本身就沒有需要繪制的東西,如果不是指定了ViewGroup的背景顏色,那么ViewGruop的onDraw()方法都不會被調用。但是,ViewGroup會使用dispatchDraw()方法去繪制其子View,其過程同樣是通過遍歷所有子View,并調用子View的繪制方法來完成繪制工作。