<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                原文出處——>[Android應用程序窗口(Activity)的測量(Measure)、布局(Layout)和繪制(Draw)過程分析](http://blog.csdn.net/luoshengyang/article/details/8372924) 在前面一篇文章中,我們分析了Android應用程序窗口的繪圖表面的創建過程。Android應用程序窗口的繪圖表面在創建完成之后,我們就可以從上到下地繪制它里面的各個視圖了,即各個UI元素了。不過在繪制這些UI元素之前,我們還需要從上到下地測量它們實際所需要的大小,以及對它們的位置進行合適的安排,即對它們進行合適的布局。在本文中,我們就將詳細地分析Android應用程序窗口的測量、布局以及繪制過程。 從前面[Android應用程序與SurfaceFlinger服務的關系概述和學習計劃](http://blog.csdn.net/luoshengyang/article/details/7846923)這一系列的文章可以知道,Android應用程序窗口請求SurfaceFlinger服務創建了一個繪圖表面之后,就可以接著請求為該繪圖表面創建圖形緩沖區,而當Android應用程序窗口往這些圖形緩沖區填充好UI數據之后,就可以請求SurfaceFlinger服務將它們渲染到硬件幀緩沖區中去,這樣我們就可以看到應用程序窗口的UI了。 Android應用程序窗口一般不會直接去操作分配給它的圖形緩沖區,而是通過一些圖形庫API來操作。例如,在前面[Android系統的開機畫面顯示過程分析](http://blog.csdn.net/luoshengyang/article/details/7691321)一文中,使用C++來開發的開機動畫應用程序bootanimation,它是通過OpenGL提供的API來繪制UI的。對于使用Java來開發的Android應用程序來說,它們一般是使用[Skia](http://zh.wikipedia.org/wiki/Skia_Graphics_Library)圖形庫提供的API來繪制UI的。在Skia圖庫中,所有的UI都是繪制在畫布(Canvas)上的,因此,Android應用程序窗口需要將它的圖形緩沖區封裝在一塊畫布里面,然后才可以使用Skia庫提供的API來繪制UI。 我們知道,一個Android應用程序窗口里面包含了很多UI元素,這些UI元素是以樹形結構來組織的,即它們存在著父子關系,其中,子UI元素位于父UI元素里面,因此,在繪制一個Android應用程序窗口的UI之前,我們首先要確定它里面的各個子UI元素在父UI元素里面的大小以及位置。確定各個子UI元素在父UI元素里面的大小以及位置的過程又稱為測量過程和布局過程。因此,**Android應用程序窗口的UI渲染過程可以分為測量、布局和繪制三個階段**,如圖1所示: :-: ![](https://box.kancloud.cn/d291a9885f918e73166f842f5aa93a48_289x289.jpg) 圖1 Android應用程序窗口渲染三步曲 從前面[Android應用程序窗口(Activity)的視圖對象(View)的創建過程分析](http://blog.csdn.net/luoshengyang/article/details/8245546)一文可以知道,Android應用程序窗口的頂層視圖是一個類型為**DecorView**的UI元素,而從前面[Android應用程序窗口(Activity)的繪圖表面(Surface)的創建過程分析](http://blog.csdn.net/luoshengyang/article/details/8303098)一文的Step 3又可以知道,這個頂層視圖最終是由ViewRoot類的成員函數performTraversals來啟動測量、布局和繪制操作的,這三個操作分別由DecorView類的成員函數measure和layout以及ViewRoot類的成員函數draw來實現的。 接下來,我們就分別從DecorView類的成員函數measure和layout以及ViewRoot類的成員函數draw開始,分析Android應用程序窗口的測量、布局和繪制過程。 **1. Android應用程序窗口的測量過程** DecorView類的成員函數measure是從父類View繼承下來的,因此,我們就從View類的成員函數measure開始分析應用程序窗口的測量過程,如圖2所示: :-: ![](https://box.kancloud.cn/b296c3ea1f31b343d690ec27759b91cc_397x438.jpg) 圖2 Android應用程序窗口的測量過程 這個過程可以分為3個步驟,接下來我們就詳細分析每一個步驟。 **Step 1. View.measure** ~~~ public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource { ...... int mPrivateFlags; ...... int mOldWidthMeasureSpec = Integer.MIN_VALUE; ...... int mOldHeightMeasureSpec = Integer.MIN_VALUE; ...... 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; ...... // 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; } ...... } ~~~ 這個函數定義在文件`frameworks/base/core/java/android/view/View.java`中。 參數**widthMeasureSpec和heightMeasureSpec用來描述當前正在處理的視圖可以獲得的最大寬度和高度**。對于應用程序窗口的頂層視圖來說,我們也可以認為這兩個參數是用來描述應用程序窗口的寬度和高度。 ViewRoot類的成員變量mPrivateFlags的類型為int,如果它的某一個位的值不等于0,那么就隱含著當前視圖有一個相應的操作在等待執行中。ViewRoot類的另外兩個成員變量mOldWidthMeasureSpec和mOldHeightMeasureSpec用來保存當前視圖上一次可以獲得的最大寬度和高度。 當ViewRoot類的成員變量mPrivateFlags的FORCE_LAYOUT位不等于0時,就表示當前視圖正在請求執行一次布局操作,這時候函數就需要重新測量當前視圖的寬度和高度。此外,當參數widthMeasureSpec和heightMeasureSpec的值不等于ViewRoot類的成員變量mldWidthMeasureSpec和mOldHeightMeasureSpec的值時,就表示當前視圖上一次可以獲得的最大寬度和高度已經失效了,這時候函數也需要重新測量當前視圖的寬度和高度。 當View類的成員函數measure決定要重新測量當前視圖的寬度和高度之后,它就會首先將成員變量mPrivateFlags的MEASURED_DIMENSION_SET位設置為0,接著再調用另外一個成員函數onMeasure來真正執行測量寬度和高度的操作。View類的成員函數onMeasure執行完成之后,需要再調用另外一個成員函數setMeasuredDimension來將測量好的寬度和高度設置到View類的成員變量mMeasuredWidth和mMeasuredHeight中,并且將成員變量mPrivateFlags的EASURED_DIMENSION_SET位設置為1。這個操作是強制的,因為當前視圖最終就是通過View類的成員變量mMeasuredWidth和mMeasuredHeight來獲得它的寬度和高度的。為了保證這個操作是強制的,View類的成員函數measure再接下來就會檢查成員變量mPrivateFlags的EASURED_DIMENSION_SET位是否被設置為1了。如果不是的話,那么就會拋出一個類型為IllegalStateException的異常來。 View類的成員函數measure最后就會把參數widthMeasureSpec和heightMeasureSpec的值保存在成員變量mldWidthMeasureSpec和mOldHeightMeasureSpec中,以便可以記錄當前視圖上一次可以獲得的最大寬度和高度。 **View類的成員函數onMeasure一般是由其子類來重寫的**。例如,對于用來應用程序窗口的頂層視圖的DecorView類來說,它是通過父類FrameLayout來重寫祖父類View的成員函數onMeasure的。因此,接下來我們就分析FrameLayout類的成員函數onMeasure的實現。 **Step 2. FrameLayout.onMeasure** ~~~ public class FrameLayout extends ViewGroup { ...... @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int count = getChildCount(); int maxHeight = 0; int maxWidth = 0; // Find rightmost and bottommost child for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (mMeasureAllChildren || child.getVisibility() != GONE) { measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); maxWidth = Math.max(maxWidth, child.getMeasuredWidth()); maxHeight = Math.max(maxHeight, child.getMeasuredHeight()); } } // Account for padding too maxWidth += mPaddingLeft + mPaddingRight + mForegroundPaddingLeft + mForegroundPaddingRight; maxHeight += mPaddingTop + mPaddingBottom + mForegroundPaddingTop + mForegroundPaddingBottom; // Check against our minimum height and width maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); // Check against our foreground's minimum height and width final Drawable drawable = getForeground(); if (drawable != null) { maxHeight = Math.max(maxHeight, drawable.getMinimumHeight()); maxWidth = Math.max(maxWidth, drawable.getMinimumWidth()); } setMeasuredDimension(resolveSize(maxWidth, widthMeasureSpec), resolveSize(maxHeight, heightMeasureSpec)); } ...... } ~~~ 這個函數定義在文件`frameworks/base/core/java/android/widget/FrameLayout.java`中。 FrameLayout類是從ViewGroup類繼承下來的,后者用來描述一個視圖容器,它有一個類型為View的數組mChildren,里面保存的就是它的各個子視圖。ViewGroup類所供了兩個成員函數getChildCount和getChildAt,它們分別用來獲得一個視圖容器所包 FrameLayout類的成員函數onMeasure首先是調用另一個成員函數measureChildWithMargins來測量每一個子視圖的寬度和高度,并且找到這些子視圖的最大寬度和高度值,保存在變量maxWidth和maxHeight 中。 FrameLayout類的成員函數onMeasure接著再將前面得到的寬度maxWidth和高度maxHeight分別加上當前視圖所設置的Padding值,其中,(mPaddingLeft,mPaddingRight,mPaddingTop,mPaddingBottom )表示當前視圖的內容區域的左右上下四條邊分別到當前視圖的左右上下四條邊的距離,它們是父類View的四個成員變量,(mForegroundPaddingLeft,mForegroundPaddingRight,mForegroundPaddingTop,mForegroundPaddingBottom)表示當前視圖的各個子視圖所圍成的區域的左右上下四條邊到當前視視的前景區域的左右上下四條邊的距離。從這里就可以看出,當前視圖的內容區域的大小就等于前景區域的大小,而前景區域的大小大于等于各個子視圖的所圍成的區域,這是因為前景區域本來就是用來覆蓋各個子視圖所圍成的區域的。 加上各個Padding值之后,得到的寬度maxWidth和高度maxHeight還不是最終的寬度和高度,還需要考慮以下兩個因素: 1. 當前視圖是否設置有最小寬度和高度。如果設置有的話,并且它們比前面計算得到的寬度maxWidth和高度maxHeight還要大,那么就將它們作為當前視圖的寬度和高度值。 2. 當前視圖是否設置有前景圖。如果設置有的話,并且它們比前面計算得到的寬度maxWidth和高度maxHeight還要大,那么就將它們作為當前視圖的寬度和高度值。 經過上述兩步檢查之后,FrameLayout類的成員函數onMeasure就得到了當前視圖的寬度maxWidth和高度maxHeight。由于得到的寬度和高度又必須要限制在參數widthMeasureSpec和heightMeasureSpec所描述的寬度和高度規范之內,因此,FrameLayout類的成員函數onMeasure就會調用從View類繼承下來的成員函數resolveSize來獲得正確的大小。得到了當前視圖的正確大小之后,FrameLayout類的成員函數onMeasure就可以調用從父類View繼承下來的成員函數setMeasuredDimension來將它們為當前視圖的大小了。 為了理解參數widthMeasureSpec和heightMeasureSpec的含義,我們繼續分析View類的成員函數resolveSize的實現,如下所示: ~~~ public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource { ...... public static int resolveSize(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: result = Math.min(size, specSize); break; case MeasureSpec.EXACTLY: result = specSize; break; } return result; } ...... } ~~~ 這個函數定義在文件rameworks/base/core/java/android/view/View.java中。 參數measureSpec的值其實是由兩部分內容來組成的,最高2位表示一個測量規范,而低30位表示一個寬度值或者高度值。測量規范有三種,分別是0、1和2,使用常量MeasureSpec.UNSPECIFIED、MeasureSpec.EXACTLY和MeasureSpec.AT_MOST來表示。 當參數measureSpec描述的規范是MeasureSpec.UNSPECIFIED時,就表示當前視圖沒有指定它的大小測量模式,這時候就使用參數size的值;當參數measureSpec描述的規范是MeasureSpec.AT_MOST時,就表示當前視圖的大小等于參數size和參數measureSpec所指定的值中的較小值;當參數measureSpec描述的規范是MeasureSpec.EXACTLY時,就表示當前視圖的大小等于參數measureSpec中所指定的值。 回到FrameLayout類的成員函數onMeasure中,我們再來看一下View類的成員函數setMeasuredDimension是如何設置當前視圖的大小的,如下所示: ~~~ public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource { ...... protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { mMeasuredWidth = measuredWidth; mMeasuredHeight = measuredHeight; mPrivateFlags |= MEASURED_DIMENSION_SET; } ...... } ~~~ 這個函數定義在文件rameworks/base/core/java/android/view/View.java中。 View類的成員函數setMeasuredDimension首先將參數measuredWidth和measuredHeight的值保存在成員變量mMeasuredWidth和mMeasuredHeight中,用來作為當前視圖的寬度和高度,并且將成員變量mPrivateFlags的位MEASURED_DIMENSION_SET設置為1,這樣返回到前面的Step 1時,就不會拋出一個類型為IllegalStateException的異常了。 FrameLayout類的另一個成員函數measureChildWithMargins是從父類ViewGroup繼承下來的,接下來我們就繼續分析它的實現,以便可以了解一個視圖容器的各個子視圖的大小的測量過程。 **Step 3. ViewGroup.measureChildWithMargins** ~~~ public abstract class ViewGroup extends View implements ViewParent, ViewManager { ...... protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + heightUsed, lp.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } ...... } ~~~ 這個函數定義在文件rameworks/base/core/java/android/view/ViewGroup.java中。 參數child用來描述當前要測量大小的子視圖,參數parentWidthMeasureSpec和parentHeightMeasureSpec用來描述當前子視圖可以獲得的最大寬度和高度,參數widthUsed和heightUsed用來描述父窗口已經使用了的寬度和高度。ViewGroup類的成員函數measureChildWithMargins必須要綜合考慮上述參數,以及當前正在測量的子視圖child所設置的大小和Margin值,還有當前視圖容器所設置的Padding值,來得到當前正在測量的子視圖child的正確寬度childWidthMeasureSpec和高度childHeightMeasureSpec,這是通過調用ViewGroup類的另外一個成員函數getChildMeasureSpec來實現的。 得到了當前正在測量的子視圖child的正確寬度childWidthMeasureSpec和高度childHeightMeasureSpec之后,就可以調用它的成員函數measure來設置它的大小了,即執行前面Step 1的操作。注意,如果當前正在測量的子視圖child描述的也是一個視圖容器,那么它又會重復執行Step 2和Step 3的操作,直到它的所有子孫視圖的大小都測量完成為止。 至此,我們就分析完成Android應用程序窗口的測量過程了,接下來我們繼續分析Android應用程序窗口的布局過程。 2. Android應用程序窗口的布局過程 DecorView類的成員函數layout是從父類View繼承下來的,因此,我們就從View類的成員函數layout開始分析應用程序窗口的布局過程,如圖3所示: :-: ![](https://box.kancloud.cn/e7d6263c8ff8bf82f514aefceec4b9ec_456x614.jpg) 圖3 Android應用程序窗口的布局過程 這個過程可以分為5個步驟,接下來我們就詳細地分析每一個步驟。 **Step 1. View.layout** ~~~ public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource { ...... int mPrivateFlags; ...... public final void layout(int l, int t, int r, int b) { boolean changed = setFrame(l, t, r, b); if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) { ...... onLayout(changed, l, t, r, b); mPrivateFlags &= ~LAYOUT_REQUIRED; } mPrivateFlags &= ~FORCE_LAYOUT; } ...... } ~~~ 這個函數定義在文件frameworks/base/core/java/android/view/View.java中。 參數l、t、r和b分別用來描述當前視圖的左上右下四條邊與其父視圖的左上右下四條邊的距離,這樣當前視圖通過這四個參數就可以知道它在父視圖中的位置以及大小。 View類的成員函數layout首先調用另外一個成員函數setFrame來設置當前視圖的位置以及大小。設置完成之后,如果當前視圖的大小或者位置與上次相比發生了變化,那么View類的成員函數setFrame的返回值changed就會等于true。在這種情況下, View類的成員函數layout就會繼續調用另外一個成員函數onLayout重新布局當前視圖的子視圖。此外,如果此時View類的成員變量mPrivateFlags的LAYOUT_REQUIRED位不等于0,那么也表示當前視圖需要重新布局它的子視圖,因此,這時候View類的成員函數layout也會調用另外一個成員函數onLayout。 當前視圖的子視圖都重新布局完成之后,View類的成員函數layout就可以將成員變量mPrivateFlags的LAYOUT_REQUIRED位設置為0了,因為此時當前視圖及其子視圖都已經執行了一次布局操作了。 View類的成員函數layout最后還會將成員變量mPrivateFlags的FORCE_LAYOUT位設置為0,也是因為此時當前視圖及其子視圖的布局已經是最新的了。 接下來,我們就繼續分析View類的成員函數setFrame和onLayout的實現,以便可以了解當前視圖及其子視圖是如何執行布局操作的。 **Step 2. View.setFrame** ~~~ public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource { ...... int mPrivateFlags; ...... int mViewFlags; ...... protected int mLeft; ...... protected int mRight; ...... protected int mTop; ...... protected int mBottom; ...... private boolean mBackgroundSizeChanged; ...... protected boolean setFrame(int left, int top, int right, int bottom) { boolean changed = false; ...... if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) { changed = true; // Remember our drawn bit int drawn = mPrivateFlags & DRAWN; // Invalidate our old position invalidate(); int oldWidth = mRight - mLeft; int oldHeight = mBottom - mTop; mLeft = left; mTop = top; mRight = right; mBottom = bottom; mPrivateFlags |= HAS_BOUNDS; int newWidth = right - left; int newHeight = bottom - top; if (newWidth != oldWidth || newHeight != oldHeight) { onSizeChanged(newWidth, newHeight, oldWidth, oldHeight); } if ((mViewFlags & VISIBILITY_MASK) == VISIBLE) { // If we are visible, force the DRAWN bit to on so that // this invalidate will go through (at least to our parent). // This is because someone may have invalidated this view // before this call to setFrame came in, therby clearing // the DRAWN bit. mPrivateFlags |= DRAWN; invalidate(); } // Reset drawn bit to original value (invalidate turns it off) mPrivateFlags |= drawn; mBackgroundSizeChanged = true; } return changed; } ...... } ~~~ 這個函數定義在文件frameworks/base/core/java/android/view/View.java中。 View類的成員變量mLeft、mRight、mTop和mBottom分別用來描述當前視圖的左右上下四條邊與其父視圖的左右上下四條邊的距離,如果它們的值與參數left、right、top和bottom的值不相等,那么就說明當前視圖的大小或者位置發生變化了。這時候View類的成員函數setFrame就需要將參數left、right、top和bottom的值分別記錄在成員變量mLeft、mRight、mTop和mBottom中。在記錄之前,還會執行兩個操作: 1. 將成員變量mPrivateFlags的DRAWN位記錄在變量drawn中,并且調用另外一個成員函數invalidate來檢查當前視圖上次請求的UI繪制操作是否已經執行。如果已經執行了的話,那么就會再請求執行一個UI繪制操作,以便可以在修改當前視圖的大小和位置之前,將當前視圖在當前位置按照當前大小顯示一次。在接下來的Step 3中,我們再詳細分析View類的成員函數invalidate的實現。 2. 計算當前視圖上一次的寬度oldWidth和oldHeight,以便接下來可以檢查當前視圖的大小是否發生了變化。 當前視圖距離父視圖的邊距一旦設置好之后,它就是一個具有邊界的視圖了,因此,View類的成員函數setFrame接著還會將成員變量mPrivateFlags的HAS_BOUNDS設置為1。 View類的成員函數setFrame再接下來又會計算當前視圖新的寬度newWidth和高度newHeight,如果它們與上一次的寬度oldWidth和oldHeight的值不相等,那么就說明當前視圖的大小發生了變化,這時候就會調用另外一個成員函數onSizeChanged來讓子類有機會處理這個變化事件。 View類的成員函數setFrame接下來繼續判斷當前視圖是否是可見的,即成員變量mViewFlags的VISIBILITY_MASK位的值是否等于VISIBLE。如果是可見的話,那么就需要將成員變量mPrivateFlags的DRAWN位設置為1,以便接下來可以調用另外一個成員函數invalidate來成功地執行一次UI繪制操作,目的是為了將當前視圖馬上顯示出來。 View類的成員變量mPrivateFlags的DRAWN位描述的是當前視圖上一次請求的UI繪制操作是否已經執行過了。如果它的值等于1,就表示已經執行過了,否則的話,就表示還沒在等待執行。前面第一次調用View類的成員函數invalidate來檢查當前視圖上次請求的UI繪制操作是否已經執行時,如果發現已經執行了,那么就會重新請求執行一次新的UI繪制操作,這時候會導致當前視圖的成員變量mPrivateFlags的DRAWN位重置為0。注意,新請求執行的UI繪制只是為了在修改當前視圖的大小以及大小之前,先將它在上一次設置的大小以及位置中繪制出來,這樣就可以使得當前視圖的大小以及位置出現平滑的變換。換句話說,新請求執行的UI繪制只是為了獲得一個中間效果,它不應該影響當前視圖的繪制狀態,即不可以修改當前視圖的成員變量mPrivateFlags的DRAWN位。因此,我們就需要在前面第一次調用View類的成員函數invalidate前,先將當前視圖的成員變量mPrivateFlags的DRAWN位保存下來,即保存在變量drawn中,然后等到調用之后,再將變量drawn的值恢復到當前視圖的成員變量mPrivateFlags的DRAWN位中去。 另一方面,如果當前視圖的大小和位置發生了變化,View類的成員函數setFrame還會將成員變量mBackgroundSizeChanged的值設置為true,以便可以表示當前視圖的背景大小發生了變化。 最后,View類的成員函數setFrame將變量changed的值返回給調用者,以便調用者可以知道當前視圖的大小和位置是否發生了變化。 接下來,我們繼續分析View類的成員函數invalidate的實現,以便可以了解當前視圖是如何執行一次UI繪制操作的。 **Step 3. View.invalidate** ~~~ public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource { ...... protected ViewParent mParent; ...... int mPrivateFlags; ...... public void invalidate() { ...... if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)) { mPrivateFlags &= ~DRAWN & ~DRAWING_CACHE_VALID; final ViewParent p = mParent; final AttachInfo ai = mAttachInfo; if (p != null && ai != null) { final Rect r = ai.mTmpInvalRect; r.set(0, 0, mRight - mLeft, mBottom - mTop); // Don't call invalidate -- we don't want to internally scroll // our own bounds p.invalidateChild(this, r); } } } ...... } ~~~ 這個函數定義在文件frameworks/base/core/java/android/view/View.java中。 View類的成員函數invalidate首先檢查成員變量mPrivateFlags的DRAWN位和HAS_BOUNDS位是否都被設置為1。如果是的話,那么就說明當前視圖上一次請求執行的UI繪制操作已經執行完成了,這時候View類的成員函數invalidate才可以請求執行新的UI繪制操作。 View類的成員函數invalidate在請求新的UI繪制操作之前,會將成員變量mPrivateFlags的DRAWN位和DRAWING_CACHE_VALID位重置為0,其中,后者表示當前視圖正在緩存的一些繪圖對象已經失效了,這是因為接下來就要重新開始繪制當前視圖的UI了。 請求繪制當前視圖的UI是通過調用View類的成員變量mParent所描述的一個ViewParent接口的成員函數invalidateChild來實現的。前面我們假設當前視圖是應用程序窗口的頂層視圖,即它是一個類型為DecoreView的視圖,它的成員變量mParent指向的是與其所關聯的一個ViewRoot對象。因此,繪制當前視圖的UI的操作實際上是通過調用ViewRoot類的成員函數invalidateChild來實現的。 注意,在調用ViewRoot類的成員函數invalidateChild的成員函數invalidateChild來繪制當前視圖的UI之前,會將當前視圖即將要繪制的區域記錄在View類的成員變量mAttachInfo所描述的一個AttachInfo對象的成員變量mTmpInvalRect中。 接下來,我們就繼續分析ViewRoot類的成員函數invalidateChild的實現。 **Step 4. ViewRoot.invalidateChild** ~~~ public final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Callbacks { ...... public void invalidateChild(View child, Rect dirty) { checkThread(); ...... if (mCurScrollY != 0 || mTranslator != null) { mTempRect.set(dirty); dirty = mTempRect; if (mCurScrollY != 0) { dirty.offset(0, -mCurScrollY); } if (mTranslator != null) { mTranslator.translateRectInAppWindowToScreen(dirty); } if (mAttachInfo.mScalingRequired) { dirty.inset(-1, -1); } } mDirty.union(dirty); if (!mWillDrawSoon) { scheduleTraversals(); } } ...... } ~~~ 這個函數定義在文件frameworks/base/core/java/android/view/ViewRoot.java中。 ViewRoot類的成員函數invalidateChild首先調用另外一個成員函數checkThread來檢查當前正在執行的是否是一個UI線程。如果不是的話,ViewRoot類的成員函數checkThread就會拋出一個異常出來。這是因為所有的UI操作都必須要在UI線程中執行。 ViewRoot類的成員函數invalidateChild接下來還會檢查當前正在處理的應用程序窗口在Y軸上是否出現有滾動條,即成員變量mCurScrollY的值不等于0, 或者前正在處理的應用程序窗口是否運行在兼容模式之下,即成員變量mTranslator的值不等于null。當一個應用程序窗口運行在兼容模式時,它顯示出來的大小和它實際被設置的大小是不一樣的,要經過相應的轉換處理。對于上述這兩種情況,ViewRoot類的成員函數invalidateChild都需要調整參數dirty所描述的一個需要重新繪制的區域的大小和位置。 調整好參數dirty所描述的一個需要重新繪制的區域之后, ViewRoot類的成員函數invalidateChild就將它所描述的一個區域與成員變量mDirty所描述的一區域執行一個合并操作,并且將得到的新區域保存在成員變量mDirty中。從這個操作就可以看出,ViewRoot類的成員變量mDirty描述的就是當前正在處理的應用程序窗口下一次所要重新繪制的總區域。 設置好當前正在處理的應用程序窗口下一次所要重新繪制的總區域之后,ViewRoot類的成員函數invalidateChild最后就檢查成員變量mWillDrawSoon的值是否不等于true。如果ViewRoot類的成員mWillDrawSoon的值等于true的話,那么就說明UI線程的消息隊列中已經有一個DO_TRAVERSAL消息在等待執行了,這時候就不需要調用ViewRoot類的成員函數scheduleTraversals來向UI線程的消息隊列發送一個DO_TRAVERSAL消息了,否則的話,就需要調用ViewRoot類的成員函數scheduleTraversals來向UI線程的消息隊列發送一個DO_TRAVERSAL消息。 ViewRoot類的成員函數scheduleTraversals在前面Android應用程序窗口(Activity)的繪圖表面(Surface)的創建過程分析一文中已經分析過了,這里不再詳述。 這一步執行完成之后,返回到前面的Step 1中,即View類的成員函數layout中,接下來它就會調用另外一個成員函數onLayout來重新布局當前視圖的子視圖的布局了。View類的成員函數onLayout是由子類來重寫的,并且只有當該子類描述的是一個容器視圖時,它才會重寫父類View的成員函數onLayout。前面我們已經假設當前正在處理的是應用程序窗口的頂層視圖,它的類型為DecorView,并且它描述的是一個容器視圖,因此,接下來我們就會繼續分析DecorView類的成員函數onLayout的實現。 事實上,DecorView類是通過FrameLayout類來間接繼承View類的,并且它的成員函數onLayout是從FrameLayout類繼承下來的,因此,接下來我們實際上要分析的是FrameLayout類的成員函數onLayout的實現。 **Step 5. FrameLayout.onLayout** ~~~ public class FrameLayout extends ViewGroup { ...... protected void onLayout(boolean changed, int left, int top, int right, int bottom) { final int count = getChildCount(); final int parentLeft = mPaddingLeft + mForegroundPaddingLeft; final int parentRight = right - left - mPaddingRight - mForegroundPaddingRight; final int parentTop = mPaddingTop + mForegroundPaddingTop; final int parentBottom = bottom - top - mPaddingBottom - mForegroundPaddingBottom; mForegroundBoundsChanged = true; for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); final int width = child.getMeasuredWidth(); final int height = child.getMeasuredHeight(); int childLeft = parentLeft; int childTop = parentTop; final int gravity = lp.gravity; if (gravity != -1) { final int horizontalGravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK; final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; switch (horizontalGravity) { case Gravity.LEFT: childLeft = parentLeft + lp.leftMargin; break; case Gravity.CENTER_HORIZONTAL: childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + lp.leftMargin - lp.rightMargin; break; case Gravity.RIGHT: childLeft = parentRight - width - lp.rightMargin; break; default: childLeft = parentLeft + lp.leftMargin; } switch (verticalGravity) { case Gravity.TOP: childTop = parentTop + lp.topMargin; break; case Gravity.CENTER_VERTICAL: childTop = parentTop + (parentBottom - parentTop - height) / 2 + lp.topMargin - lp.bottomMargin; break; case Gravity.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中。 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應用程序窗口的繪制過程。 3. Android應用程序窗口的繪制過程 ViewRoot類的成員函數draw首先會創建一塊畫布,接著再在畫布上繪制Android應用程序窗口的UI,最后再將畫布的內容交給SurfaceFlinger服務來渲染,這個過程如圖4所示: :-: ![](https://box.kancloud.cn/deb309efb4fe81b61d02d959e4f461e8_1016x1107.jpg) 圖4 Android應用程序窗口的繪制過程 這個過程可以分為14個步驟,接下來我們就詳細分析每一個步驟。 **Step 1. ViewRoot.draw** ~~~ public final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Callbacks { ...... private void draw(boolean fullRedrawNeeded) { Surface surface = mSurface; ...... int yoff; final boolean scrolling = mScroller != null && mScroller.computeScrollOffset(); if (scrolling) { yoff = mScroller.getCurrY(); } else { yoff = mScrollY; } if (mCurScrollY != yoff) { mCurScrollY = yoff; fullRedrawNeeded = true; } float appScale = mAttachInfo.mApplicationScale; boolean scalingRequired = mAttachInfo.mScalingRequired; Rect dirty = mDirty; ...... if (mUseGL) { if (!dirty.isEmpty()) { Canvas canvas = mGlCanvas; if (mGL != null && canvas != null) { ...... int saveCount = 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; ...... int saveCount = 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中。 ViewRoot類的成員函數draw的執行流程如下所示: 1. 將成員變量mSurface所描述的應用程序窗口的繪圖表面保存在變量surface中,以便接下來可以通過變量surface來操作應用程序窗口的繪圖表面。 2. 調用成員變量mScroller所描述的一個Scroller對象的成員函數computeScrollOffset來計算應用程序窗口是否處于正在滾動的狀態中。如果是的話,那么得到的變量scrolling就會等于true,這時候調用成員變量mScroller所描述的一個Scroller對象的成員函數getCurrY就可以得到應用程序窗口在Y軸上的即時滾動位置yoff。 3. 成員變量mScrollY用來描述應用程序窗口下一次繪制時在Y軸上應該滾動到的位置,因此,如果應用程序窗口不是處于正在滾動的狀態,那么它在下一次繪制時,就應該直接將它在Y軸上的即時滾動位置yoff設置為mScrollY。 4. 成員變量mCurScrollY用來描述應用程序窗口上一次繪制時在Y軸上的滾動位置,如果它的值不等變量yoff的值,那么就表示應用程序窗口在Y軸上的滾動位置發生變化了,這時候就需要將變量yoff的值保存在成員變量mCurScrollY中,并且將參數fullRedrawNeeded的設置為true,表示要重新繪制應用程序窗口的所有區域。 5. 成員變量mAttachInfo所描述的一個AttachInfo對象的成員變量mScalingRequired表示應用程序窗口是否正在請求進行大小縮放,如果是的話,那么所請求的大小縮放因子就保存在這個AttachInfo對象的另外一個成員變量mApplicationScale中。函數將這兩個值保存在變量scalingRequired和appScale中,以便接下來可以使用。 6. 成員變量mDirty描述的是一個矩形區域,表示應用程序窗口的臟區域,即需要重新繪制的區域。函數將這個臟區域保存變量dirty中,以便接下來可以使用。 7. 成員變量mUseGL用來描述應用程序窗口是否直接使用OpenGL接口來繪制UI。當應用程序窗口的繪圖表面的內存類型等于WindowManager.LayoutParams.MEMORY_TYPE_GPU時,那么就表示它需要使用OpenGL接口來繪制UI,以便可以利用GPU來繪制UI。當應用程序窗口需要直接使用OpenGL接口來繪制UI時,另外一個成員變量mGlCanvas就表示應用程序窗口的繪圖表面所使用的畫布,這塊畫布同樣是通過OpenGL接口來創建的。 8. 當應用程序窗口需要直接使用OpenGL接口來繪制UI時,函數接下來就會將它的UI繪制在成員變量mGlCanvas所描述的一塊畫布上,這是通過調用成員變量mView所描述的一個類型為DecorView的頂層視圖的成員函數draw來實現的。注意,在繪制之前,還需要對畫布進行適當的轉換:A. 設置畫布在Y軸上的偏移值yoff,以便可以正確反映應用程序窗口的滾動狀態;B. 如果成員變量mTranslator的值不等于null,即它指向了一個Translator對象,那么就說明應用程序窗口運行在兼容模式下,這時候就需要相應對畫布進行變換,以便可以正確反映應用程序窗口的大小;C. 當變量scalingRequired的值等于true時,同樣說明應用程序窗口是運行在兼容模式下,這時候就需要修改畫布在兼容模式下的點密度,以便可以正確地反映應用程序窗口的分辨率,注意,這時候屏幕在兼容模式下的點密度保存在DisplayMetrics類的靜態成員變量DENSITY_DEVICE中。由于上述畫布的轉換操作只針對當前的這一次繪制操作有效,因此,函數就需要在繪制之后,調用畫布的成員函數save來保存它在轉換前的矩陣變換堆棧狀態,以便在繪制完成之后,可以調用畫布的成員函數restoreToCount來恢復之前的矩陣變換堆棧狀態。 9. 使用OpenGL接口來繪制完成UI后,如果變量scrolling的值等于true,即應用程序窗口是處于正在滾動的狀態,那么就意味著應用程序窗口接下來還需要馬上進行下一次重繪,而且是所有的區域都需要重繪,因此,函數接下來就會將成員變量mFullRedrawNeeded的值設置為true,并且調用另外一個成員函數scheduleTraversals來請求執行下一次的重繪操作。 10. 以下的步驟針適用于使用非OpenGL接口來繪制UI的情況,也是本文所要關注的重點。 11. 參數fullRedrawNeeded用來描述是否需要繪制應用程序窗口的所有區域。如果需要的話,那么就會將應用程序窗口的臟區域的大小設置為整個應用程序窗口的大小(0,0,mWidth,mHeight),其中,成員變量mWidth和mHeight表示應用程序窗口的寬度和高度。注意,如果應用程序窗口的大小被設置了一個縮放因子,即變量appScale的值不等于1,那么就需要將應用程序窗口的寬度mWidth和高度mHeight乘以這個縮放因子,然后才可以得到應用程序窗口的實際大小。 12. 經過前面的一系列計算之后,如果應用程序窗口的臟區域dirty不等于空,或者應用程序窗口在正處于動畫狀態,即成員變量mIsAnimating的值等于true,那么函數接下來就需要重新繪制應用程序窗口的UI了。在繪制之前,首先會調用用來描述應用程序窗口的繪圖表面的一個Surface對象surface的成員函數lockCanvas來創建一塊畫布canvas。有了這塊畫布之后,接下來就可以調用成員變量mView所描述的一個類型為DecorView的頂層視圖的成員函數draw來在上面繪制應用程序窗口的UI了。 與前面的第8步一樣,在繪制之前,還需要對畫布進行適當的A、B和C轉換,以及需要在繪制之后恢復畫布在繪制之前的矩陣變換堆棧狀態。 13. 繪制完成之后,應用程序窗口的UI就都體現在前面所創建的畫布canvas上了,因此,這時候就需要將它交給SurfaceFlinger服務來渲染,這是通過調用用來描述應用程序窗口的繪圖表面的一個Surface對象surface的成員函數unlockCanvasAndPost來實現的。 14. 在請求SurfaceFlinger服務渲染應用程序窗口的UI之后,函數同樣是需要判斷變量scrolling的值是否等于true。如果等于的話,那么就與前面的第9步一樣,函數需要將成員變量mFullRedrawNeeded的值設置為true,并且調用另外一個成員函數scheduleTraversals來請求執行下一次的重繪操作。 在本文中,我們只關注使用非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的實現。 **Step 2. Surface.lockCanvas** ~~~ public class Surface implements Parcelable { ...... public Canvas lockCanvas(Rect dirty) throws OutOfResourcesException, IllegalArgumentException { /* the dirty rectangle may be expanded to the surface's size, if * for instance it has been resized or if the bits were lost, since * the last call. */ return lockCanvasNative(dirty); } private native Canvas lockCanvasNative(Rect dirty); ...... } ~~~ 這個函數定義在文件frameworks/base/core/java/android/view/Surface.java中。 Surface類的成員函數lockCanvas調用另外一個成員函數lockCanvasNative來創建一塊畫布。Surface類的成員函數lockCanvasNative是一個JNI方法,它是由C++層的函數Surface_lockCanvas來實現的,如下所示: ~~~ static jobject Surface_lockCanvas(JNIEnv* env, jobject clazz, jobject dirtyRect) { const sp<Surface>& surface(getSurface(env, clazz)); ...... // get dirty region Region dirtyRegion; if (dirtyRect) { Rect dirty; dirty.left = env->GetIntField(dirtyRect, ro.l); dirty.top = env->GetIntField(dirtyRect, ro.t); dirty.right = env->GetIntField(dirtyRect, ro.r); dirty.bottom= env->GetIntField(dirtyRect, ro.b); if (!dirty.isEmpty()) { dirtyRegion.set(dirty); } } ...... Surface::SurfaceInfo info; status_t err = surface->lock(&info, &dirtyRegion); ...... // Associate a SkCanvas object to this surface jobject canvas = env->GetObjectField(clazz, so.canvas); ...... SkCanvas* nativeCanvas = (SkCanvas*)env->GetIntField(canvas, no.native_canvas); SkBitmap bitmap; ...... if (info.w > 0 && info.h > 0) { bitmap.setPixels(info.bits); } else { // be safe with an empty bitmap. bitmap.setPixels(NULL); } ...... SkRegion clipReg; if (dirtyRegion.isRect()) { // very common case const Rect b(dirtyRegion.getBounds()); clipReg.setRect(b.left, b.top, b.right, b.bottom); } else { size_t count; Rect const* r = dirtyRegion.getArray(&count); while (count) { clipReg.op(r->left, r->top, r->right, r->bottom, SkRegion::kUnion_Op); r++, count--; } } nativeCanvas->clipRegion(clipReg); int saveCount = nativeCanvas->save(); env->SetIntField(clazz, so.saveCount, saveCount); if (dirtyRect) { const Rect& bounds(dirtyRegion.getBounds()); env->SetIntField(dirtyRect, ro.l, bounds.left); env->SetIntField(dirtyRect, ro.t, bounds.top); env->SetIntField(dirtyRect, ro.r, bounds.right); env->SetIntField(dirtyRect, ro.b, bounds.bottom); } return canvas; } ~~~ 這個函數定義在文件frameworks/base/core/jni/android_view_Surface.cpp中。 參數clazz指向的是一個Java層的Surface對象。從前面Android應用程序窗口(Activity)的繪圖表面(Surface)的創建過程分析一文可以知道,每一個Java層的Surface對象在C++層都對應有一個Surface對象。因此,函數首先調用另外一個函數getSurface來獲得與參數clazz所對應的C++層的Surface對象surface。 參數dirtyRect指向的是一個Java層的Rect對象,它描述的是應用程序窗口即將要重繪的一塊矩形區域,函數接下來就將它所描述的矩形區域轉換成一個C++層的Region對象dirtyRegion來表示。 函數接下來就調用前面所獲得的C++層的Surface對象surface的成員函數lock來獲得一個圖形緩沖區,這個圖形緩沖區使用一個SurfaceInfo對象info來描述,其中,圖形緩沖區的地址就保存在它的成員變量bits中。 獲得圖形緩沖區之后,我們就可以在上面繪制應用程序窗口的UI了。由于Java層的應用程序窗口是通Skia圖形庫來繪制應用程序窗口的UI的,而Skia圖形庫在繪制UI時,是需要一塊畫布的,因此,函數接下來就會將前面所獲得的圖形緩沖區封裝在一塊畫布中。 從前面Android應用程序窗口(Activity)的繪圖表面(Surface)的創建過程分析一文還可以知道,每一個Java層的Surface對象內部都有一塊畫布,這塊畫布是通過它的成員變量mCanvas所指向的一個Java層的CompatibleCanvas對象來描述的。so是一個類型為so_t的結構體,它的成員變量canvas描述的是Java層的Surface類的成員變量mCanva在類中的偏移量,因此,通過這個偏移量就可以獲得參數clazz所指向的一個Java層的Surface對象的內部的一塊類型為CompatibleCanvas的畫布canvas。 畫布canvas的類型為Java層的CompatibleCanvas,它是從Canvas類繼承下來的。Canvas類有一個成員變量mNativeCanvas,它指向的是一個C++層的SkCanvas對象,這個C++層的SkCanvas對象描述的就是Skia圖形庫繪制應用程序窗口UI時所需要的畫布。no是一個類型為no_t的結構體,它的成員變量native_canvas描述的是Java層的Canvas類的成員變量mNativeCanvas在類中的偏移量,因此,通過這個偏移量就可以獲得變量canvas所指向的一個Java層的CompatibleCanvas對象的內部的一塊類型為SkCanvas的畫布nativeCanvas。 獲得了Skia圖形庫所需要的畫布nativeCanvas之后,函數就可以將前面所獲得的圖形緩沖區的地址,即SurfaceInfo對象info的成員變量bits封裝到它內部去了,這是通過調用它的成員函數setPixels來實現的。 函數接下來還會設置畫布nativeCanvas的裁剪區域。這個裁剪區域是通過Region對象dirtyRegion來描述的,不過Skia圖形庫需要使用另外一個類型為SkRegion的對象clipReg來描述它。Region對象dirtyRegion所描述的區域有可能是一個矩形區域,也可能是一個不規則的區域。如果Region對象dirtyRegion描述的是一個矩形區域,那么就可以直接將這個矩形區域設置到SkRegion的對象clipReg里面去。如果Region對象dirtyRegion描述的是一個不規則區域,那么這個不規則區域就是由一系列的矩形小區域來描述的,這時候就將這些矩形小區域合并起來,并且設置到kRegion的對象clipReg里面去。 設置好SkRegion的對象clipReg所包含的區域之后,函數就可以調用前面得到的SkCanvas畫布nativeCanvas的成員函數clipRegion來將它設置為自己的裁剪區域了,接下來函數還會將該裁剪區域所圍成的一個矩形區域的位置和大小設置到參數dirtyRect所描述的一個Java層的Rect對象中去,以便調用者可以知道現在正在創建的畫布的大小。 函數在將與C++層的SkCanvas畫布nativeCanvas所關聯的一個Java層的CompatibleCanvas畫布canvas返回給調用者之前,還會將畫布的當前堆棧狀態保存下來,以便在繪制完成應用程序窗口的UI之后,可以恢復回來,這是通過調用C++層的SkCanvas畫布nativeCanvas的成員函數save來實現的。畫布的當前堆棧狀態是通過一個整數來描述的,這個整數即為C++層的SkCanvas畫布nativeCanvas的成員函數save的返回值saveCount,它會被保存在參數clazz所描述的一個Java層的Surface對象的成員變量mSaveCount中,等到應用程序窗口的UI繪制完成之后,就可以通過這個整數來恢復畫布的堆棧狀態了。 接下來,我們繼續分析C++層的Surface類的成員函數lock的實現,以便可以了解用來創建繪制應用程序窗口UI所需要的畫布的圖形緩沖區是如何獲得的。 **Step 3. Surface.lock** ~~~ status_t Surface::lock(SurfaceInfo* other, Region* dirtyIn, bool blocking) { ...... if (mApiLock.tryLock() != NO_ERROR) { ...... return WOULD_BLOCK; } /* Here we're holding mApiLock */ if (mLockedBuffer != 0) { ...... mApiLock.unlock(); return INVALID_OPERATION; } // we're intending to do software rendering from this point setUsage(GRALLOC_USAGE_SW_READ_OFTEN | GRALLOC_USAGE_SW_WRITE_OFTEN); android_native_buffer_t* out; status_t err = dequeueBuffer(&out); ...... if (err == NO_ERROR) { sp<GraphicBuffer> backBuffer(GraphicBuffer::getSelf(out)); err = lockBuffer(backBuffer.get()); ...... if (err == NO_ERROR) { const Rect bounds(backBuffer->width, backBuffer->height); const Region boundsRegion(bounds); Region scratch(boundsRegion); Region& newDirtyRegion(dirtyIn ? *dirtyIn : scratch); newDirtyRegion &= boundsRegion; // figure out if we can copy the frontbuffer back const sp<GraphicBuffer>& frontBuffer(mPostedBuffer); const bool canCopyBack = (frontBuffer != 0 && backBuffer->width == frontBuffer->width && backBuffer->height == frontBuffer->height && backBuffer->format == frontBuffer->format && !(mFlags & ISurfaceComposer::eDestroyBackbuffer)); ...... if (canCopyBack) { // copy the area that is invalid and not repainted this round const Region copyback(mOldDirtyRegion.subtract(newDirtyRegion)); if (!copyback.isEmpty()) copyBlt(backBuffer, frontBuffer, copyback); } else { // if we can't copy-back anything, modify the user's dirty // region to make sure they redraw the whole buffer newDirtyRegion = boundsRegion; } ...... void* vaddr; status_t res = backBuffer->lock( GRALLOC_USAGE_SW_READ_OFTEN | GRALLOC_USAGE_SW_WRITE_OFTEN, newDirtyRegion.bounds(), &vaddr); ...... mLockedBuffer = backBuffer; other->w = backBuffer->width; other->h = backBuffer->height; other->s = backBuffer->stride; other->usage = backBuffer->usage; other->format = backBuffer->format; other->bits = vaddr; } } mApiLock.unlock(); return err; } ~~~ 這個函數定義在文件frameworks/base/libs/surfaceflinger_client/Surface.cpp中。 Surface類的成員變量mApiLock是一個類型為Mutex的互斥鎖,它是用來保證Surface類的成員函數lock是線程安全的。如果調用Surface類的成員變量mApiLock所描述的一個Mutex對象的成員函數tryLock的返回值不等于NO_ERROR,那么就說明這個Mutex對象已經被另外一個線程獲得了,因此,這時候函數就直接返回一個錯誤碼WOULD_BLOCK給調用者了。 Surface類的成員變量mLockedBuffer的類型為GraphicBuffer,如果它的值不等于0,那么它指向的就是應用程序窗口當前正在使用的圖形緩沖區。如果應用程序窗口正在使用一個圖形緩沖區,那么它是不可以再請求分配另一個圖形緩沖區的,因此,當Surface類的成員變量mLockedBuffer的值不等于0時,函數就直接返回一個錯誤碼INVALID_OPERATION給調用者了。 通過了前面的檢查之后,Surface類的成員函數lock接下來就開始要分配一個圖形緩沖區了,不過在分配之后,首先調用另外一個成員函數setUsage來將當前正在處理的Surface對象所描述的應用程序窗口的繪圖表面的屬性設置為(GRALLOC_USAGE_SW_READ_OFTEN | GRALLOC_USAGE_SW_WRITE_OFTEN),表示該應用程序窗口的UI是需要通過軟件方式來渲染的,這是相對于使用GPU來渲染而言的。 Surface類的成員函數lock接下來就調用另外一個成員函數dequeueBuffer來獲得一個新的圖形緩沖區了,這個新的圖形緩沖區使用一個android_native_buffer_t對象out來描述的。在前面Android應用程序請求SurfaceFlinger服務渲染Surface的過程分析一文中,我們已經分析過Surface類的成員函數dequeueBuffer的實現了,它主要就是請求SurfaceFlinger服務來分配一個圖形緩沖區。 前面獲得的android_native_buffer_t對象out接下來又會被封裝成一個GraphicBuffer對象backBuffer,這樣,Surface類的成員函數lock接下來就會通過GraphicBuffer對象backBuffer來訪問前面所獲得的圖形緩沖區。 Surface類是使用一種稱為雙緩沖的技術來渲染應用程序窗口的UI的。這種雙緩沖技術需要兩個圖形緩沖區,其中一個稱為前端緩沖區,另外一個稱為后端緩沖區。前端緩沖區是正在渲染的圖形緩沖區,而后端緩沖區是接下來要渲染的圖形緩沖區,它們分別通過Surface類的成員變量mPostedBuffer和mLockedBuffer所指向的兩個GraphicBuffer對象來描述。前面所獲得的圖形緩沖區backBuffer是作為后端緩沖區來使用的,即接下來它所指向的圖形緩沖區也需要保存在Surface類的成員變量mLockedBuffer中。 在將圖形緩沖區backBuffer返回給調用者之前,Surface類的成員函數lock還需要對它進行進一步的處理,即判斷是否需要前端緩沖區mPostedBuffer的內容拷貝回它里面去,以便可以支持部分更新應用程序窗口UI的功能。在滿足以下三個條件下,Surface類的成員函數lock可以將前端緩沖區的內容拷貝到后端緩沖區中去: 1. 前端緩沖區的內容拷貝到后端緩沖區所描述的區域的寬度和高度相同。 2. 前端緩沖區和后端緩沖區的像素格式相同。 3. 應用程序窗口繪圖表面的屬性值mFlags的ISurfaceComposer::eDestroyBackbuffer位等于0,即在渲染了應用程序窗口的UI之后,應該保留正在渲染的圖形緩沖區的內容。 如果能將前端緩沖區的內容拷貝到后端緩沖區中去,那么就不用重新繪制應用程序窗口的所有區域,而只需要繪制那些臟的區域,即Region對象newDirtyRegion所描述的區域。注意,參數dirtyIn所描述的區域是原先指定的臟區域,但是在分配了新的后端緩沖區backBuffer之后,我們需要將新的圖形緩沖區backBuffer所描述的區域boundsRegion與原先指定的臟區域作一個與操作,得到才是最后需要重繪的臟區域newDirtyRegion。由于在這種情況下,我們只在后端緩沖區backBuffer繪制中繪制應用程序窗口的臟區域,因此,就需要將那些干凈的區域從前端緩沖區frontBuffer拷貝到圖形緩沖區backBuffer的對應位置去,這是通過調用函數copyBlt來實現的。應用程序窗口的干凈區域使用Region對象copyback來描述,它是從應用程序窗口上一次所重繪的區域減去接下來需要重繪的臟區域newDirtyRegion得到的,而應用程序窗口上一次所重繪的區域是保存在Surface類的成員變量mOldDirtyRegion中的。 如果不能將前端緩沖區的內容拷貝到后端緩沖區中去,那么接下來就需要重新繪制應用程序窗口的所有區域了,這時候應用程序窗口的臟區域newDirtyRegion就會被修改為后端緩沖區backBuffer所描述的區域boundsRegion。 Surface類的成員函數lock處理完成前后端緩沖區的拷貝問題之后,接下來就會調用后端緩沖區backBuffer所指向的一個GraphicBuffer對象的成員函數lock來獲得它的地址vaddr,以便接下來保存在參數other所描述的一個SurfaceInfo對象的成員變量bits中,這樣調用者就獲得后端緩沖區backBuffer的地址值了。注意,同時保存在SurfaceInfo對象中的信息還包括后端緩沖區backBuffer的寬度width、高度height、每行像素點stride、用途usage和像素格式format。 Surface類的成員函數lock還會將接下來要重繪的臟緩沖區newDirtyRegion保存在Surface類的成員變量mOldDirtyRegion中,以便再下一次為應用程序窗口分配圖形緩沖區時,可以知道應用程序窗口的上一次重繪區域,即上一次干凈區域。 此外,Surface類的成員函數lock還會將后端緩沖區backBuffer保存在Surface類的成員變量mLockedBuffer,這樣就可以知道應用程序窗口當前正在使用的圖形緩沖區,即下一次要請求SurfaceFlinger服務渲染的圖形緩沖區。 最后,Surface類的成員函數lock首先調用成員變量mApiLock所指向的一個Mutex對象的成員函數unlock,以便中可以釋放前面所獲得的鎖,然后再返回到上一步去。 接下來,我們繼續分析GraphicBuffer類的成員函數lock的實現,以便可以了解一個圖形緩沖區的地址是如何獲得的。 **Step 4. GraphicBuffer.lock** ~~~ status_t GraphicBuffer::lock(uint32_t usage, const Rect& rect, void** vaddr) { ...... status_t res = getBufferMapper().lock(handle, usage, rect, vaddr); return res; } ~~~ 這個函數定義在文件frameworks/base/libs/ui/GraphicBuffer.cpp中。 GraphicBuffer類的成員變量handle是從父類android_native_buffer_t繼承下來的,它的類型為buffer_handle_t,用來作為一個圖形緩沖區的句柄,這個知識點可以參考前面Android應用程序請求SurfaceFlinger服務渲染Surface的過程分析一文。 GraphicBuffer類的成員函數lockGraphicBuffer首先調用另外一個成員函數getBufferMapper來獲得一個GraphicBufferMapper對象,然后再調用這個GraphicBufferMapper對象的成員函數lock來獲得成員變量handle所描述的一個圖形緩沖區的地址,并且保存在輸出參數vaddr中。 接下來,我們就繼續分析GraphicBufferMapper類的成員函數lock的實現。 **Step 5. GraphicBufferMapper.lock** ~~~ status_t GraphicBufferMapper::lock(buffer_handle_t handle, int usage, const Rect& bounds, void** vaddr) { status_t err; if (sw_gralloc_handle_t::validate(handle) < 0) { err = mAllocMod->lock(mAllocMod, handle, usage, bounds.left, bounds.top, bounds.width(), bounds.height(), vaddr); } else { err = sw_gralloc_handle_t::lock((sw_gralloc_handle_t*)handle, usage, bounds.left, bounds.top, bounds.width(), bounds.height(), vaddr); } LOGW_IF(err, "lock(...) failed %d (%s)", err, strerror(-err)); return err; } ~~~ 這個函數定義在文件frameworks/base/libs/ui/GraphicBufferMapper.cpp。 GraphicBufferMapper類的成員函數lock首先調用sw_gralloc_handle_t類的靜態成員函數validate來驗證參數andle所描述的一個圖形緩沖區是否是在w_gralloc_handle_t模塊中分配的。如果是的話,那么就需要調用sw_gralloc_handle_t類的靜態成員函數lock來獲得參數andle所描述的一個圖形緩沖區的地址,否則的話,就需要調用GraphicBufferMapper類的成員變量mAllocMod所描述的一個HAL模塊Gralloc的成員函數lock來獲得參數andle所描述的一個圖形緩沖區的地址。 從前面Android應用程序請求SurfaceFlinger服務渲染Surface的過程分析一文可以知道,應用程序窗口所使用的圖形緩沖區一般都是在HAL模塊Gralloc中分配的,因此,GraphicBufferMapper類的成員函數lock接下來就會調用成員變量mAllocMod所描述的一個HAL模塊Gralloc的成員函數lock來獲得參數andle所描述的一個圖形緩沖區的地址,并且保存在輸出參數vaddr中。HAL模塊Gralloc的成員函數lock的實現可以參考前面Android幀緩沖區(Frame Buffer)硬件抽象層(HAL)模塊Gralloc的實現原理分析一文,這里不再詳述。 這一步執行完成之后,返回到前面的Step 1中,即ViewRoot類的成員函數draw中,接下來就會繼續調用其成員變量mView所描述的一個DecorView對象的成員函數draw來在前面所獲得一塊畫布上面繪制應用程序窗口的UI。 **Step 6. DecorView.draw** ~~~ public class PhoneWindow extends Window implements MenuBuilder.Callback { ...... private final class DecorView extends FrameLayout implements RootViewSurfaceTaker { ...... private Drawable mMenuBackground; ...... @Override public void draw(Canvas canvas) { super.draw(canvas); if (mMenuBackground != null) { mMenuBackground.draw(canvas); } } ...... } ...... } ~~~ 這個函數定義在文件frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java中。 DecorView類的成員函數draw首先調用父類FrameLayout的成員函數draw來繪制它的UI內容,然后再檢查它是否設置了菜單背景,即成員變量mMenuBackground的值是否不等于null。如果不等于null的話,那么就會調用它所指向的一個Drawable對象的成員函數draw來在畫布canvas上繪制這個菜單背景。 接下來,我們就繼續分析FrameLayout的成員函數draw的實現,以便可以了解應用程序窗口UI的繪制過程。 **Step 7. FrameLayout.draw** ~~~ public class FrameLayout extends ViewGroup { ...... private Drawable mForeground; ...... public void draw(Canvas canvas) { super.draw(canvas); if (mForeground != null) { final Drawable foreground = mForeground; if (mForegroundBoundsChanged) { mForegroundBoundsChanged = false; final Rect selfBounds = mSelfBounds; final Rect overlayBounds = mOverlayBounds; final int w = mRight-mLeft; final int h = mBottom-mTop; if (mForegroundInPadding) { selfBounds.set(0, 0, w, h); } else { selfBounds.set(mPaddingLeft, mPaddingTop, w - mPaddingRight, h - mPaddingBottom); } Gravity.apply(mForegroundGravity, foreground.getIntrinsicWidth(), foreground.getIntrinsicHeight(), selfBounds, overlayBounds); foreground.setBounds(overlayBounds); } foreground.draw(canvas); } } ...... } ~~~ 這個函數定義在文件frameworks/base/core/java/android/widget/FrameLayout.java中。 FrameLayout類的成員函數draw首先調用父類View的成員函數draw來繪制它的UI內容,然后再檢查它是否設置了一個前景圖,即成員變量mForeground的值是否等于null。如果不等于null的話,那么就會先設置這個前景圖的大小和位置,然后再調用用成員變量mForeground所指向的一個Drawable對象的成員函數draw來在畫布canvas上繪制這個前景圖。 接下來,我們就繼續分析View類的成員函數draw的實現,以便可以了解應用程序窗口UI的繪制過程。 **Step 8. View.draw** 這個函數定義在文件frameworks/base/core/java/android/view/View.java中,它主要是完成以下六個操作: 1. 繪制當前視圖的背景。 2. 保存當前畫布的堆棧狀態,并且在在當前畫布上創建額外的圖層,以便接下來可以用來繪制當前視圖在滑動時的邊框漸變效果。 3. 繪制當前視圖的內容。 4. 繪制當前視圖的子視圖的內容。 5. 繪制當前視圖在滑動時的邊框漸變效果。 6. 繪制當前視圖的滾動條。 在上面六個操作中,有些是可以優化的。例如,如果當前視圖的某一個子視圖是不透明的,并且覆蓋了當前視圖的內容,那么當前視圖的背景以及內容就不會繪制了,即不用執行第1和第3個操作。又如,如果當前視圖不是處于滑動的狀態,那么第2和第5個操作也是不用執行的。 接下來我們就分段來閱讀View類的成員函數draw的代碼: ~~~ public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource { ...... public void draw(Canvas canvas) { ...... final int privateFlags = mPrivateFlags; final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN; ~~~ 這段代碼檢查View類的成員變量mPrivateFlags的DIRTY_OPAQUE位是否等于1。如果等于1的話,那么就說明當前視圖的某一個子視圖請求了一個不透明UI繪制操作。在這種情況下,當前視圖會被子視圖覆蓋,因此,就不需要執行前面所說的第1和第3個操作了。不過,不用執行第1和第3個操作還有一個前提,那就是View類的成員變量mAttachInfo所指向的一個AttachInfo對象的成員變量mIgnoreDirtyState的值等于false,這表示當前視圖不可以忽略成員變量mPrivateFlags的DIRTY_OPAQUE位。滿足了上述兩個條件之后,變量dirtyOpaque的值就會等于true。 View類的成員函數在繼續往下執行之前,還會將成員變量mPrivateFlags的DIRTY_MASK位重置為0,以及將DRAWN位設置為1,因為接下來就要開始繪制當前視圖的UI了。 我們繼續往下閱讀代碼: ~~~ // 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); } } } ~~~ 這段代碼用來執行上述的第1個操作,但是它只會變量dirtyOpaque的值等于false的情況下才會執行。當前視圖的背景是通過成員變量mBGDrawable所指向的一個Drawable對象來描述的。在繪制當前視圖的背景之前,還會先設置它的大小和位置。 我們繼續往下閱讀代碼: ~~~ // skip step 2 & 5 if possible (common case) 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; } ~~~ 這段代碼檢查是否可以跳過上述的第2和第5個操作。當View類的成員變量mViewFlags的FADING_EDGE_HORIZONTAL位等于1的時候,就說明當前視圖正在處于水平滑動狀態,并且需要繪制水平邊框的漸變效果。同樣,當View類的成員變量mViewFlags的FADING_EDGE_VERTICAL位等于1的時候,就說明當前視圖正在處于垂直滑動狀態,并且需要繪制垂直邊框的漸變效果。但是,如果當前視圖不是處于滑動狀態,即變量horizontalEdges和verticalEdges的值均等于false的時候,那么就不需要執行上述的第2和第5個操作了,而只需要執行第3、第4和第6個操作。注意,當變量dirtyOpaque的值等于true的時候,第3個操作也是不需要執行的。 我們繼續往下分析代碼: ~~~ boolean drawTop = false; boolean drawBottom = false; boolean drawLeft = false; boolean drawRight = false; float topFadeStrength = 0.0f; float bottomFadeStrength = 0.0f; float leftFadeStrength = 0.0f; float rightFadeStrength = 0.0f; // Step 2, save the canvas' layers int paddingLeft = mPaddingLeft; int paddingTop = mPaddingTop; final boolean offsetRequired = isPaddingOffsetRequired(); if (offsetRequired) { paddingLeft += getLeftPaddingOffset(); paddingTop += getTopPaddingOffset(); } int left = mScrollX + paddingLeft; int right = left + mRight - mLeft - mPaddingRight - paddingLeft; int top = mScrollY + paddingTop; int bottom = top + mBottom - mTop - mPaddingBottom - paddingTop; if (offsetRequired) { right += getRightPaddingOffset(); bottom += getBottomPaddingOffset(); } final ScrollabilityCache scrollabilityCache = mScrollCache; int length = scrollabilityCache.fadingEdgeLength; // clip the fade length if top and bottom fades overlap // overlapping fades produce odd-looking artifacts if (verticalEdges && (top + length > bottom - length)) { length = (bottom - top) / 2; } // also clip horizontal fades if necessary if (horizontalEdges && (left + length > right - length)) { length = (right - left) / 2; } if (verticalEdges) { topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength())); drawTop = topFadeStrength >= 0.0f; bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength())); drawBottom = bottomFadeStrength >= 0.0f; } if (horizontalEdges) { leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength())); drawLeft = leftFadeStrength >= 0.0f; rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength())); drawRight = rightFadeStrength >= 0.0f; } saveCount = canvas.getSaveCount(); int solidColor = getSolidColor(); if (solidColor == 0) { final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG; if (drawTop) { canvas.saveLayer(left, top, right, top + length, null, flags); } if (drawBottom) { canvas.saveLayer(left, bottom - length, right, bottom, null, flags); } if (drawLeft) { canvas.saveLayer(left, top, left + length, bottom, null, flags); } if (drawRight) { canvas.saveLayer(right - length, top, right, bottom, null, flags); } } else { scrollabilityCache.setFadeColor(solidColor); } ~~~ 這段代碼用來檢查是否需要保存參數canvas所描述的一塊畫布的堆棧狀態,并且創建額外的圖層來繪制當前視圖在滑動時的邊框漸變效果。視圖的邊框是繪制在內容區域的邊界位置上的,而視圖的內容區域是需要排除成員變量mPaddingLeft、mPaddingRight、mPaddingTop和mPaddingBottom所描述的視圖內邊距的。此外,視圖的邊框有四個,分別位于視圖的左、右、上以及下內邊界上。因此,這段代碼首先需要計算出當前視圖的左、右、上以及下內邊距的大小,以便得到邊框所要繪制的區域。 本來通過View類的成員變量mPaddingLeft、mPaddingRight、mPaddingTop和mPaddingBottom就可以得到當視圖的左、右、上以及下內邊距的大小的,但是有時候我們在定制一個視圖的時候,可能會需要在視圖的內邊距上繪制其它額外的東西,這時候就有擴展視圖的內邊距的需求。如果有擴展視圖的內邊距的需求,那么就需要重寫View類的成員函數isPaddingOffsetRequired,即將它的返回值設置為true,并且重載另外四個成員函數getLeftPaddingOffset、getRightPaddingOffset、getTopPaddingOffset和getBottomPaddingOffset來提供額外的左、右、上以及下內邊距。 這段代碼經過計算后,就得到四個值left、right、top和bottom,它們分別表示當前視圖可以用來繪制的內容區域,這個區域已經將內置的和擴展的內邊距排除之外。 計算好left、right、top和bottom這四個值之后,就相當于得到左、右、上以及下邊框的起始位置了,但是我還需要知道邊框的長度,才能確定左、右、上以及下邊框所要繪制的區域。 邊框的長度length設置在View類的成員變量mScrollCache所指向的一個ScrollabilityCache對象的成員變量fadingEdgeLength中。但是,這個預先設置的邊框長度length不一定適合當前視圖使用。這是因為視圖的大小是可以隨時改變的,一旦發生了改變之后,原先設置的邊框長度length可能就會顯得過長。具體來說,就是當上下兩個邊框或者左右兩個邊框發生重疊時,就說明原先設置的邊框長度過長了。在這種情況下,就要將邊框長度length修改為當前視圖的內容區域的高度和寬度的較小者的一半,以便可以保證上下兩個邊框或者左右兩個邊框不會發生重疊。 左、右、上以及下邊框都對應有一個強度值,強度值的取值范圍為[0.0, 1.0]。如果一個邊框的強度值等于0.0,那么它就是不可見的,這時候就不需要繪制它的漸變效果。另一方面,如果一個邊框的強度值等于1.0,那么它的長度等于原來設置的長度。我們可以將這個強度值理解為一個縮放因子。左、右、上以及下邊框的強度值可以分別通過調用View類的成員函數getLeftFadingEdgeStrength、getRightFadingEdgeStrength、getTopFadingEdgeStrength以及getBottomFadingEdgeStrength來獲得。注意,只有在變量verticalEdges的值等于true的時候,這段代碼才會計算上下兩個邊框的強度值topFadeStrength和bottomFadeStrength;同樣,只有變量horizontalEdges的值等于true的時候,這代碼才會計算左右兩個邊框的強度值leftFadeStrength和rightFadeStrength。 計算好左、右、上以及下邊框的強度值leftFadeStrength、rightFadeStrength、topFadeStrength以及bottomFadeStrength之后,這段代碼就會判斷它們的值是否大于0。如果大于0,那么與它們所對應的四個變量drawLeft、drawRight、drawTop以及drawBottom的值就會等于true,表示需要繪制左、右、上以及下四個邊框的漸變效果。 View類的成員函數getSolidColor返回的是當前視圖的背景顏色。如果當前視圖的背景顏色是純色的,即變量solidColor的值不等于0,那么這時候就會使用這個背景顏色來繪制邊框的漸變效果,即調用變量scrollabilityCache所指向的一個ScrollabilityCache對象的成員函數setFadeColor來將將邊框的漸變效果顏色設置為solidColor,這種情況是比較簡單的。如果當前視圖的背景顏色不是純色的,即變量solidColor的值等于0,這種情況就比較復雜了,我們需要創建在參數canvas所描述的一塊畫布上來創建額外的圖層來繪制邊框的漸變效果,這樣做是為了能夠使用背景顏色來繪制邊框的漸變效果。 參數canvas所描述的一塊畫布上來創建額外的圖層是通過調用它的成員函數saveLayer來實現的。我們注意到在調用參數canvas所指向的一個Canvas對象的成員函數saveLayer的時候,最后一個參數指定為Canvas.HAS_ALPHA_LAYER_SAVE_FLAG,這表示在將額外創建的圖層合成到參數canvas所描述的一塊畫布上去,要給額外創建的圖層設置一個透明度值。同時,我們還可以看出,當前視圖的左、右、上和下邊框所占據的范圍分別為(left, top, left + length, bottom)、(right - length, top, right, bottom)、(left, top, right, top + length)和(left, bottom - length, right, bottom)。還有另外一個地方需要注意的是,在參數canvas所描述的一塊畫布上來創建額外的圖層之前,這段代碼首先會獲得畫布的當前堆棧狀態,這是通過一個整數saveCount來描述的,并且這個整數是通過調用參數canvas所指向的一個Canvas對象的成員函數getSaveCount來獲得的。這樣,后面在額外創建的圖層上繪制了邊框的漸變效果之后,就可以通過前面得到的整數saveCount將恢復畫布的堆棧狀態,也就是將前面額外創建的圖層合成到畫布上來。 我們接著往下閱讀代碼: ~~~ // Step 3, draw the content if (!dirtyOpaque) onDraw(canvas); // Step 4, draw the children dispatchDraw(canvas); ~~~ 這段代碼用來執行上述的第3和第4個操作,即繪制當前視圖的內容,以及當前視圖的子視圖的內容,它們分別是通過調用View類的成員函數onDraw和dispatchDraw來實現的。本文主要關注當前視圖的子視圖的繪制過程,因此,在接下來的Step 9中,我們再詳細分析View類的成員函數dispatchDraw的實現。 我們接著往下閱讀代碼: ~~~ // Step 5, draw the fade effect and restore layers final Paint p = scrollabilityCache.paint; final Matrix matrix = scrollabilityCache.matrix; final Shader fade = scrollabilityCache.shader; final float fadeHeight = scrollabilityCache.fadingEdgeLength; if (drawTop) { matrix.setScale(1, fadeHeight * topFadeStrength); matrix.postTranslate(left, top); fade.setLocalMatrix(matrix); canvas.drawRect(left, top, right, top + length, p); } if (drawBottom) { matrix.setScale(1, fadeHeight * bottomFadeStrength); matrix.postRotate(180); matrix.postTranslate(left, bottom); fade.setLocalMatrix(matrix); canvas.drawRect(left, bottom - length, right, bottom, p); } if (drawLeft) { matrix.setScale(1, fadeHeight * leftFadeStrength); matrix.postRotate(-90); matrix.postTranslate(left, top); fade.setLocalMatrix(matrix); canvas.drawRect(left, top, left + length, bottom, p); } if (drawRight) { matrix.setScale(1, fadeHeight * rightFadeStrength); matrix.postRotate(90); matrix.postTranslate(right, top); fade.setLocalMatrix(matrix); canvas.drawRect(right - length, top, right, bottom, p); } canvas.restoreToCount(saveCount); ~~~ 這段代碼是用來繪制當前視圖的左、右、上以及下邊框的漸變效果。注意,只有左、右、上以及下邊框所對應的四個變量drawLeft、drawRight、drawTop以及drawBottom的值等于true時,左、右、上以及下邊框的漸變效果才需要繪制。同時,左、右、上以及下邊框在繪制的時候,都會被設置一個縮放因子,即前面計算得到的左、右、上以及下邊框的強度值leftFadeStrength、rightFadeStrength、topFadeStrength以及bottomFadeStrength。 由于當前視圖的左、右、上以及下邊框的漸變效果是在參數canvas所描述的一塊畫布的額外創建的圖層上繪制的,因此,在繪制完之后,這段代碼需要調用參數canvas所指向的一個Canvas對象來恢復參數canvas所描述的一塊畫布在創建額外圖層時的堆棧狀態,即相當于是將前面所繪制的邊框漸變效果合成到參數canvas所描述的一塊畫布來。 我們繼續往下閱讀最后一段代碼: ~~~ // Step 6, draw decorations (scrollbars) onDrawScrollBars(canvas); } ...... } ~~~ 這段代碼用來執行上述的第6個操作,即調用View類的成員函數onDrawScrollBars來繪制當前視圖的滾動條。 接下來,我們就主要關注當前視圖的子視圖的繪制過程,即View類的成員函數dispatchDraw的實現。注意,View類的成員函數dispatchDraw是一個空實現,它是由子類ViewGroup來重寫的,也就是說,只有當一個視圖描述的是一個視圖容器時,它才會重寫父類View的成員函數dispatchDraw。 前面我們已經假設當前正在處理的視圖是應用程序窗口的頂層視圖,即它是一個類型為DecorView視圖。DecorView類是從ViewGroup類繼承下來的,并且在ViewGroup類中重寫了父類View類的成員函數dispatchDraw。因此,接下來我們就繼續分析ViewGroup的成員函數dispatchDraw的實現。 **Step 9. ViewGroup.dispatchDraw** 這個函數定義在文件frameworks/base/core/java/android/view/ViewGroup.java中,它的實現比較長,我們分段來閱讀: ~~~ public abstract class ViewGroup extends View implements ViewParent, ViewManager { ...... @Override protected void dispatchDraw(Canvas canvas) { final int count = mChildrenCount; final View[] children = mChildren; int flags = mGroupFlags; ~~~ ViewGroup類的成員變量mChildrenCount描述的是當前視圖組的子視圖的個數,另外一個成員變量mChildren是一個類型為View的數組,用來保存當前視圖組的子視圖。此外,ViewGroup類的成員變量mGroupFlags用來描述當前視圖組的標志位。這段代碼將上述ViewGroup類的三個成員變量分別保存在變量count、children和flags中,以便接下來可以訪問。 我們繼續往下閱讀代碼: ~~~ if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) { ...... for (int i = 0; i < count; i++) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { ...... bindLayoutAnimation(child); ...... } } final LayoutAnimationController controller = mLayoutAnimationController; ...... controller.start(); ...... if (mAnimationListener != null) { mAnimationListener.onAnimationStart(controller.getAnimation()); } } ~~~ 這段代碼用來檢查當前視圖組的子視圖是否需要顯示動畫。如果變量flags的FLAG_RUN_ANIMATION位等于1,并且ViewGroup類的成員函數canAnimate的返回值等于true,即當前當前視圖組允許其子視圖顯示動畫,那么這段代碼接下來就要開始顯示動畫了。 這段代碼首先檢查當前視圖組的每一個子視圖child,如果它是可見的,那么就會調用ViewGroup類的另外一個成員函數bindLayoutAnimation來設置它的動畫。設置完成子視圖的動畫之后,這段代碼接下來再調用ViewGroup類的成員變量mLayoutAnimationController所指向的一個LayoutAnimationController對象的成員函數start來啟動動畫,并且調用ViewGroup類的成員變量mAnimationListener所指向的一個AnimationListener對象的成員函數onAnimationStart來通知那些注冊到當前視圖組的動畫監聽者,當前視圖組開始顯示動畫了。 我們繼續往下閱讀代碼: ~~~ int saveCount = 0; final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK; if (clipToPadding) { saveCount = canvas.save(); canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop, mScrollX + mRight - mLeft - mPaddingRight, mScrollY + mBottom - mTop - mPaddingBottom); } ~~~ 這段代碼檢查變量flags的CLIP_TO_PADDING_MASK位是否不等于1。如果不等于1的話,那么就說明需要設置參數canvas所描述的一塊畫布的剪裁區域,使得這個裁剪區域不包含當前視圖組的內邊距。注意,當前視圖組的內邊距是通過從父類View繼承下來的四個成員變量mPaddingLeft、mPaddingRight、mPaddingTop和mPaddingBottom來描述的。此外,當前視圖組的區域是通過從父類繼承下來的四個成員變量量mLeft、mRight、mTop和mBottom描述的。再結合當前視圖的當前滾動位置mScrollX的mScrollY,就可以計算出參數canvas所描述的一塊畫布的剪裁區域。 在設置參數canvas所描述的一塊畫布的剪裁區域之前,這段代碼會先調用參數canvas所指向的一個Canvas對象的成員函數save來保存它的堆棧狀態,以便在繪制完成當前視圖組的UI之后,可以恢復canvas所描述的一塊畫布的堆棧狀態。 我們繼續往下閱讀代碼: ~~~ boolean more = false; final long drawingTime = getDrawingTime(); if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) { for (int i = 0; i < count; i++) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { more |= drawChild(canvas, child, drawingTime); } } } else { for (int i = 0; i < count; i++) { final View child = children[getChildDrawingOrder(count, i)]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { more |= drawChild(canvas, child, drawingTime); } } } ~~~ 這段代碼用來繪制當前視圖組的子視圖。如果一個子視圖child是可見的,即它的成員變量mViewFlags的VISIBLE位等于1,或者它有一個動畫需要顯示,即它的成員函數getAnimation的返回值不等于null,那么這個子視圖就是需要繪制的。繪制一個子視圖是通過調用ViewGroup類的成員函數drawChild來實現的。ViewGroup類的成員函數drawChild在繪制一個子視圖的時候,如果這個子視圖的動畫還沒有結束,那么它的返回值就等于true,并且會被設置到變量more中去。 注意,當變量flags的FLAG_USE_CHILD_DRAWING_ORDER位等于0的時候,就表示當前視圖組的子視圖按照它們在數組children中的位置從小到在三類繪制,否則的話,就需要通過ViewGroup類的成員函數getChildDrawingOrder來決定這些子視圖的繪制順序。 我們接著往下閱讀代碼: ~~~ // Draw any disappearing views that have animations if (mDisappearingChildren != null) { final ArrayList<View> disappearingChildren = mDisappearingChildren; final int disappearingCount = disappearingChildren.size() - 1; // Go backwards -- we may delete as animations finish for (int i = disappearingCount; i >= 0; i--) { final View child = disappearingChildren.get(i); more |= drawChild(canvas, child, drawingTime); } } ~~~ ViewGroup類的成員變量mDisappearingChildren用來保存那些正在消失的子視圖,但是這些子視圖正在顯示動畫的過程中,因此,這些子視圖也是需要繪制的,這段代碼同樣是通過調用ViewGroup類的成員函數drawChild來繪制它們。 我們繼續往下閱讀最后一段代碼: ~~~ if (clipToPadding) { canvas.restoreToCount(saveCount); } // mGroupFlags might have been updated by drawChild() flags = mGroupFlags; if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) { invalidate(); } if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 && mLayoutAnimationController.isDone() && !more) { // We want to erase the drawing cache and notify the listener after the // next frame is drawn because one extra invalidate() is caused by // drawChild() after the animation is over mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER; final Runnable end = new Runnable() { public void run() { notifyAnimationListener(); } }; post(end); } } ...... ~~~ 這段代碼執行以下三個操作: 1. 檢查變量clipToPadding的值是否等于true。如果是的話,那么就說明前面設置過參數canvas所描述的一塊畫布的裁剪區域。由于現在已經在這塊畫布上繪制完成當前視圖組的UI了,因此,就需要恢復參數canvas所描述的一塊畫布堆棧狀態。這是通過調用參數canvas所指向的一個Canvas對象的成員函數restoreToCount來實現的。 2. 前面在繪制當前視圖組的子視圖的UI的時候,有可能會需要修改當前視圖組的標志位,即修改ViewGroup類的成員變量mGroupFlags的值。如果修改后的mGroupFlags的FLAG_INVALIDATE_REQUIRED位等于1,那么就說明當前視圖組需要重新發起一個繪制UI的請求。這是通過調用ViewGroup類的另外一個成員函數invalidate來實現的。 3. 如果當前視圖組的動畫已經顯示完成,并且當前視圖組的子視圖的動畫也已經顯示完成,再并且當前視圖組注冊有動畫監聽者,那么就是會調用ViewGroup類的另外一個成員函數notifyAnimationListener來通知這些動畫監聽者,當前視圖組的動畫已經顯示結束。注意,ViewGroup類的成員函數notifyAnimationListener是以消息的形式來調用的,即ViewGroup類的成員函數dispatchDraw不是在動畫一顯示結束,就馬上通知那些動畫監聽者。 接下來,我們就繼續分析ViewGroup類的成員函數drawChild的實現,以便可以了解一個視圖組的子視圖的繪制過程。 Step 10. ViewGroup.drawChild 這個函數定義在文件frameworks/base/core/java/android/view/ViewGroup.java中,它的實現比較長,我們分段來閱讀: [java] view plain copy public abstract class ViewGroup extends View implements ViewParent, ViewManager { ...... protected boolean drawChild(Canvas canvas, View child, long drawingTime) { boolean more = false; final int cl = child.mLeft; final int ct = child.mTop; final int cr = child.mRight; final int cb = child.mBottom; final int flags = mGroupFlags; 這段代碼首先獲得子視圖child的區域(cl, ct, cr, cb),以及當前視圖組的標志位flags,以便接下來可以使用。另外,變量more的值用來表示子視圖child是否還在顯示動畫。 我們接著往下閱讀代碼: [java] view plain copy Transformation transformToApply = null; final Animation a = child.getAnimation(); ...... if (a != null) { ...... if (mChildTransformation == null) { mChildTransformation = new Transformation(); } more = a.getTransformation(drawingTime, mChildTransformation); transformToApply = mChildTransformation; ...... } else if ((flags & FLAG_SUPPORT_STATIC_TRANSFORMATIONS) == FLAG_SUPPORT_STATIC_TRANSFORMATIONS) { if (mChildTransformation == null) { mChildTransformation = new Transformation(); } final boolean hasTransform = getChildStaticTransformation(child, mChildTransformation); if (hasTransform) { final int transformType = mChildTransformation.getTransformationType(); transformToApply = transformType != Transformation.TYPE_IDENTITY ? mChildTransformation : null; ...... } } 這段代碼用來獲得子視圖child的變換矩陣transformToApply。獲得了子視圖child的變換矩陣transformToApply之后,我們就可以知道如何來顯示它了。 在兩種情況下,子視圖child會被設置一個變換矩陣。第一種情況子視圖child正在顯示動畫的過程中,第二種情況是當前視圖組給每一個子視圖設置了一個變換矩陣。下面我們就分別討論這兩種情況。 對于第一種情況,子視圖child的成員函數getAnimation的返回值a不等于null,并且它所指向的一個Animation對象就是用來描述子視圖child的動畫的。獲得了子視圖的動畫對象a之后,我們就可以調用它的成員函數getTransformation來繼續執行它的動畫了。如果該動畫還需要繼續執行,那么調用Animation對象a的成員函數getTransformation的返回值more就會等于true,并且會返回子視圖child的接下來需要使用的變換矩陣,保存在ViewGroup類的成員變量mChildTransformation中。ViewGroup類的成員變量mChildTransformation最后又會保存在變量transformToApply中。 對于第二種情況,變量flags的FLAG_SUPPORT_STATIC_TRANSFORMATIONS位等于1,這時候調用ViewGroup類的成員函數getChildStaticTransformation就可以知道子視圖child是否被設置了一個變換矩陣。如果設置了的話,那么ViewGroup類的成員函數getChildStaticTransformation的返回值hasTransform就會等于true。在這種情況下,ViewGroup類的成員變量mChildTransformation所描述的變換矩陣就是要應用在子視圖child中的。不過有一個前提,即ViewGroup類的成員變量mChildTransformation所描述的變換矩陣不是一個單位矩陣,這是因為單位矩陣是沒有變換效果的。如果ViewGroup類的成員變量mChildTransformation所描述的變換矩陣不是一個單位矩陣,那么它同樣會被保存在變量transformToApply中。 我們繼續往下閱讀代碼: [java] view plain copy // Sets the flag as early as possible to allow draw() implementations // to call invalidate() successfully when doing animations child.mPrivateFlags |= DRAWN; ...... child.computeScroll(); final int sx = child.mScrollX; final int sy = child.mScrollY; boolean scalingRequired = false; Bitmap cache = null; if ((flags & FLAG_CHILDREN_DRAWN_WITH_CACHE) == FLAG_CHILDREN_DRAWN_WITH_CACHE || (flags & FLAG_ALWAYS_DRAWN_WITH_CACHE) == FLAG_ALWAYS_DRAWN_WITH_CACHE) { cache = child.getDrawingCache(true); if (mAttachInfo != null) scalingRequired = mAttachInfo.mScalingRequired; } final boolean hasNoCache = cache == null; 這段代碼執行以下三個操作: 1. 將子視圖child的標志值mPrivateFlags的DRAWN設置為1,因為接下來它就會被繪制了。 2. 計算子視圖child的滾動位置,這是通過調用子視圖child的成員函數computeScroll來實現的。計算好子視圖child的滾動位置之后,我們就可以通過它的成員變量mScrollX和mScrollY來獲得它在X軸和Y軸上的偏移了。這兩個偏移值保存在變量sx和sy中。 3. 檢查變量flags的FLAG_CHILDREN_DRAWN_WITH_CACHE位或者FLAG_ALWAYS_DRAWN_WITH_CACHE位是否等于1。如果其中的一個等于1的話,那么就說明子視圖是使用緩沖方式來繪制,即它的UI來緩沖在一個Bitmap里面,通過調用子視圖child的成員函數getDrawingCache就可以獲得這個Bitmap,并且保存在變量cache中。同時,通過ViewGroup類的成員變量mAttachInfo所指向的一個AttachInfo對象的成員變量mScalingRequired還可以知道子視圖是否要進行縮放。如果需要縮放,那么變量scalingRequired的值就會等于true。另外,如果子視圖child有一個緩沖的Bitmap,那么變量hasNoCache的值就會等于false。 我們繼續往下閱讀代碼: [java] view plain copy final int restoreTo = canvas.save(); if (hasNoCache) { canvas.translate(cl - sx, ct - sy); } else { canvas.translate(cl, ct); if (scalingRequired) { // mAttachInfo cannot be null, otherwise scalingRequired == false final float scale = 1.0f / mAttachInfo.mApplicationScale; canvas.scale(scale, scale); } } float alpha = 1.0f; if (transformToApply != null) { ...... alpha = transformToApply.getAlpha(); ...... } ...... if ((flags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) { if (hasNoCache) { canvas.clipRect(sx, sy, sx + (cr - cl), sy + (cb - ct)); } else { if (!scalingRequired) { canvas.clipRect(0, 0, cr - cl, cb - ct); } else { canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight()); } } } 這段代碼用來設置子視圖child的偏移、Alpha通道以及裁剪區域: 1. 如果子視圖child不是以緩沖的方式來繪制的,那么它的偏移值就需要同時考慮它在X軸和Y軸上的起始位置(cl, ct)以及滾動值sx、sy;如果子視圖child是以緩沖的方式來繪制的,那么就不需要考慮它在X軸和Y軸上的滾動位置sx和sy,而只需要考慮它在X軸和Y軸上的起始位置(cl, ct),這是因為它所緩沖的Bitmap已經包含了滾動信息。注意,在子視圖child是以緩沖的方式來繪制的情況中,如果變量scalingRequired的值等于true,那么這段代碼同時還需要為子視圖child設置一個縮放因子,以便接下來可以同步緩沖的Bitmap的所表示的大小。 2. 如果子視圖child此時被設置了一個變換矩陣transformToApply,那么一般它就會有一個Alpha值。例如,前面提到,當子視圖child還處理動畫顯示的狀態時,它就會有一個變換矩陣,而這個動畫一般會有透明的效果,因此,就會有一個Alpha值。通過調用變量transformToApply所指向的一個Transformation對象的成員函數getAlpha就可以獲得子視圖child的Alpha值,保存在變量alpha中。如果子視圖child此時沒有被設置變換矩陣的話,那么它的Alpha值alpha就會等于1.0,表示不是透明的。 3. 如果變量flags的FLAG_CLIP_CHILDREN位等于1,那么就說明需要為子視圖child設置一個剪裁區域。在設置子視圖child的剪裁區域時,同樣是需要考慮子視圖child是否使用緩沖方式來繪制。如果不使用緩沖方式來會繪制,那么子視圖child的剪裁區域就需要同時考慮它在X軸和Y軸上的起始位置(cl, ct)以及滾動值sx、sy;如果不是使用緩沖方式來會繪制,那么就不需要考慮它在X軸和Y軸上的滾動位置sx和sy。注意,在子視圖child是以緩沖的方式來繪制的情況中,如果變量scalingRequired的值等于false,那么它的剪裁區域的寬度和高度就分別為(cr - cl)和(cb - ct),否則的話,它的剪裁區域的寬度和高度就等于上一次的緩沖Bitmap的寬度和寬度,這是因為對于后者來說,前面在設置子視圖child的偏移時,已經同時設置過它的縮放因子了。 注意,在子視圖child的偏移、Alpha通道以及裁剪區域之前,這段代碼首先會保存在參數canvas所描述的一塊畫布的堆棧狀態,以便在繪制完成子視圖child的UI之后,可以恢復這塊畫布的堆棧狀態來繪制其它子視圖的UI。保存在參數canvas所描述的一塊畫布的堆棧狀態是通過調用參數canvas所指向的一個Canvas對象的成員函數save來實現的。 我們繼續往下閱讀代碼: [java] view plain copy if (hasNoCache) { // Fast path for layouts with no backgrounds if ((child.mPrivateFlags & SKIP_DRAW) == SKIP_DRAW) { ...... child.dispatchDraw(canvas); } else { child.draw(canvas); } } else { final Paint cachePaint = mCachePaint; if (alpha < 1.0f) { cachePaint.setAlpha((int) (alpha * 255)); mGroupFlags |= FLAG_ALPHA_LOWER_THAN_ONE; } else if ((flags & FLAG_ALPHA_LOWER_THAN_ONE) == FLAG_ALPHA_LOWER_THAN_ONE) { cachePaint.setAlpha(255); mGroupFlags &= ~FLAG_ALPHA_LOWER_THAN_ONE; } ...... canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint); } 這段代碼就是用來繪制子視圖child的UI的了,分為兩種情況來考慮: 1. 以非緩沖的方式來繪制。這時候需要檢查子視圖child的標志值mPrivateFlags的SKIP_DRAW位是否等于1。如果等于1的話,那么就說明需要跳過子視圖child的繪制,但是需要繪制子視圖child的子視圖,否則的話,就需要先繪制子視圖child的UI,再繪制它的子視圖的UI。繪制子視圖child的子視圖是通過調用它的成員函數dispatchDraw來實現的,而繪制子視圖child本身及其子視圖是通過調用它的成員函數draw來實現的。 2. 以緩沖的方式來繪制。這時候只需要將上一次的緩沖的Bitmap對象cache繪制到參數canvas所描述的一塊畫布上面就行了。在繪制之前,需要先設置用來繪制Bitmap對象cache的一個Paint對象的Alpha值,這個Paint對象保存在ViewGroup類的成員變量mCachePaint中。這個Alpha值保存在變量alpha中,如果它的值小于1.0,那么就說明子視圖child有一個透明值,因此,就需要將它設置到ViewGroup類的成員變量mCachePaint所指向的一個Paint對象中去,并且將ViewGroup類的成員變量mGroupFlags的FLAG_ALPHA_LOWER_THAN_ONE位設置為1。另一方面,如果變量alpha的值大于等于1.0,那么就說明不需要設置子視圖child的透明值,但是如果之前設置過子視圖child的透明值,即ViewGroup類的成員變量mGroupFlags的FLAG_ALPHA_LOWER_THAN_ONE位等于1,那么還需要修改ViewGroup類的成員變量mCachePaint所指向的一個Paint對象是不透明的,即將它的透明值設置為255,并且將ViewGroup類的成員變量mGroupFlags的FLAG_ALPHA_LOWER_THAN_ONE位重置為0。最后,就可以調用參數canvas所指向的一個Canvas對象的成員函數drawBitmap來繪制子視圖child的UI了。 我們繼續往下閱讀最后一段代碼: [java] view plain copy canvas.restoreToCount(restoreTo); ...... return more; } ...... 繪制完成子視圖child的UI之后,就可以恢復參數canvas所描述的一塊畫布的堆棧狀態了,這是通過調用參數canvas所指向的一個Canvas對象的成員函數restoreToCount來實現的。 ViewGroup類的成員函數drawChild最后就將變量more的值返回給調用者了,以便調用者可以知道當前正在繪制的子視圖child是否還處于動畫狀態中。 從上面的分析就可以知道,當子視圖child不是以非緩沖模式來繪制,并且它需要繪制自己及其子視圖時,它的成員函數draw就會被調用,這時候就會重復執行Step 8、Step 9和Step 10,直到所有子孫視圖都繪制完成為止。 這一步執行完成之后,應用程序窗口的UI就全部繪制到在前面的Step 2中所獲得的一塊畫布上面去了,返回到前面的Step 1中,即ViewRoot類的成員函數draw中,接下來就會Java層的Surface類的成員函數unlockCanvasAndPost來請求SurfaceFlinger服務渲染這塊畫布里面所包含的一個圖形緩沖區了。 接下來,我們就繼續分析Java層的Surface類的成員函數unlockCanvasAndPost的實現。 Step 11. Surface.unlockCanvasAndPost [java] view plain copy public class Surface implements Parcelable { ...... /** unlock the surface and asks a page flip */ public native void unlockCanvasAndPost(Canvas canvas); ...... } 這個函數定義在文件frameworks/base/core/java/android/view/Surface.java中。 Surface類的成員函數unlockCanvasAndPost是一個JNI方法,它是由C++層的函數Surface_unlockCanvasAndPost來實現的,如下所示: [cpp] view plain copy static void Surface_unlockCanvasAndPost( JNIEnv* env, jobject clazz, jobject argCanvas) { jobject canvas = env->GetObjectField(clazz, so.canvas); if (canvas != argCanvas) { doThrow(env, "java/lang/IllegalArgumentException", NULL); return; } const sp<Surface>& surface(getSurface(env, clazz)); if (!Surface::isValid(surface)) return; // detach the canvas from the surface SkCanvas* nativeCanvas = (SkCanvas*)env->GetIntField(canvas, no.native_canvas); int saveCount = env->GetIntField(clazz, so.saveCount); nativeCanvas->restoreToCount(saveCount); nativeCanvas->setBitmapDevice(SkBitmap()); env->SetIntField(clazz, so.saveCount, 0); // unlock surface status_t err = surface->unlockAndPost(); if (err < 0) { doThrow(env, "java/lang/IllegalArgumentException", NULL); } } 這個函數定義在文件frameworks/base/core/jni/android_view_Surface.cpp中。 參數clazz指向的是一個Java層的Surface對象,用來描述當前正在繪制的應用程序窗口的繪圖表面,而參數argCanvas指向的是一個Java層的Canvas對象。 參數clazz指向的是一個Java層的Surface對象的成員變量mCanvas所指向的一個Canvas對象canvas應當等于參數argCanvas指向的是一個Java層的Canvas對象,否則的話,函數就會拋出一個異常。 從前面Android應用程序窗口(Activity)的繪圖表面(Surface)的創建過程分析一文可以知道,每一個Java層的Surface對象在C++層都有一個對應的Surface對象。因此,函數t就可以通過調用另外一個函數getSurface來獲得與參數clazz所指向的是一個Java層的Surface對象所對應的C++層的Surface對象surface。C++層的Surface類的靜態成員函數isValid用來驗證Surface對象surface是否已經連接到SurfaceFlinger服務中。如果還沒有連接到,那么函數就會出錯返回了。 通過了上述的合法性檢查之后,函數接下來主要就是做兩件事情: 1. 恢復變量canvas所描述的一塊畫布的堆棧狀態。變量canvas所描述的一塊畫布是在前面的Step 2中開始初始化的,每次應用程序窗口在上面繪制完成UI之后,我們都應該恢復它的堆棧狀態,以便下一次使用時不會受上一次影響。變量canvas指向的是一個Java層的Canvas對象,函數首先找到與它所對應的一個C++層的SkCanvas對象nativeCanvas,然后就可以調用這個SkCanvas對象nativeCanvas的成員函數restoreToCount來恢復它所描述的一塊畫布的堆棧狀態了。這塊畫布在繪制應用程序窗口UI前的堆棧狀態保存在參數clazz所指向的一個Java層的Surface對象的成員變量mSaveCount中。因此,函數就先獲得參數clazz所指向的一個Java層的Surface對象的成員變量mSaveCount的值,然后再以它為參數來調用SkCanvas對象nativeCanvas的成員函數restoreToCount,這樣就可以恢復畫布的堆棧狀態了。 2. 請求SurfaceFlinger服務渲染Surface對象surface所描述的應用程序窗口的繪圖表面。應用程序窗口的UI是繪制在SkCanvas對象nativeCanvas所描述的一塊畫布上的,而這塊畫布所使用的圖形緩沖區是保存在Surface對象surface的內部的,因此,函數就調用Surface對象surface的成員函數unlockAndPost來請求SurfaceFlinger服務渲染這塊圖形緩沖區。 接下來,我們就繼續分析C++層的Surface類的成員函數unlockAndPost的實現,以便可以了解用來繪制應用程序窗口UI的圖形緩沖區是如何渲染的。 Step 12. Surface.unlockAndPost [cpp] view plain copy status_t Surface::unlockAndPost() { if (mLockedBuffer == 0) { LOGE("Surface::unlockAndPost failed, no locked buffer"); return INVALID_OPERATION; } status_t err = mLockedBuffer->unlock(); LOGE_IF(err, "failed unlocking buffer (%p)", mLockedBuffer->handle); err = queueBuffer(mLockedBuffer.get()); LOGE_IF(err, "queueBuffer (idx=%d) failed (%s)", getBufferIndex(mLockedBuffer), strerror(-err)); mPostedBuffer = mLockedBuffer; mLockedBuffer = 0; return err; } 這個函數定義在文件frameworks/base/libs/surfaceflinger_client/Surface.cpp中。 從前面的Step 3可以知道,應用程序窗口當前正在使用的圖形緩沖區保存在Surface類的成員變量mLockedBuffer中,因此,Surface類的成員函數unlockAndPost的目標就是要將它交給SurfaceFlinger服務來渲染,這是通過調用另外一個成員函數queueBuffer來實現的。在前面Android應用程序請求SurfaceFlinger服務渲染Surface的過程分析一文中,我們已經分析過Surface類的成員函數queueBuffer的實現了,它主要就是向應用程序窗口的待渲染圖形緩沖區隊列中添加一個圖形緩沖區,然后再請請求SurfaceFlinger服務來渲染這個圖形緩沖區。 在渲染成員變量mLockedBuffer所描述的一個圖形緩沖區之前,Surface類的成員函數unlockAndPost還會調用它的成員函數unlock來執行一個“解鎖”操作。從前面的Step 3可以知道,成員變量mLockedBuffer所描述的一個圖形緩沖區在交給應用程序窗口使用之前,它會被執行一個“鎖定”的操作,即它的成員函數lock會被調用,因此,這里執行的“解鎖”操作是與前面的“鎖定”操作相對應的。事實上,對成員變量mLockedBuffer所描述的一個圖形緩沖區進行鎖定,主要是為了獲得這個圖形緩沖區的地址,是否真的要對個圖形緩沖區進行鎖定,是由HAL層模塊Gralloc的實現來決定的。 在請求SurfaceFlinger服務渲染了成員變量mLockedBuffer所描述的一個圖形緩沖區之后,Surface類的成員函數unlockAndPost還會把成員變量mLockedBuffer所描述的一個圖形緩沖區保存在另外一個成員變量mPostedBuffer中,表示這個圖形緩沖區已經變成是正在渲染的圖形緩沖區了,或者說是前端緩沖區了。 最后,Surface類的成員函數unlockAndPost就把成員變量mLockedBuffer的值設置為0,這樣就可以將應用程序窗口下一次請求分配和使用的圖形緩沖區保存在它里面。 Surface類的成員變量mLockedBuffer指向的是一個GraphicBuffer對象,接下來我們就繼續分析它的成員函數unlock的實現,以便可以了解它所描述的圖形緩沖區的“解鎖”過程。 **Step 13. GraphicBuffer.unlock** ~~~ status_t GraphicBuffer::unlock() { status_t res = getBufferMapper().unlock(handle); return res; } ~~~ 這個函數定義在文件frameworks/base/libs/ui/GraphicBuffer.cpp中。 從前面的Step 4可以知道,GraphicBuffer類的成員變量handle用來描述當前正在處理的圖形緩沖區的句柄,而GraphicBuffer類的成員函數getBufferMapper返回的是一個GraphicBufferMapper對象。有了這個GraphicBufferMapper對象之后,就可以調用它的成員函數unlock解鎖成員變量handle所描述的一個圖形緩沖區了。 接下來,我們就繼續分析GraphicBufferMapper類的成員函數unlock的實現。 **Step 14. GraphicBufferMapper.unlock** ~~~ status_t GraphicBufferMapper::unlock(buffer_handle_t handle) { status_t err; if (sw_gralloc_handle_t::validate(handle) < 0) { err = mAllocMod->unlock(mAllocMod, handle); } else { err = sw_gralloc_handle_t::unlock((sw_gralloc_handle_t*)handle); } LOGW_IF(err, "unlock(...) failed %d (%s)", err, strerror(-err)); return err; } ~~~ 這個函數定義在文件frameworks/base/libs/ui/GraphicBufferMapper.cpp。 從前面的Step 5可以知道,參數handle所描述的圖形緩沖區是在HAL模塊Gralloc中分配的,這個HAL模塊Gralloc是由GraphicBufferMapper類的成員變量mAllocMod來描述的,因此,函數就終就會調用GraphicBufferMapper類的成員變量mAllocMod所描述的一個HAL模塊Gralloc的成員函數unlock來解鎖參數andle所描述的一個圖形緩沖區。HAL模塊Gralloc的成員函數unlock的實現可以參考前面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的實現,以便可以從系統的角度來分析應用程序窗口的實現。敬請關注!
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看