Android應用程序窗口(Activity)的測量(Measure)、佈局(Layout)和繪製(Draw)過程分析(下)

       ViewRoot類的成員函數invalidateChild首先調用另一個成員函數checkThread來檢查當前正在執行的是不是一個UI線程。若是不是的話,ViewRoot類的成員函數checkThread就會拋出一個異常出來。這是由於全部的UI操做都必需要在UI線程中執行。java

       ViewRoot類的成員函數invalidateChild接下來還會檢查當前正在處理的應用程序窗口在Y軸上是否出現有滾動條,即成員變量mCurScrollY的值不等於0, 或者前正在處理的應用程序窗口是否運行在兼容模式之下,即成員變量mTranslator的值不等於null。當一個應用程序窗口運行在兼容模式時,它顯示出來的大小和它實際被設置的大小是不同的,要通過相應的轉換處理。對於上述這兩種狀況,ViewRoot類的成員函數invalidateChild都須要調整參數dirty所描述的一個須要從新繪製的區域的大小和位置。android

       調整好參數dirty所描述的一個須要從新繪製的區域以後, ViewRoot類的成員函數invalidateChild就將它所描述的一個區域與成員變量mDirty所描述的一區域執行一個合併操做,而且將獲得的新區域保存在成員變量mDirty中。從這個操做就能夠看出,ViewRoot類的成員變量mDirty描述的就是當前正在處理的應用程序窗口下一次所要從新繪製的總區域。canvas

       設置好當前正在處理的應用程序窗口下一次所要從新繪製的總區域以後,ViewRoot類的成員函數invalidateChild最後就檢查成員變量mWillDrawSoon的值是否不等於true。若是ViewRoot類的成員mWillDrawSoon的值等於true的話,那麼就說明UI線程的消息隊列中已經有一個DO_TRAVERSAL消息在等待執行了,這時候就不須要調用ViewRoot類的成員函數scheduleTraversals來向UI線程的消息隊列發送一個DO_TRAVERSAL消息了,不然的話,就須要調用ViewRoot類的成員函數scheduleTraversals來向UI線程的消息隊列發送一個DO_TRAVERSAL消息。app

       ViewRoot類的成員函數scheduleTraversals在前面Android應用程序窗口(Activity)的繪圖表面(Surface)的建立過程分析一文中已經分析過了,這裏再也不詳述。框架

       這一步執行完成以後,返回到前面的Step 1中,即View類的成員函數layout中,接下來它就會調用另一個成員函數onLayout來從新佈局當前視圖的子視圖的佈局了。View類的成員函數onLayout是由子類來重寫的,而且只有當該子類描述的是一個容器視圖時,它纔會重寫父類View的成員函數onLayout。前面咱們已經假設當前正在處理的是應用程序窗口的頂層視圖,它的類型爲DecorView,而且它描述的是一個容器視圖,所以,接下來咱們就會繼續分析DecorView類的成員函數onLayout的實現。ide

       事實上,DecorView類是經過FrameLayout類來間接繼承View類的,而且它的成員函數onLayout是從FrameLayout類繼承下來的,所以,接下來咱們實際上要分析的是FrameLayout類的成員函數onLayout的實現。函數

       Step 5. FrameLayout.onLayout佈局

