1. 程式人生 > >Android控制元件架構與控制元件繪製

Android控制元件架構與控制元件繪製

導語

控制元件是每個Android App都必不可少的一部分,無論是使用系統控制元件,還是使用自定義控制元件。

主要內容

  • Android控制元件架構
  • View的測量與繪製
  • ViewGroup的測量與繪製

具體內容

瞭解Android控制元件架構以及View與ViewGroup的測量繪製,方便去自定義控制元件。

Android控制元件架構

Android中的每一個控制元件都會在介面中佔得一塊矩形區域,而在Android中控制元件大致被分為兩類,即ViewGroup控制元件與View控制元件。ViewGroup控制元件作為父控制元件可以包含多個View控制元件,並管理其包含的View控制元件。
通過ViewGroup,整個介面上的控制元件形成了一個樹形結構,這也就是我們常說的控制元件樹,上層控制元件負責下層子控制元件的測量與繪製,並傳遞互動事件。
通常在Activity中使用findViewById()方法在控制元件樹中以樹的深度優先遍歷來查詢對應元素。在每棵控制元件樹的頂部,都有一個ViewParent物件,這就是整棵樹的控制元件核心,所有的互動管理事件都由它來統一排程和分配,從而可以對整個檢視進行整體控制。下圖展示了一個View檢視樹。

View樹結構

通常情況下,在Activity中使用setContentView()方法來設定一個佈局,在呼叫該方法後,佈局內容才真正地顯示出來。Android介面的架構圖如下圖所示。

UI介面架構圖

在每個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的繪製方法來完成繪製工作。

總結

  • 測量模式分為三種:EXACTLY(精確值模式)、AT_MOST(最大值模式)、UNSPECIFIED。
  • onMeasure()方法可以對View進行測量。
  • onDraw()方法可以對View進行繪製。