標籤:
http://blog.csdn.net/guolin_blog/article/details/16330267
任何一個視圖都不可能憑空突然出現在螢幕上,它們都是要經過非常科學的繪製流程後才能顯示出來的。每一個視圖的繪製過程都必須經曆三個最主要的階段,即onMeasure()、onLayout()和onDraw(),下面我們逐個對這三個階段展開進行探討。
onMeasure()用於測量視圖大小
onLayout()用於確定視圖位置
onDraw()用於繪製視圖
一. onMeasure()
measure是測量的意思,那麼onMeasure()方法顧名思義就是用於測量視圖的大小的。
View系統的繪製流程會從ViewRoot的performTraversals()方法中開始,在其內部調用View的measure()方法。measure()方法接收兩個參數,widthMeasureSpec和heightMeasureSpec,這兩個值分別用於確定視圖的寬度和高度的規格和大小。
MeasureSpec的值由specSize和specMode共同組成的,其中specSize記錄的是大小,specMode記錄的是規格。specMode一共有三種類型,如下所示:
1. EXACTLY
表示父視圖希望子視圖的大小應該是由specSize的值來決定的,系統預設會按照這個規則來設定子視圖的大小,開發人員當然也可以按照自己的意願設定成任意的大小。
2. AT_MOST
表示子視圖最多隻能是specSize中指定的大小,開發人員應該儘可能小得去設定這個視圖,並且保證不會超過specSize。系統預設會按照這個規則來設定子視圖的大小,開發人員當然也可以按照自己的意願設定成任意的大小。
3. UNSPECIFIED
表示開發人員可以將視圖按照自己的意願設定成任意的大小,沒有任何限制。這種情況比較少見,不太會用到。
那麼你可能會有疑問了,widthMeasureSpec和heightMeasureSpec這兩個值又是從哪裡得到的呢?通常情況下,這兩個值都是由父視圖經過計算後傳遞給子視圖的,說明父視圖會在一定程度上決定子視圖的大小。但是最外層的根視圖,它的widthMeasureSpec和heightMeasureSpec又是從哪裡得到的呢?這就需要去分析ViewRoot中的源碼了,觀察performTraversals()方法可以發現如下代碼:
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
可以看到,這裡調用了getRootMeasureSpec()方法去擷取widthMeasureSpec和heightMeasureSpec的值,注意方法中傳入的參數,其中lp.width和lp.height在建立ViewGroup執行個體的時候就被賦值了,它們都等於MATCH_PARENT。然後看下getRootMeasureSpec()方法中的代碼,如下所示:
private int getRootMeasureSpec(int windowSize, int rootDimension) { int measureSpec; switch (rootDimension) { case ViewGroup.LayoutParams.MATCH_PARENT: measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); break; case ViewGroup.LayoutParams.WRAP_CONTENT: measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec; }
可以看到,這裡使用了MeasureSpec.makeMeasureSpec()方法來組裝一個MeasureSpec,當rootDimension參數等於MATCH_PARENT的時候,MeasureSpec的specMode就等於EXACTLY,當rootDimension等於WRAP_CONTENT的時候,MeasureSpec的specMode就等於AT_MOST。並且MATCH_PARENT和WRAP_CONTENT時的specSize都是等於windowSize的,也就意味著根視圖總是會充滿全屏的。
接下來看View裡的measure方法:
注意觀察,measure()這個方法是final的,因此我們無法在子類中去重寫這個方法,說明Android是不允許我們改變View的measure架構的。其中調用了onMeasure()方法,這裡才是真正去測量並設定View大小的地方,預設會調用getDefaultSize()方法來擷取視圖的大小,如下所示:
public static int getDefaultSize(int size, int measureSpec) { int result = size; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: result = size; break; case MeasureSpec.AT_MOST: case MeasureSpec.EXACTLY: result = specSize; break; } return result; }
這裡傳入的measureSpec是一直從measure()方法中傳遞過來的。然後調用MeasureSpec.getMode()方法可以解析出specMode,調用MeasureSpec.getSize()方法可以解析出specSize。接下來進行判斷,如果specMode等於AT_MOST或EXACTLY就返回specSize,這也是系統預設的行為。之後會在onMeasure()方法中調用setMeasuredDimension()方法來設定測量出的大小,這樣一次measure過程就結束了。
由上面我們可以知道:
MeasureSpec是父視圖傳遞給子視圖的布局要求。每個MeasureSpec代表了一組寬度和高度的要求。一個MeasureSpec由大小和模式組成。
一個View的measure:
performTraversals() ->
private int getRootMeasureSpec(int windowSize, int rootDimension) -> //得到根視圖的MeasureSpec
public final void measure(int widthMeasureSpec, int heightMeasureSpec)-> //其中參數為父視圖傳遞下來的
onMeasure() -> public static int getDefaultSize(int size, int measureSpec) //設定視圖大小
當然,一個介面的展示可能會涉及到很多次的measure,因為一個布局中一般都會包含多個子視圖,每個視圖都需要經曆一次measure過程。ViewGroup中定義了一個measureChildren()方法來去測量子視圖的大小,如下所示:
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { final int size = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < size; ++i) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { measureChild(child, widthMeasureSpec, heightMeasureSpec); } } }
這裡首先會去遍曆當前布局下的所有子視圖,然後逐個調用measureChild()方法來測量相應子視圖的大小,如下所示:
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { final LayoutParams lp = child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
可以看到,在第4行和第6行分別調用了getChildMeasureSpec()方法來去計運算元視圖的MeasureSpec,計算的依據就是布局檔案中定義的MATCH_PARENT、WRAP_CONTENT等值,這個方法的內部細節就不再貼出。然後在第8行調用子視圖的measure()方法,並把計算出的MeasureSpec傳遞進去,之後的流程就和前面所介紹的一樣了。
當然,onMeasure()方法是可以重寫的,也就是說,如果你不想使用系統預設的測量方式,可以按照自己的意願進行定製,比如:
public class MyView extends View { ...... @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(200, 200); } }
這樣的話就把View預設的測量流程覆蓋掉了,不管在布局檔案中定義MyView這個視圖的大小是多少,最終在介面上顯示的大小都將會是200*200。
需要注意的是,在setMeasuredDimension()方法調用之後,我們才能使用getMeasuredWidth()和getMeasuredHeight()來擷取視圖測量出的寬高,以此之前調用這兩個方法得到的值都會是0。
由此可見,視圖大小的控制是由父視圖、布局檔案、以及視圖本身共同完成的,父視圖會提供給子視圖參考的大小,而開發人員可以在XML檔案中指定視圖的大小,然後視圖本身會對最終的大小進行拍板。
到此為止,我們就把視圖繪製流程的第一階段分析完了。
二. onLayout()
measure過程結束後,視圖的大小就已經測量好了,接下來就是layout的過程了。正如其名字所描述的一樣,這個方法是用於給視圖進行布局的,也就是確定視圖的位置。ViewRoot的performTraversals()方法會在measure結束後繼續執行,並調用View的layout()方法來執行此過程,如下所示:
host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
layout()方法接收四個參數,分別代表著左、上、右、下的座標,當然這個座標是相對於當前視圖的父視圖而言的。可以看到,這裡還把剛才測量出的寬度和高度傳到了layout()方法中。那麼我們來看下layout()方法中的代碼是什麼樣的吧,如下所示:
public void layout(int l, int t, int r, int b) { int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; boolean changed = setFrame(l, t, r, b); if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) { if (ViewDebug.TRACE_HIERARCHY) { ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT); } onLayout(changed, l, t, r, b); mPrivateFlags &= ~LAYOUT_REQUIRED; if (mOnLayoutChangeListeners != null) { ArrayList<OnLayoutChangeListener> listenersCopy = (ArrayList<OnLayoutChangeListener>) mOnLayoutChangeListeners.clone(); int numListeners = listenersCopy.size(); for (int i = 0; i < numListeners; ++i) { listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); } } } mPrivateFlags &= ~FORCE_LAYOUT; }
在layout()方法中,首先會調用setFrame()方法來判斷視圖的大小是否發生過變化,以確定有沒有必要對當前的視圖進行重繪,同時還會在這裡把傳遞過來的四個參數分別賦值給mLeft、mTop、mRight和mBottom這幾個變數。接下來會在第11行調用onLayout()方法,正如onMeasure()方法中的預設行為一樣,也許你已經迫不及待地想知道onLayout()方法中的預設行為是什麼樣的了。進入onLayout()方法,咦?怎麼這是個空方法,一行代碼都沒有?!
沒錯,View中的onLayout()方法就是一個空方法,因為onLayout()過程是為了確定視圖在布局中所在的位置,而這個操作應該是由布局來完成的,即父視圖決定子視圖的顯示位置。既然如此,我們來看下ViewGroup中的onLayout()方法是怎麼寫的吧,代碼如下:
@Override protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
可以看到,ViewGroup中的onLayout()方法竟然是一個抽象方法,這就意味著所有ViewGroup的子類都必須重寫這個方法。沒錯,像LinearLayout、RelativeLayout等布局,都是重寫了這個方法,然後在內部按照各自的規則對子視圖進行布局的
Android-視圖繪製