原文地址:http://blog.csdn.net/guolin_blog/article/details/16330267html
在此轉載,以備不時之需,建議看原文,轉載的排版有點問題java
在上一篇文章中,我帶着你們一塊兒剖析了一下LayoutInflater的工做原理,能夠算是對View進行深刻了解的第一步吧。那麼本篇文章中,咱們將繼續對View進行深刻探究,看一看它的繪製流程究竟是什麼樣的。若是你尚未看過個人上一篇文章,能夠先去閱讀 Android LayoutInflater原理分析,帶你一步步深刻了解View(一) 。android
相信每一個Android程序員都知道,咱們天天的開發工做當中都在不停地跟View打交道,Android中的任何一個佈局、任何一個控件其實都是直接或間接繼承自View的,如TextView、Button、ImageView、ListView等。這些控件雖然是Android系統自己就提供好的,咱們只須要拿過來使用就能夠了,但你知道它們是怎樣被繪製到屏幕上的嗎?多知道一些老是沒有壞處的,那麼咱們趕快進入到本篇文章的正題內容吧。程序員
要知道,任何一個視圖都不可能憑空忽然出如今屏幕上,它們都是要通過很是科學的繪製流程後才能顯示出來的。每個視圖的繪製過程都必須經歷三個最主要的階段,即onMeasure()、onLayout()和onDraw(),下面咱們逐個對這三個階段展開進行探討。canvas
measure是測量的意思,那麼onMeasure()方法顧名思義就是用於測量視圖的大小的。View系統的繪製流程會從ViewRoot的performTraversals()方法中開始,在其內部調用View的measure()方法。measure()方法接收兩個參數,widthMeasureSpec和heightMeasureSpec,這兩個值分別用於肯定視圖的寬度和高度的規格和大小。框架
MeasureSpec的值由specSize和specMode共同組成的,其中specSize記錄的是大小,specMode記錄的是規格。specMode一共有三種類型,以下所示:
ide
1. EXACTLY函數
表示父視圖但願子視圖的大小應該是由specSize的值來決定的,系統默認會按照這個規則來設置子視圖的大小,開發人員固然也能夠按照本身的意願設置成任意的大小。佈局
2. AT_MOST學習
表示子視圖最多隻能是specSize中指定的大小,開發人員應該儘量小得去設置這個視圖,而且保證不會超過specSize。系統默認會按照這個規則來設置子視圖的大小,開發人員固然也能夠按照本身的意願設置成任意的大小。
3. UNSPECIFIED
表示開發人員能夠將視圖按照本身的意願設置成任意的大小,沒有任何限制。這種狀況比較少見,不太會用到。
那麼你可能會有疑問了,widthMeasureSpec和heightMeasureSpec這兩個值又是從哪裏獲得的呢?一般狀況下,這兩個值都是由父視圖通過計算後傳遞給子視圖的,說明父視圖會在必定程度上決定子視圖的大小。可是最外層的根視圖,它的widthMeasureSpec和heightMeasureSpec又是從哪裏獲得的呢?這就須要去分析ViewRoot中的源碼了,觀察performTraversals()方法能夠發現以下代碼:
[java] view plaincopy
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
能夠看到,這裏調用了getRootMeasureSpec()方法去獲取widthMeasureSpec和heightMeasureSpec的值,注意方法中傳入的參數,其中lp.width和lp.height在建立ViewGroup實例的時候就被賦值了,它們都等於MATCH_PARENT。而後看下getRootMeasureSpec()方法中的代碼,以下所示:
[java] view plaincopy
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的,也就意味着根視圖老是會充滿全屏的。
介紹了這麼多MeasureSpec相關的內容,接下來咱們看下View的measure()方法裏面的代碼吧,以下所示:
[java] view plaincopy
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
mPrivateFlags &= ~MEASURED_DIMENSION_SET;
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE);
}
onMeasure(widthMeasureSpec, heightMeasureSpec);
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()這個方法是final的,所以咱們沒法在子類中去重寫這個方法,說明Android是不容許咱們改變View的measure框架的。而後在第9行調用了onMeasure()方法,這裏纔是真正去測量並設置View大小的地方,默認會調用getDefaultSize()方法來獲取視圖的大小,以下所示:
[java] view plaincopy
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過程就結束了。
固然,一個界面的展現可能會涉及到不少次的measure,由於一個佈局中通常都會包含多個子視圖,每一個視圖都須要經歷一次measure過程。ViewGroup中定義了一個measureChildren()方法來去測量子視圖的大小,以下所示:
[java] view plaincopy
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()方法來測量相應子視圖的大小,以下所示:
[java] view plaincopy
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()方法是能夠重寫的,也就是說,若是你不想使用系統默認的測量方式,能夠按照本身的意願進行定製,好比:
[java] view plaincopy
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文件中指定視圖的大小,而後視圖自己會對最終的大小進行拍板。
到此爲止,咱們就把視圖繪製流程的第一階段分析完了。
measure過程結束後,視圖的大小就已經測量好了,接下來就是layout的過程了。正如其名字所描述的同樣,這個方法是用於給視圖進行佈局的,也就是肯定視圖的位置。ViewRoot的performTraversals()方法會在measure結束後繼續執行,並調用View的layout()方法來執行此過程,以下所示:
[java] view plaincopy
host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
layout()方法接收四個參數,分別表明着左、上、右、下的座標,固然這個座標是相對於當前視圖的父視圖而言的。能夠看到,這裏還把剛纔測量出的寬度和高度傳到了layout()方法中。那麼咱們來看下layout()方法中的代碼是什麼樣的吧,以下所示:
[java] view plaincopy
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()方法是怎麼寫的吧,代碼以下:
[java] view plaincopy
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
能夠看到,ViewGroup中的onLayout()方法居然是一個抽象方法,這就意味着全部ViewGroup的子類都必須重寫這個方法。沒錯,像LinearLayout、RelativeLayout等佈局,都是重寫了這個方法,而後在內部按照各自的規則對子視圖進行佈局的。因爲LinearLayout和RelativeLayout的佈局規則都比較複雜,就不單獨拿出來進行分析了,這裏咱們嘗試自定義一個佈局,藉此來更深入地理解onLayout()的過程。
自定義的這個佈局目標很簡單,只要可以包含一個子視圖,而且讓子視圖正常顯示出來就能夠了。那麼就給這個佈局起名叫作SimpleLayout吧,代碼以下所示:
[java] view plaincopy
public class SimpleLayout extends ViewGroup {
public SimpleLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getChildCount() > 0) {
View childView = getChildAt(0);
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getChildCount() > 0) {
View childView = getChildAt(0);
childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
}
}
}
代碼很是的簡單,咱們來看下具體的邏輯吧。你已經知道,onMeasure()方法會在onLayout()方法以前調用,所以這裏在onMeasure()方法中判斷SimpleLayout中是否有包含一個子視圖,若是有的話就調用measureChild()方法來測量出子視圖的大小。
接着在onLayout()方法中一樣判斷SimpleLayout是否有包含一個子視圖,而後調用這個子視圖的layout()方法來肯定它在SimpleLayout佈局中的位置,這裏傳入的四個參數依次是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),分別表明着子視圖在SimpleLayout中左上右下四個點的座標。其中,調用childView.getMeasuredWidth()和childView.getMeasuredHeight()方法獲得的值就是在onMeasure()方法中測量出的寬和高。
這樣就已經把SimpleLayout這個佈局定義好了,下面就是在XML文件中使用它了,以下所示:
[html] view plaincopy
<com.example.viewtest.SimpleLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher"
/>
</com.example.viewtest.SimpleLayout>
能夠看到,咱們可以像使用普通的佈局文件同樣使用SimpleLayout,只是注意它只能包含一個子視圖,多餘的子視圖會被捨棄掉。這裏SimpleLayout中包含了一個ImageView,而且ImageView的寬高都是wrap_content。如今運行一下程序,結果以下圖所示:
OK!ImageView成功已經顯示出來了,而且顯示的位置也正是咱們所指望的。若是你想改變ImageView顯示的位置,只須要改變childView.layout()方法的四個參數就好了。
在onLayout()過程結束後,咱們就能夠調用getWidth()方法和getHeight()方法來獲取視圖的寬高了。說到這裏,我相信不少朋友長久以來都會有一個疑問,getWidth()方法和getMeasureWidth()方法到底有什麼區別呢?它們的值好像永遠都是相同的。其實它們的值之因此會相同基本都是由於佈局設計者的編碼習慣很是好,實際上它們之間的差異仍是挺大的。
首先getMeasureWidth()方法在measure()過程結束後就能夠獲取到了,而getWidth()方法要在layout()過程結束後才能獲取到。另外,getMeasureWidth()方法中的值是經過setMeasuredDimension()方法來進行設置的,而getWidth()方法中的值則是經過視圖右邊的座標減去左邊的座標計算出來的。
觀察SimpleLayout中onLayout()方法的代碼,這裏給子視圖的layout()方法傳入的四個參數分別是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),所以getWidth()方法獲得的值就是childView.getMeasuredWidth() - 0 = childView.getMeasuredWidth() ,因此此時getWidth()方法和getMeasuredWidth() 獲得的值就是相同的,但若是你將onLayout()方法中的代碼進行以下修改:
[java] view plaincopy
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (getChildCount() > 0) {
View childView = getChildAt(0);
childView.layout(0, 0, 200, 200);
}
}
這樣getWidth()方法獲得的值就是200 - 0 = 200,不會再和getMeasuredWidth()的值相同了。固然這種作法充分不尊重measure()過程計算出的結果,一般狀況下是不推薦這麼寫的。getHeight()與getMeasureHeight()方法之間的關係同上,就再也不重複分析了。
到此爲止,咱們把視圖繪製流程的第二階段也分析完了。
measure和layout的過程都結束後,接下來就進入到draw的過程了。一樣,根據名字你就可以判斷出,在這裏才真正地開始對視圖進行繪製。ViewRoot中的代碼會繼續執行並建立出一個Canvas對象,而後調用View的draw()方法來執行具體的繪製工做。draw()方法內部的繪製過程總共能夠分爲六步,其中第二步和第五步在通常狀況下不多用到,所以這裏咱們只分析簡化後的繪製過程。代碼以下所示:
[java] view plaincopy
public void draw(Canvas canvas) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
}
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
final Drawable background = mBGDrawable;
if (background != null) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
}
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
}
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
// we're done...
return;
}
}
能夠看到,第一步是從第9行代碼開始的,這一步的做用是對視圖的背景進行繪製。這裏會先獲得一個mBGDrawable對象,而後根據layout過程肯定的視圖位置來設置背景的繪製區域,以後再調用Drawable的draw()方法來完成背景的繪製工做。那麼這個mBGDrawable對象是從哪裏來的呢?其實就是在XML中經過android:background屬性設置的圖片或顏色。固然你也能夠在代碼中經過setBackgroundColor()、setBackgroundResource()等方法進行賦值。
接下來的第三步是在第34行執行的,這一步的做用是對視圖的內容進行繪製。能夠看到,這裏去調用了一下onDraw()方法,那麼onDraw()方法裏又寫了什麼代碼呢?進去一看你會發現,原來又是個空方法啊。其實也能夠理解,由於每一個視圖的內容部分確定都是各不相同的,這部分的功能交給子類來去實現也是理所固然的。
第三步完成以後緊接着會執行第四步,這一步的做用是對當前視圖的全部子視圖進行繪製。但若是當前的視圖沒有子視圖,那麼也就不須要進行繪製了。所以你會發現View中的dispatchDraw()方法又是一個空方法,而ViewGroup的dispatchDraw()方法中就會有具體的繪製代碼。
以上都執行完後就會進入到第六步,也是最後一步,這一步的做用是對視圖的滾動條進行繪製。那麼你可能會奇怪,當前的視圖又不必定是ListView或者ScrollView,爲何要繪製滾動條呢?其實無論是Button也好,TextView也好,任何一個視圖都是有滾動條的,只是通常狀況下咱們都沒有讓它顯示出來而已。繪製滾動條的代碼邏輯也比較複雜,這裏就再也不貼出來了,由於咱們的重點是第三步過程。
經過以上流程分析,相信你們已經知道,View是不會幫咱們繪製內容部分的,所以須要每一個視圖根據想要展現的內容來自行繪製。若是你去觀察TextView、ImageView等類的源碼,你會發現它們都有重寫onDraw()這個方法,而且在裏面執行了至關很多的繪製邏輯。繪製的方式主要是藉助Canvas這個類,它會做爲參數傳入到onDraw()方法中,供給每一個視圖使用。Canvas這個類的用法很是豐富,基本能夠把它當成一塊畫布,在上面繪製任意的東西,那麼咱們就來嘗試一下吧。
這裏簡單起見,我只是建立一個很是簡單的視圖,而且用Canvas隨便繪製了一點東西,代碼以下所示:
[java] view plaincopy
public class MyView extends View {
private Paint mPaint;
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
}
@Override
protected void onDraw(Canvas canvas) {
mPaint.setColor(Color.YELLOW);
canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
mPaint.setColor(Color.BLUE);
mPaint.setTextSize(20);
String text = "Hello View";
canvas.drawText(text, 0, getHeight() / 2, mPaint);
}
}
能夠看到,咱們建立了一個自定義的MyView繼承自View,並在MyView的構造函數中建立了一個Paint對象。Paint就像是一個畫筆同樣,配合着Canvas就能夠進行繪製了。這裏咱們的繪製邏輯比較簡單,在onDraw()方法中先是把畫筆設置成黃色,而後調用Canvas的drawRect()方法繪製一個矩形。而後在把畫筆設置成藍色,並調整了一下文字的大小,而後調用drawText()方法繪製了一段文字。
就這麼簡單,一個自定義的視圖就已經寫好了,如今能夠在XML中加入這個視圖,以下所示:
[html] view plaincopy
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.example.viewtest.MyView
android:layout_width="200dp"
android:layout_height="100dp"
/>
</LinearLayout>
將MyView的寬度設置成200dp,高度設置成100dp,而後運行一下程序,結果以下圖所示:
圖中顯示的內容也正是MyView這個視圖的內容部分了。因爲咱們沒給MyView設置背景,所以這裏看不出來View自動繪製的背景效果。
固然了Canvas的用法還有不少不少,這裏我不可能把Canvas的全部用法都列舉出來,剩下的就要靠你們自行去研究和學習了。
到此爲止,咱們把視圖繪製流程的第三階段也分析完了。整個視圖的繪製過程就所有結束了,你如今是否是對View的理解更加深入了呢?感興趣的朋友能夠繼續閱讀 Android視圖狀態及重繪流程分析,帶你一步步深刻了解View(三) 。