繼承ViewGroup重寫onMeasure方法的詳解
我們繼承重寫ViewGroup的目的是要做自定義控件,所以我們有必要先看一下安卓View的繪製過程:
首先當Activity獲得焦點時,它將被要求繪製自己的布局,Android framework將會處理繪製過程,Activity隻需提供它的布局的根節點。
繪製過程從布局的根節點開始,從根節點開始測量和繪製整個layout tree,繪畫通過遍曆整個樹來完成,不可見的區域的View被放棄。
每一個ViewGroup 負責要求它的每一個孩子被繪製,每一個View負責繪製自己。
因為整個樹是按順序遍曆的,所以父節點會先被繪製,而兄弟節點會按照它們在樹中出現的順序被繪製。
繪製是兩個過程:一個measure過程和一個layout過程。
1.測量過程
是在measure(int, int)中實現的,是從樹的頂端由上到下進行的。
在這個遞歸過程中,每一個View會把自己的dimension specifications傳遞下去。
在measure過程的最後,每一個View都存儲好了自己的measurements,即測量結果。
2.布局過程
發生在 layout(int, int, int, int)中,仍然是從上到下進行。
在這一遍中,每一個parent都會負責用測量過程中得到的尺寸,把自己的所有孩子放在正確的地方。
所以在繼承ViewGroup類時,需要重寫兩個方法,分別是onMeasure和onLayout。重寫ViewGroup的過程大致是兩個:
1)測量過程>>>onMeasure(int widthMeasureSpec, int heightMeasureSpec)
傳入的參數是本View的可見長和寬,通過這個方法循環測量所有View的尺寸並且存儲在View裏麵;
2)布局過程>>>onLayout(boolean changed, int l, int t, int r, int b)
傳入的參數是View可見區域的上下左右四邊的位置,在這個方法裏麵可以通過layout來放置子View;
我們先來看一下測量的過程,也就是該如何重寫onMeasure方法,重寫之前我們先要了解這個方法:
onMeasure方法
onMeasure方法是測量view和它的內容,決定measured width和measured height的,子類可以覆寫onMeasure來提供更加準確和有效的測量。
注意:在覆寫onMeasure方法的時候,必須調用 setMeasuredDimension(int,int)
來存儲這個View經過測量得到的measured
width and height。
如果沒有這麼做,將會由measure(int, int)方法拋出一個IllegalStateException。
並且覆寫onMeasure方法的時候,子類有責任確保measured height and width至少為這個View的最小height和width。getSuggestedMinimumHeight()
and getSuggestedMinimumWidth()
onMeasure方法如下:
protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec)
其中兩個參數如下:
widthMeasureSpec
heightMeasureSpec
傳入的參數是兩個int分別是parent提出的水平和垂直的空間要求。
這兩個要求是按照View.MeasureSpec類來進行編碼的。參見View.MeasureSpec這個類的說明:
兩個參數分別代表寬度和高度的MeasureSpec,android2.2文檔中對於MeasureSpec中的說明是:
一個MeasureSpec封裝了從父容器傳遞給子容器的布局需求.每一個MeasureSpec代表了一個寬度,或者高度的說明.一個MeasureSpec是一個大小跟模式的組合值.
這個類包裝了從parent傳遞下來的布局要求,傳遞給這個child。
簡單地說就是每一個MeasureSpec代表了對寬度或者高度的一個要求。
每一個MeasureSpec有一個尺寸(size)和一個模式(mode)構成。
MeasureSpecs這個類提供了把一個<size, mode>的元組包裝進一個int型的方法,從而減少對象分配。當然也提供了逆向的解析方法,從int值中解出size和mode。我們先看三種模式:
有三種模式:
UNSPECIFIED
這說明parent沒有對child強加任何限製,child可以是它想要的任何尺寸,子容器想要多大就多大。
EXACTLY
Parent為child決定了一個絕對尺寸,child將會被賦予這些邊界限製,不管child自己想要多大,子容器應當服從這些邊界。
AT_MOST
Child可以是自己任意的大小,但是有個絕對尺寸的上限,即子容器可以是聲明大小內的任意大小。
當我們設置width或height為fill_parent時,容器在布局時調用子view的measure方法傳入的模式是EXACTLY,因為子view會占據剩餘容器的空間,所以它大小是確定的。而當設置為wrap_content時,容器傳進去的是AT_MOST, 表示子view的大小最多是多少,這樣子view會根據這個上限來設置自己的尺寸。當子view的大小設置為精確值時,容器傳入的是EXACTLY, 而MeasureSpec的UNSPECIFIED模式目前還沒有發現在什麼情況下使用。
View的onMeasure方法默認行為是當模式為UNSPECIFIED時,設置尺寸為mMinWidth(通常為0)或者背景drawable的最小尺寸,當模式為EXACTLY或者AT_MOST時,尺寸設置為傳入的MeasureSpec的大小。
具體取出模式或者值的方法:
根據提供的測量值(格式)提取模式(上述三個模式之一)
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
根據提供的測量值(格式)提取大小值(這個大小也就是我們通常所說的大小)
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
而合成則可以使用下麵的方法:
根據提供的大小值和模式創建一個測量值(格式)
MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
我們回憶一下之前一開始講過的View繪製過程
當一個View的measure()方法返回的時候,它的getMeasuredWidth和getMeasuredHeight方法的值一定是被設置好的。它所有的子節點同樣被設置好。一個View的測量寬和測量高一定要遵循父View的約束,這保證了在測量過程結束的時候,所有的父View可以接受子View的測量值。一個父View或許會多次調用子View的measure()方法。舉個例子,父View會使用不明確的尺寸去丈量看看子View到底需要多大,當子View總的尺寸太大或者太小的時候會再次使用實際的尺寸去調用onMeasure().
下麵我們來看看具體代碼:
我們來看View類中measure和onMeasure函數的源碼:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { // first clears the measured dimension flag mPrivateFlags &= ~MEASURED_DIMENSION_SET; if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE); } // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); // flag not set, setMeasuredDimension() was not invoked, we raise // an exception to warn the developer if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) { throw new IllegalStateException("onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } mPrivateFlags |= LAYOUT_REQUIRED; } mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; }
measure的過程是固定的,而measure中調用了onMeasure函數,因此真正有變數的是onMeasure函數,onMeasure的默認實現很簡單,源碼如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
onMeasure默認的實現僅僅調用了setMeasuredDimension,setMeasuredDimension函數是一個很關鍵的函數,它對View的成員變量mMeasuredWidth和mMeasuredHeight變量賦值,而measure的主要目的就是對View樹中的每個View的mMeasuredWidth和mMeasuredHeight進行賦值。一旦這兩個變量被賦值,則意味著該View的測量工作結束。
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= MEASURED_DIMENSION_SET; }
對於非ViewGroup的View而言,通過調用上麵默認的measure——>onMeasure,即可完成View的測量,當然你也可以重載onMeasure,並調用setMeasuredDimension來設置任意大小的布局,但一般不這麼做。
對於ViewGroup的子類而言,往往會重載onMeasure函數負責其children的measure工作,重載時不要忘記調用setMeasuredDimension來設置自身的mMeasuredWidth和mMeasuredHeight。如果我們在layout的時候不需要依賴子視圖的大小,那麼不重載onMeasure也可以,但是必須重載onLayout來安排子視圖的位置。
ViewGroup中定義了measureChildren, measureChild, measureChildWithMargins來對子視圖進行測量,measureChildren內部隻是循環調用measureChild,measureChild和measureChildWithMargins的區別就是是否把margin和padding也作為子視圖的大小。
getChildMeasureSpec的總體思路就是通過其父視圖提供的MeasureSpec參數得到specMode和specSize,並根據計算出來的specMode以及子視圖的childDimension(layout_width和layout_height中定義的)來計算自身的measureSpec,如果其本身包含子視圖,則計算出來的measureSpec將作為調用其子視圖measure函數的參數,同時也作為自身調用setMeasuredDimension的參數,如果其不包含子視圖則默認情況下最終會調用onMeasure的默認實現,並最終調用到setMeasuredDimension,而該函數的參數正是這裏計算出來的。
總結:從上麵的描述看出,決定權最大的就是View的設計者,因為設計者可以通過調用setMeasuredDimension決定視圖的最終大小,例如調用setMeasuredDimension(100, 100)將視圖的mMeasuredWidth和mMeasuredHeight設置為100,100,那麼父視圖提供的大小以及程序員在xml中設置的layout_width和layout_height將完全不起作用,當然良好的設計一般會根據子視圖的measureSpec來設置mMeasuredWidth和mMeasuredHeight的大小,以尊重程序員的意圖。
下麵我們看一下具體的重寫代碼:
/** * 計算控件的大小 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int measureWidth = measureWidth(0, widthMeasureSpec); int measureHeight = measureHeight(0, heightMeasureSpec); // 計算自定義的ViewGroup中所有子控件的大小 // 首先判斷params.width的值是多少,有三種情況。 // // 如果是大於零的話,及傳遞的就是一個具體的值,那麼,構造MeasupreSpec的時候可以直接用EXACTLY。 // // 如果為-1的話,就是MatchParent的情況,那麼,獲得父View的寬度,再用EXACTLY來構造MeasureSpec。 // // 如果為-2的話,就是wrapContent的情況,那麼,構造MeasureSpec的話直接用一個負數就可以了。 // measureChildren(widthMeasureSpec, heightMeasureSpec); for (int i = 0; i < getChildCount(); i++) { View v = getChildAt(i); int widthSpec = 0; int heightSpec = 0; LayoutParams params = v.getLayoutParams(); if (params.width > 0) { widthSpec = MeasureSpec.makeMeasureSpec(params.width, MeasureSpec.EXACTLY); } else if (params.width == -1) { widthSpec = MeasureSpec.makeMeasureSpec(measureWidth, MeasureSpec.EXACTLY); } else if (params.width == -2) { widthSpec = MeasureSpec.makeMeasureSpec(measureWidth, MeasureSpec.AT_MOST); } if (params.height > 0) { heightSpec = MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.EXACTLY); } else if (params.height == -1) { heightSpec = MeasureSpec.makeMeasureSpec(measureHeight, MeasureSpec.EXACTLY); } else if (params.height == -2) { heightSpec = MeasureSpec.makeMeasureSpec(measureWidth, MeasureSpec.AT_MOST); } v.measure(widthSpec, heightSpec); } // 設置自定義的控件MyViewGroup的大小 setMeasuredDimension(measureWidth, measureHeight); } private int measureWidth(int size, int pWidthMeasureSpec) { int result = size; int widthMode = MeasureSpec.getMode(pWidthMeasureSpec);// 得到模式 int widthSize = MeasureSpec.getSize(pWidthMeasureSpec);// 得到尺寸 switch (widthMode) { /** * mode共有三種情況,取值分別為MeasureSpec.UNSPECIFIED, MeasureSpec.EXACTLY, * MeasureSpec.AT_MOST。 * * * MeasureSpec.EXACTLY是精確尺寸, * 當我們將控件的layout_width或layout_height指定為具體數值時如andorid * :layout_width="50dip",或者為FILL_PARENT是,都是控件大小已經確定的情況,都是精確尺寸。 * * * MeasureSpec.AT_MOST是最大尺寸, * 當控件的layout_width或layout_height指定為WRAP_CONTENT時 * ,控件大小一般隨著控件的子空間或內容進行變化,此時控件尺寸隻要不超過父控件允許的最大尺寸即可 * 。因此,此時的mode是AT_MOST,size給出了父控件允許的最大尺寸。 * * * MeasureSpec.UNSPECIFIED是未指定尺寸,這種情況不多,一般都是父控件是AdapterView, * 通過measure方法傳入的模式。 */ case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = widthSize; break; } return result; } private int measureHeight(int size, int pHeightMeasureSpec) { int result = size; int heightMode = MeasureSpec.getMode(pHeightMeasureSpec); int heightSize = MeasureSpec.getSize(pHeightMeasureSpec); switch (heightMode) { case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = heightSize; break; } return result; }
這是一個重寫的簡單例子,已經經過測試了。我再貼一下這個類的代碼吧:
package com.example.component; import android.content.Context; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; public class MyLayout extends ViewGroup { // 三種默認構造器 <span > </span>public MyLayout(Context context) { <span > </span>super(context); <span > </span>} public MyLayout(Context context, AttributeSet attrs) { super(context, attrs); } public MyLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } /** * 計算控件的大小 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int measureWidth = measureWidth(0, widthMeasureSpec); int measureHeight = measureHeight(0, heightMeasureSpec); // 計算自定義的ViewGroup中所有子控件的大小 // 首先判斷params.width的值是多少,有三種情況。 // // 如果是大於零的話,及傳遞的就是一個具體的值,那麼,構造MeasupreSpec的時候可以直接用EXACTLY。 // // 如果為-1的話,就是MatchParent的情況,那麼,獲得父View的寬度,再用EXACTLY來構造MeasureSpec。 // // 如果為-2的話,就是wrapContent的情況,那麼,構造MeasureSpec的話直接用一個負數就可以了。 // measureChildren(widthMeasureSpec, heightMeasureSpec); for (int i = 0; i < getChildCount(); i++) { View v = getChildAt(i); int widthSpec = 0; int heightSpec = 0; LayoutParams params = v.getLayoutParams(); if (params.width > 0) { widthSpec = MeasureSpec.makeMeasureSpec(params.width, MeasureSpec.EXACTLY); } else if (params.width == -1) { widthSpec = MeasureSpec.makeMeasureSpec(measureWidth, MeasureSpec.EXACTLY); } else if (params.width == -2) { widthSpec = MeasureSpec.makeMeasureSpec(measureWidth, MeasureSpec.AT_MOST); } if (params.height > 0) { heightSpec = MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.EXACTLY); } else if (params.height == -1) { heightSpec = MeasureSpec.makeMeasureSpec(measureHeight, MeasureSpec.EXACTLY); } else if (params.height == -2) { heightSpec = MeasureSpec.makeMeasureSpec(measureWidth, MeasureSpec.AT_MOST); } v.measure(widthSpec, heightSpec); } // 設置自定義的控件MyLayout的大小 setMeasuredDimension(measureWidth, measureHeight); } private int measureWidth(int size, int pWidthMeasureSpec) { int result = size; int widthMode = MeasureSpec.getMode(pWidthMeasureSpec);// 得到模式 int widthSize = MeasureSpec.getSize(pWidthMeasureSpec);// 得到尺寸 switch (widthMode) { /** * mode共有三種情況,取值分別為MeasureSpec.UNSPECIFIED, MeasureSpec.EXACTLY, * MeasureSpec.AT_MOST。 * * * MeasureSpec.EXACTLY是精確尺寸, * 當我們將控件的layout_width或layout_height指定為具體數值時如andorid * :layout_width="50dip",或者為FILL_PARENT是,都是控件大小已經確定的情況,都是精確尺寸。 * * * MeasureSpec.AT_MOST是最大尺寸, * 當控件的layout_width或layout_height指定為WRAP_CONTENT時 * ,控件大小一般隨著控件的子空間或內容進行變化,此時控件尺寸隻要不超過父控件允許的最大尺寸即可 * 。因此,此時的mode是AT_MOST,size給出了父控件允許的最大尺寸。 * * * MeasureSpec.UNSPECIFIED是未指定尺寸,這種情況不多,一般都是父控件是AdapterView, * 通過measure方法傳入的模式。 */ case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = widthSize; break; } return result; } private int measureHeight(int size, int pHeightMeasureSpec) { int result = size; int heightMode = MeasureSpec.getMode(pHeightMeasureSpec); int heightSize = MeasureSpec.getSize(pHeightMeasureSpec); switch (heightMode) { case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = heightSize; break; } return result; } /** * 覆寫onLayout,其目的是為了指定視圖的顯示位置,方法執行的前後順序是在onMeasure之後,因為視圖肯定是隻有知道大小的情況下, * 才能確定怎麼擺放 */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // 記錄總高度 int mTotalHeight = 0; // 遍曆所有子視圖 int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { View childView = getChildAt(i); // 獲取在onMeasure中計算的視圖尺寸 int measureHeight = childView.getMeasuredHeight(); int measuredWidth = childView.getMeasuredWidth(); childView.layout(l, mTotalHeight, measuredWidth, mTotalHeight + measureHeight); mTotalHeight += measureHeight; } } }
希望大家能有所收獲。所用到的知識上麵已經講過了,我對這部分知識目前理解的也還是不透徹,最近需要用到,從網上看了很多大神的文章邊學邊寫的,等到我繼續深入之後還會再給大家補充。
我也是學生,還請大家多多指教,這次篇幅有些長了,下一次如果時間充裕,我在學習onLayout方法和學習一些例子的時候還會和大家分享的!
最後更新:2017-04-03 05:39:49