publicclassFrameLayout extendsViewGroup {
......
protectedvoidonLayout(booleanchanged, intleft, inttop, intright, intbottom) {
finalintcount = getChildCount();
finalintparentLeft = mPaddingLeft + mForegroundPaddingLeft;
finalintparentRight = right - left - mPaddingRight - mForegroundPaddingRight;
finalintparentTop = mPaddingTop + mForegroundPaddingTop;
finalintparentBottom = bottom - top - mPaddingBottom - mForegroundPaddingBottom;
mForegroundBoundsChanged = true;
for(inti = 0; i < count; i++) {
finalView child = getChildAt(i);
if(child.getVisibility() != GONE) {
finalLayoutParams lp = (LayoutParams) child.getLayoutParams();
finalintwidth = child.getMeasuredWidth();
finalintheight = child.getMeasuredHeight();
intchildLeft = parentLeft;
intchildTop = parentTop;
finalintgravity = lp.gravity;
if(gravity != -1) {
finalinthorizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
finalintverticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
switch(horizontalGravity) {
caseGravity.LEFT:
childLeft = parentLeft + lp.leftMargin;
break;
caseGravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2+
lp.leftMargin - lp.rightMargin;
break;
caseGravity.RIGHT:
childLeft = parentRight - width - lp.rightMargin;
break;
default:
childLeft = parentLeft + lp.leftMargin;
}
switch(verticalGravity) {
caseGravity.TOP:
childTop = parentTop + lp.topMargin;
break;
caseGravity.CENTER_VERTICAL:
childTop = parentTop + (parentBottom - parentTop - height) / 2+
lp.topMargin - lp.bottomMargin;
break;
caseGravity.BOTTOM:
childTop = parentBottom - height - lp.bottomMargin;
break;
default:
childTop = parentTop + lp.topMargin;
}
}
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
......
}

       這個函數定義在文件frameworks/base/core/java/android/widget/FrameLayout.java中。post


       FrameLayout類的成員變量mPaddingLeft、mPaddingRight、mPaddingTop、mPaddingBottom和mForegroundPaddingLeft、mForegroundPaddingRight、mForegroundPaddingTop、mForegroundPaddingBottom的含義咱們在前面分析Android應用程序窗品的測量過程時已經解釋過了,它們描述的是當前視圖的內邊距,而參數left、top、right和bottom描述的是當前視圖的外邊距,即它與父窗口的邊距。經過上述這些參數,咱們就能夠獲得當前視圖的子視圖所能佈局在的區域。學習

       FrameLayout類的成員函數onLayout經過一個for循環來佈局當前視圖的每個子視圖。若是一個子視圖child是可見的,那麼FrameLayout類的成員函數onLayout就會根據當前視圖能夠用來顯示子視圖的區域以及它所設置的gravity屬性來獲得它在應用程序窗口中的左上角位置(childeLeft,childTop)。

       當一個子視圖child在應用程序窗口中的左上角位置肯定了以後,再結合它在前面的測量過程當中所肯定的寬度width和高度height,咱們就能夠徹底地肯定它在應用程序窗口中的佈局了,便可以調用它的成員函數layout來設置它的位置和大小了,這恰好就是前面的Step 1所執行的操做。注意,若是當前正在佈局的子視圖child描述的也是一個視圖容器,那麼它又會重複執行Step 5的操做,直到它的全部子孫視圖都佈局完成爲止。

      至此,咱們就分析完成Android應用程序窗口的佈局過程了,接下來咱們繼續分析Android應用程序窗口的繪製過程。

      ViewRoot類的成員函數draw首先會建立一塊畫布,接着再在畫布上繪製Android應用程序窗口的UI,最後再將畫布的內容交給SurfaceFlinger服務來渲染,這個過程如圖4所示:

圖4 Android應用程序窗口的繪製過程

       Step 1. ViewRoot.draw

publicfinalclassViewRoot extendsHandler implementsViewParent,
View.AttachInfo.Callbacks {
......
privatevoiddraw(booleanfullRedrawNeeded) {
Surface surface = mSurface;
......
intyoff;
finalbooleanscrolling = mScroller != null&& mScroller.computeScrollOffset();
if(scrolling) {
yoff = mScroller.getCurrY();
} else{
yoff = mScrollY;
}
if(mCurScrollY != yoff) {
mCurScrollY = yoff;
fullRedrawNeeded = true;
}
floatappScale = mAttachInfo.mApplicationScale;
booleanscalingRequired = mAttachInfo.mScalingRequired;
Rect dirty = mDirty;
......
if(mUseGL) {
if(!dirty.isEmpty()) {
Canvas canvas = mGlCanvas;
if(mGL != null&& canvas != null) {
......
intsaveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
try{
canvas.translate(0, -yoff);
if(mTranslator != null) {
mTranslator.translateCanvas(canvas);
}
canvas.setScreenDensity(scalingRequired
? DisplayMetrics.DENSITY_DEVICE : 0);
mView.draw(canvas);
......
} finally{
canvas.restoreToCount(saveCount);
}
......
}
}
if(scrolling) {
mFullRedrawNeeded = true;
scheduleTraversals();
}
return;
}
if(fullRedrawNeeded) {
......
dirty.union(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
}
......
if(!dirty.isEmpty() || mIsAnimating) {
Canvas canvas;
try{
......
canvas = surface.lockCanvas(dirty);
......
} catch(Surface.OutOfResourcesException e) {
......
return;
} catch(IllegalArgumentException e) {
......
return;
}
try{
if(!dirty.isEmpty() || mIsAnimating) {
.....
mView.mPrivateFlags |= View.DRAWN;
......
intsaveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
try{
canvas.translate(0, -yoff);
if(mTranslator != null) {
mTranslator.translateCanvas(canvas);
}
canvas.setScreenDensity(scalingRequired
? DisplayMetrics.DENSITY_DEVICE : 0);
mView.draw(canvas);
} finally{
......
canvas.restoreToCount(saveCount);
}
......
}
} finally{
surface.unlockCanvasAndPost(canvas);
}
}
......
if(scrolling) {
mFullRedrawNeeded = true;
scheduleTraversals();
}
}
......
}

       這個函數定義在文件frameworks/base/core/java/android/view/ViewRoot.java中。


       1. 將成員變量mSurface所描述的應用程序窗口的繪圖表面保存在變量surface中,以便接下來能夠經過變量surface來操做應用程序窗口的繪圖表面。

       3. 成員變量mScrollY用來描述應用程序窗口下一次繪製時在Y軸上應該滾動到的位置,所以,若是應用程序窗口不是處於正在滾動的狀態,那麼它在下一次繪製時,就應該直接將它在Y軸上的即時滾動位置yoff設置爲mScrollY。

       5. 成員變量mAttachInfo所描述的一個AttachInfo對象的成員變量mScalingRequired表示應用程序窗口是否正在請求進行大小縮放,若是是的話,那麼所請求的大小縮放因子就保存在這個AttachInfo對象的另一個成員變量mApplicationScale中。函數將這兩個值保存在變量scalingRequired和appScale中,以便接下來可使用。

       7. 成員變量mUseGL用來描述應用程序窗口是否直接使用OpenGL接口來繪製UI。當應用程序窗口的繪圖表面的內存類型等於WindowManager.LayoutParams.MEMORY_TYPE_GPU時,那麼就表示它須要使用OpenGL接口來繪製UI,以即可以利用GPU來繪製UI。當應用程序窗口須要直接使用OpenGL接口來繪製UI時,另一個成員變量mGlCanvas就表示應用程序窗口的繪圖表面所使用的畫布,這塊畫布一樣是經過OpenGL接口來建立的。

      9. 使用OpenGL接口來繪製完成UI後,若是變量scrolling的值等於true,即應用程序窗口是處於正在滾動的狀態,那麼就意味着應用程序窗口接下來還須要立刻進行下一次重繪,並且是全部的區域都須要重繪,所以,函數接下來就會將成員變量mFullRedrawNeeded的值設置爲true,而且調用另一個成員函數scheduleTraversals來請求執行下一次的重繪操做。

     11. 參數fullRedrawNeeded用來描述是否須要繪製應用程序窗口的全部區域。若是須要的話,那麼就會將應用程序窗口的髒區域的大小設置爲整個應用程序窗口的大小(0,0,mWidth,mHeight),其中,成員變量mWidth和mHeight表示應用程序窗口的寬度和高度。注意,若是應用程序窗口的大小被設置了一個縮放因子,即變量appScale的值不等於1,那麼就須要將應用程序窗口的寬度mWidth和高度mHeight乘以這個縮放因子,而後才能夠獲得應用程序窗口的實際大小。

     13. 繪製完成以後,應用程序窗口的UI就都體如今前面所建立的畫布canvas上了,所以,這時候就須要將它交給SurfaceFlinger服務來渲染,這是經過調用用來描述應用程序窗口的繪圖表面的一個Surface對象surface的成員函數unlockCanvasAndPost來實現的。

      在本文中,咱們只關注使用非OpenGL接口來繪製應用程序窗口的UI的步驟,其中,第12步和第13步是關鍵所在。第12步調用了Java層的Surface類的成員函數lockCanvas來爲應用程序窗口的繪圖表面建立了一塊畫布,而且調用了DecorView類的成員函數draw來在這塊畫布上繪製了應用程序窗口的UI,而第13步調用了Java層的Surface類的成員函數unlockCanvasAndPost來將前面已經繪製了應用程序窗口UI的畫布交給SurfaceFlinger服務來渲染。接下來,咱們就分別分析Java層的Surface類的成員函數lockCanvas、DecorView類的成員函數draw和Java層的Surface類的成員函數unlockCanvasAndPost的實現。

Android幀緩衝區(Frame Buffer)硬件抽象層(HAL)模塊Gralloc的實現原理分析一文,這裏再也不詳述。

      至此,咱們就分析完成Android應用程序窗口的渲染過程了,從中就能夠看出:

      1. 渲染Android應用程序窗口UI須要通過三步曲:測量、佈局、繪製。

      2. Android應用程序窗口UI首先是使用Skia圖形庫API來繪製在一塊畫布上,實際地是繪製在這塊畫布裏面的一個圖形緩衝區中,這個圖形緩衝區最終會被交給SurfaceFlinger服務,而SurfaceFlinger服務再使用OpenGL圖形庫API來將這個圖形緩衝區渲染到硬件幀緩衝區中。

      Android應用程序窗口的渲染過程分析完成以後,Android應用程序窗口的實現框架就分析完成了,從新學習請回到Android應用程序窗口(Activity)實現框架簡要介紹和學習計劃一文中。

      在Android應用程序窗口(Activity)實現框架簡要介紹和學習計劃這一系列文章中,咱們主要是從單個應用程序窗口的角度來分析的。可是,Android系統在運行的過程當中,須要管理的是一系列的應用程序窗口,而且這些應用程序窗口的類型可能各不相同,而且相互影響。所以,Android的窗口管理系統是很是複雜的。在接下來的一個系列的文章中,咱們就將詳細地分析Android窗口管理服務WindowManagerService的實現,以即可以從系統的角度來分析應用程序窗口的實現。敬請關注!

老羅的新浪微博:http://weibo.com/shengyangluo,歡迎關注!

相關文章
相關標籤/搜索