閱讀97 返回首頁    go 阿裏雲 go 技術社區[雲棲]


繼承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類時,需要重寫兩個方法,分別是onMeasureonLayout。重寫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

  上一篇:go HDU1398-Square Coins
  下一篇:go Qt構建及編譯出錯問題及解決方案