標籤:
View的繪製流程
Android中的任何一個布局、任何一個控制項其實都是直接或間接繼承自View的
每一個視圖的繪製過程都必須經曆三個最主要的階段,即onMeasure()、onLayout()和onDraw()
一. onMeasure()
measure是測量的意思,那麼onMeasure()方法顧名思義就是用於測量視圖的大小的。View系統的繪製流程會從ViewRoot的performTraversals()方法中開始,在其內部調用View的measure()方法。measure()方法接收兩個參數,widthMeasureSpec和heightMeasureSpec,這兩個值分別用於確定視圖的寬度和高度的規格和大小。
MeasureSpec的值由specSize和specMode共同組成的,其中specSize記錄的是大小,specMode記錄的是規格。
1. EXACTLY
表示父視圖希望子視圖的大小應該是由specSize的值來決定的,系統預設會按照這個規則來設定子視圖的大小,開發人員當然也可以按照自己的意願設定成任意的大小。
2. AT_MOST
表示子視圖最多(小於等於)只能是specSize中指定的大小,開發人員應該儘可能小得去設定這個視圖,並且保證不會超過specSize。系統預設會按照這個規則來設定子視圖的大小,開發人員當然也可以按照自己的意願設定成任意的大小。
3. UNSPECIFIED(不常用unspecified)
表示開發人員可以將視圖按照自己的意願設定成任意的大小,沒有任何限制。這種情況比較少見,不太會用到。
通常情況下,這兩個值是父視圖通過計算傳給子視圖的,說明父視圖會在一定程度上決定子視圖的大小。但是最外層的根視圖,它的widthMeasureSpec和heightMeasureSpec又是從哪裡得到的呢?這就需要去分析ViewRoot中的源碼了,觀察performTraversals()方法可以發現如下代碼:
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
可以看到,這裡調用了getRootMeasureSpec()方法去擷取widthMeasureSpec和heightMeasureSpec的值,注意方法中傳入的參數,其中lp.width和lp.height在建立ViewGroup執行個體的時候就被賦值了,它們都等於MATCH_PARENT。
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; }
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()方法裡面的代碼吧,如下所示:
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; }
final 因此無法在子類中重寫這個方法。說明Android是不允許我們改變View的measure架構的,這裡才是真正去測量並設定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過程就結束了。
當然,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()
View中的onLayout()方法就是一個空方法,因為onLayout()過程是為了確定視圖在布局中所在的位置,而這個操作應該是由布局來完成的,即父視圖決定子視圖的顯示位置。既然如此,我們來看下ViewGroup中的onLayout()方法是怎麼寫的吧
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
可以看到,ViewGroup中的onLayout()方法竟然是一個抽象方法,這就意味著所有ViewGroup的子類都必須重寫這個方法。
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) { //判斷SimpleLayout 是否包含一個子布局 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()); //確定它在SimpleLayout布局中的位置 }
這裡傳入的四個參數依次是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),分別代表著子視圖在SimpleLayout中左上右下四個點的座標。其中,調用childView.getMeasuredWidth()和childView.getMeasuredHeight()方法得到的值就是在onMeasure()方法中測量出的寬和高。 }}
<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。現在運行一下程式,結果如所示:
在onLayout()過程結束後,我們就可以調用getWidth()方法和getHeight()方法來擷取視圖的寬高了
getWidth()方法和getMeasureWidth()方法到底有什麼區別呢?它們的值好像永遠都是相同的。其實它們的值之所以會相同基本都是因為布局設計者的編碼習慣非常好,實際上它們之間的差別還是挺大的。
首先getMeasureWidth()方法在measure()過程結束後就可以擷取到了,而getWidth()方法要在layout()過程結束後才能擷取到。另外,getMeasureWidth()方法中的值是通過setMeasuredDimension()方法來進行設定的,而getWidth()方法中的值則是通過視圖右邊的座標減去左邊的座標計算出來的。
三. onDraw()繪製
measure和layout的過程都結束後,接下來就進入到draw的過程了。同樣,根據名字你就能夠判斷出,在這裡才真正地開始對視圖進行繪製。
ViewRoot中的代碼會繼續執行並建立出一個Canvas對象,然後調用View的draw()方法來執行具體的繪製工作。draw()方法內部的繪製過程總共可以分為六步,其中第二步和第五步在一般情況下很少用到,因此這裡我們只分析簡化後的繪製過程。代碼如下所示:
第三步完成之後緊接著會執行第四步,這一步的作用是對當前視圖的所有子視圖進行繪製。但如果當前的視圖沒有子視圖,那麼也就不需要進行繪製了。因此你會發現View中的dispatchDraw()方法又是一個空方法,而ViewGroup的dispatchDraw()方法中就會有具體的繪製代碼。
以上都執行完後就會進入到第六步,也是最後一步,這一步的作用是對視圖的捲軸進行繪製。那麼你可能會奇怪,當前的視圖又不一定是ListView或者ScrollView,為什麼要繪製捲軸呢?其實不管是Button也好,TextView也好,任何一個視圖都是有捲軸的,只是一般情況下我們都沒有讓它顯示出來而已。繪製捲軸的代碼邏輯也比較複雜,這裡就不再貼出來了,因為我們的重點是第三步過程。
通過以上流程分析,相信大家已經知道,View是不會幫我們繪製內容部分的,因此需要每個視圖根據想要展示的內容來自行繪製。如果你去觀察TextView、ImageView等類的源碼,你會發現它們都有重寫onDraw()這個方法,並且在裡面執行了相當不少的繪製邏輯。繪製的方式主要是藉助Canvas這個類,它會作為參數傳入到onDraw()方法中,供給每個視圖使用。Canvas這個類的用法非常豐富,基本可以把它當成一塊畫布,在上面繪製任意的東西
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); } }
Android--自訂控制項(二)