天天看點

Android 控件架構及View、ViewGroup的測量

附錄:示例代碼位址

控件在Android開發的過程中是必不可少的,無論是我們在使用系統控件還是自定義的控件。下面我們将講解一下Android的控件架構,以及如何實作自定義控件。

1.Android控件架構

Android中的控件可以分為兩類:ViewGroup 控件與View控件。ViewGroup控件作為父控件可以容納多個View控件,并管理裡面的View控件。ViewGroup可以将界面上的整個控件形成一個樹形結構,也就是我們經常說的控件樹。上層的控件負責下層的子控件的測量和繪制,并傳遞互動事件。通常在Activity中使用findById()方法,就是在控件樹中以書的深度優先周遊來查找對應的元素。在每棵控件樹的頂部有一個ViewParent對象作為整棵樹的控制核心,所有的互動管理事件都由它來統一排程和配置設定,進而對整個視圖控制。

Android 控件架構及View、ViewGroup的測量

下面是Activity的UI界面架構圖,并描述的了與WindowsManager的基本關系:

Android 控件架構及View、ViewGroup的測量

如上圖所示,每個Activity都包含一個Window對象,Android中的Window對象通常由PhoneWindow來實作,PhoneWindow将一個DecorView設定為整個應用視窗的根View。Decorview作為視窗界面的頂層視圖,封裝了一些視窗操作的通用方法。也就是說,DecorView将要顯示的内容呈現在了PhoneView上,這裡的所有的View的監聽事件都是通過WindowManagerService來進行接收,并通過Activity對象來回調相應的onClickListener。在顯示上,将螢幕分為兩部分--TitleView和ContentView。在這裡我們就可以想到在Activity經常用到的方法setContentView(**),而ContentView就是一個ID為content的FrameLayout,我們做的事情就是将指定的布局設定在這個FrameLayout裡面。

Android 控件架構及View、ViewGroup的測量

ViewGroup這層的布局結構會根據對應的參數來設定不同的布局格式,例如我們最常見的布局---上面顯示TitleBar,下面顯示Content的布局方式。例如:如果使用者設定requestWindowFeature(Window.FEAURE_NO_TITILE)來設定全屏顯示,這樣視圖中隻有Content了。而且我們在閱讀Activity的源碼的時候會發現在setContentView方法裡面有initWindowDecorActionBar();被調用到,這也就說明了,為什麼我們在setContentView()方法之前必須要設定requestWindowFeature()才能做到設定生效的原因。

**2.View的測量**

首先說明一點:如果我們想要畫一個圖形,那麼我們必須要知道的元素就是大小和位置。是以系統在繪制View之前,必須要對View進行測量,這樣才能知道要話一個多大的View,這個過程在onMeasure()的方法中進行的。

Android 系統給我們提供了一個類--MeasureSpec,我們可以用這個類來測量View。MeasureSpec是一個32位的int值,其中高2為表示測量的模式,低30為表示為測量的大小,而在計算中使用位運算是為了要提高和優化效率。

測量模式可以分為以下三種:

- EXACTLY

精确值模式,當我們将控件的layout_width屬性或layout_height屬性為具體數值時,比如android:layout_width="100dp",或者指定為match_parent時,系統使用的是EXACTLY模式。

- AT_MOST

最大值模式,将控件的layout_width屬性或layout_height屬性為wrap_content時,控件大小一般随着控件的子控件或内容的變化而變化,此時控件的尺寸隻要不超過父控件允許的最大尺寸即可。

- 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()方法,系統最終會調用setMeasuredDimension(int widthMeasureSpec, int heightMeasureSpec)方法将測量後的寬高值設定進去,進而完成測量工作。是以在重寫onMeasure()方法後,最終要做的工作就是把測量後的寬高值作為參數設定給setMeasuredDimension()方法。

通過上面的分析,重寫的onMeasure()方法代碼如下所示:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
}
           

在onMeasure()方法中,我們調用自定義的measureWidth()方法和measureHeight()方法,分别對寬高進行重新定義,參數則是寬和高的MeasureSpec對象,MeasureSpec對象中包含了測量的模式和測量值的大小。

下面我們以measureWidth()方法為例,講解如何自定義測量值。

第一步,從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 = 0;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    if (specMode == MeasureSpec.EXACTLY) {
        result = specSize;
    } esle {
        result = 200;
        if (specMode == MeasureSpec.AT_MOST) {
            result = Math.min(result, specSize);
        }
    }
    return result;
}
           

measureHeight()方法的計算方法基本與measureWidth()計算方法一緻。這裡不做過多的說明。通過這兩個方法,我們就完成了對寬高值的自定義。

3.ViewGroup的測量

ViewGroup去會管理其子View,包括管理負責子View的顯示大小。當ViewGroup的大小為wrap_content,ViewGroup就需要對子View進行周遊,以便獲得所有子View的大小,進而來決定自己的大小。在其他模式下會通過具體的指定值來設定自身的大小。

VIewGroup在測量時會通過周遊所有子View,進而調用子View的Measure方法來獲得每個子View的測量結果,前面所說的對View的測量,就是在這裡進行的。

當子View測量完畢後,就需要将子View放到合适的位置,這個過程就是View的Layout過程。ViewGroup在執行Layout過程時,同樣是周遊來調用子View的Layout方法,并指定其具體顯示的位置,進而來決定其布局位置。

在自定義ViewGroup時,通常會去重寫onLayout()方法來控制其子View顯示位置的邏輯。同樣,如果需要支援wrap_content屬性,那麼它必須要還要重寫onMeasure()方法,這點與View是相同的。

下面簡述一下ViewGroup繪制的邏輯:通常ViewGroup情況下不需要繪制,因為本身就沒什麼可繪制的東西,如果不是指定了ViewGroup的背景顔色,那麼ViewGroup的onDraw()方法都不會被調用。ViewGroup會使用dispatchDraw()方法來繪制其子View,其過程同樣是通過周遊所有子View,并調用子View的繪制方法來完成繪制工作。