Android之View繪制流程源碼分析


版權聲明:本文出自汪磊的博客,轉載請務必注明出處。

對於稍有自定義View經驗的安卓開發者來說,onMeasure,onLayout,onDraw這三個方法都不會陌生,起碼多少都有所接觸吧。

在安卓中,一個View顯示到屏幕上基本上都是經過測量,擺放,繪制這三個過程才顯示出來,那么這三個過程到底是怎么執行的呢?本文與大家一起探討一下安卓中View的繪制流程。

一,View樹繪制流程開始的地方(API23)

對一個布局進項測量,擺放,繪制肯定要有開始的地方吧,這里就直接跟大家說了,View繪制流程開始的地方是ViewRootImpl類的performTraversals()方法(至於為什么是這里不是本篇重點,后續有時間寫一篇針對這里的文章說明一下),接下來我們看下performTraversals()方法(此方法過長,只列出重要邏輯代碼)

 1 private void performTraversals() {
 2         ......
 3         int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
 4         int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
 5          ......
 6          // Ask host how big it wants to be
 7          performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
 8          ......
 9          performLayout(lp, desiredWindowWidth, desiredWindowHeight);
10          ......
11          performDraw();
12         ......
13      }

 第3,4行代碼調用getRootMeasureSpec方法生成對應寬高,我們先看下getRootMeasureSpec都做了什么,源碼如下:

 1   /**
 2      * Figures out the measure spec for the root view in a window based on it's
 3      * layout params.
 4      *
 5      * @param windowSize
 6      *            The available width or height of the window
 7      *
 8      * @param rootDimension
 9      *            The layout params for one dimension (width or height) of the
10      *            window.
11      *
12      * @return The measure spec to use to measure the root view.
13      */
14     private static int getRootMeasureSpec(int windowSize, int rootDimension) {
15         int measureSpec;
16         switch (rootDimension) {
17 
18         case ViewGroup.LayoutParams.MATCH_PARENT:
19             // Window can't resize. Force root view to be windowSize.
20             measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
21             break;
22         case ViewGroup.LayoutParams.WRAP_CONTENT:
23             // Window can resize. Set max size for root view.
24             measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
25             break;
26         default:
27             // Window wants to be an exact size. Force root view to be that size.
28             measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
29             break;
30         }
31         return measureSpec;
32     }

 先說一下MeasureSpec這個概念:也叫測量規格,MeasureSpec是一個32位整數,由SpecMode和SpecSize兩部分組成,其中,高2位為SpecMode,低30位為SpecSize。SpecMode為測量模式,SpecSize為相應測量模式下的測量尺寸。

View(包括普通View和ViewGroup)的SpecMode由本View的LayoutParams結合父View的MeasureSpec生成(普通View的MeasureSpec是由其父類ViewGroup生成的,后面會詳細講到)。
SpecMode的取值可為以下三種:

 MeasureSpec.EXACTLY //確定模式,父View希望子View的大小是確定的,由specSize決定;

 MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;

 MeasureSpec.UNSPECIFIED //未指定模式,父View對子View的大小不做限制,完全由子View自己決定;

 

getRootMeasureSpec方法就是生成根視圖的MeasureSpec,還記得我們上一篇《Android之View繪制流程開胃菜---setContentView(...)詳細分析》中分析的嗎,平時我們自己寫的布局都是被添加到DecorView中id為content的布局中的,這里傳入進來的windowSize參數是window的可用寬高信息,rootDimension寬高參數均為MATCH_PARENT。

我們上面說普通View的MeasureSpec是由其父類ViewGroup生成的,但是根視圖DecorView是沒有父類的,所以getRootMeasureSpec就是給根視圖生成測量規格的,生成的MeasureSpec中SpecMode為MeasureSpec.EXACTLY,SpecSize則為窗口的可用尺寸。

回到performTraversals()方法中:

3,4行代碼分別生成寬高的測量規格

7,9,11行代碼分別執行performMeasure(childWidthMeasureSpec, childHeightMeasureSpec), performLayout(lp, desiredWindowWidth, desiredWindowHeight),performDraw();

方法,我們看下這三個方法源碼:都經過簡化處理

1 private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
2         Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
3         try {
4             mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
5         } finally {
6             Trace.traceEnd(Trace.TRACE_TAG_VIEW);
7         }
8     }
1    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
2             int desiredWindowHeight) {
3   
4         ...
5         final View host = mView;
6 
7         host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
8        ...
9     }        
1     private void performDraw() {
2            ...
3             draw(fullRedrawNeeded);
4             ...
5     }
1 private void draw(boolean fullRedrawNeeded) {
2 
3           ...
4           if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
5              return;
6           }
7           ...
8     }
1     private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
2             boolean scalingRequired, Rect dirty) {
3         ...
4          mView.draw(canvas);
5         ...
6         return true;
7     }
performMeasure方法最核心的是第4行調用mView的measure方法。
performLayout方法通過5,7行代碼發現其實也是調用的mView的layout方法。
performDraw最終調用的也是調用的mView的draw方法。
上面的mView就是DecorView,我們知道DecorView是FrameLayout,FrameLayout繼承自ViewGroup,ViewGroup繼承自View,所以最終都會調用View類中measure,
layout,draw方法。
實際上View的繪制流程可以分為三個階段:
  • measure: 判斷是否需要重新計算View的大小,需要的話則計算;
  • layout: 判斷是否需要重新計算View的位置,需要的話則計算;
  • draw: 判斷是否需要重新繪制View,需要的話則重繪制。
大體流程如圖:

二,View繪制流程第一步measure過程分析(API23)
接下來我們看下View中的measure源碼:簡化處理
 1    /**
 2      * <p>
 3      * This is called to find out how big a view should be. The parent
 4      * supplies constraint information in the width and height parameters.
 5      * </p>
 6      *
 7      * <p>
 8      * The actual measurement work of a view is performed in
 9      * {@link #onMeasure(int, int)}, called by this method. Therefore, only
10      * {@link #onMeasure(int, int)} can and must be overridden by subclasses.
11      * </p>
12      *
13      *
14      * @param widthMeasureSpec Horizontal space requirements as imposed by the
15      *        parent
16      * @param heightMeasureSpec Vertical space requirements as imposed by the
17      *        parent
18      *
19      * @see #onMeasure(int, int)
20      */
21     public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
22         ...
23         // measure ourselves, this should set the measured dimension flag back
24         onMeasure(widthMeasureSpec, heightMeasureSpec);
25          ...
26     }

注釋已經給出大體描述:這個被調用用來測算出view大小,並且其父類提供了約束信息widthMeasureSpec與heightMeasureSpec。

我們發現measure方法被final修飾,所以這個方法不能被子類重寫。

實際的測量是在onMeasure方法進行,所以在View的普通子類中需要重寫onMeasure方法來實現自己的測量邏輯。

對於普通View,調用View類的onMeasure()方法來進行實際的測量工作即可,當然我們也可以重載onMeasure並調用setMeasuredDimension來設置任意大小的布局。

接下來我們看下默認情況下View類中onMeasure方法都做了什么,源碼如下;

 1     /**
 2      * <p>
 3      * Measure the view and its content to determine the measured width and the
 4      * measured height. This method is invoked by {@link #measure(int, int)} and
 5      * should be overridden by subclasses to provide accurate and efficient
 6      * measurement of their contents.
 7      * </p>
 8      *
 9      * <p>
10      * <strong>CONTRACT:</strong> When overriding this method, you
11      * <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
12      * measured width and height of this view. Failure to do so will trigger an
13      * <code>IllegalStateException</code>, thrown by
14      * {@link #measure(int, int)}. Calling the superclass'
15      * {@link #onMeasure(int, int)} is a valid use.
16      * </p>
17      *
18      * <p>
19      * The base class implementation of measure defaults to the background size,
20      * unless a larger size is allowed by the MeasureSpec. Subclasses should
21      * override {@link #onMeasure(int, int)} to provide better measurements of
22      * their content.
23      * </p>
24      *
25      * <p>
26      * If this method is overridden, it is the subclass's responsibility to make
27      * sure the measured height and width are at least the view's minimum height
28      * and width ({@link #getSuggestedMinimumHeight()} and
29      * {@link #getSuggestedMinimumWidth()}).
30      * </p>
31      *
32      * @param widthMeasureSpec horizontal space requirements as imposed by the parent.
33      *                         The requirements are encoded with
34      *                         {@link android.view.View.MeasureSpec}.
35      * @param heightMeasureSpec vertical space requirements as imposed by the parent.
36      *                         The requirements are encoded with
37      *                         {@link android.view.View.MeasureSpec}.
38      *
39      * @see #getMeasuredWidth()
40      * @see #getMeasuredHeight()
41      * @see #setMeasuredDimension(int, int)
42      * @see #getSuggestedMinimumHeight()
43      * @see #getSuggestedMinimumWidth()
44      * @see android.view.View.MeasureSpec#getMode(int)
45      * @see android.view.View.MeasureSpec#getSize(int)
46      */
47     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
48         setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
49                 getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
50     }

這個方法看注釋就已經大體明白了,簡單翻譯一下吧:這個方法用來測量view以及自身內容來決定寬高,子類應該重寫這個方法提供更精確更高效的測量的內容。當重寫這個方法的時候子類必須調用setMeasuredDimension(int, int)來存儲已經測量出來的寬高。
我們看到系統默認的onMeasure方法只是直接調用了setMeasuredDimension,setMeasuredDimension函數是一個很關鍵的函數,它對View的成員變量mMeasuredWidth和mMeasuredHeight變量賦值,measure的主要目的就是對View樹中的每個View的mMeasuredWidth和mMeasuredHeight進行賦值,所以一旦這兩個變量被賦值意味着該View的測量工作結束。

接下來我們看看設置的默認View寬高,默認寬高都是通過getDefaultSize方法來獲取的,而getDefaultSize又調用了getSuggestedMinimumXXX方法,我們先看下getSuggestedMinimumXXX方法:

1 protected int getSuggestedMinimumHeight() {
2         return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
3 }
1 protected int getSuggestedMinimumWidth() {
2         return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
3 }
mMinHeight或mMinWidth就是我們設置的android:minHeight或android:minWidth參數。
如果我們沒有設置背景則直接返回mMinHeight或mMinWidth,如果設置了背景,則返回miniXXX屬性與mBackground二者中較大者。如背景以及miniXXX屬性都沒設置呢?那就返回0了。
接下來再看getDefaultSize方法源碼:
 1 public static int getDefaultSize(int size, int measureSpec) {
 2         int result = size;
 3         int specMode = MeasureSpec.getMode(measureSpec);
 4         int specSize = MeasureSpec.getSize(measureSpec);
 5 
 6         switch (specMode) {
 7         case MeasureSpec.UNSPECIFIED:
 8             result = size;
 9             break;
10         case MeasureSpec.AT_MOST:
11         case MeasureSpec.EXACTLY:
12             result = specSize;
13             break;
14         }
15         return result;
16 }
getDefaultSize返回值由上面講到的getSuggestedMinimumXXX方法獲取的Size以及父類傳遞過來的measureSpec共同決定。
可以看到如果specMode等於AT_MOST或EXACTLY就返回specSize,這也是系統默認的規格。
到此為止,普通View(非ViewGroup)的測量就基本講完了。但是ViewGroup這種容器類布局是怎么測量其內每個子View的呢?
ViewGroup容器類布局大部分情況下是用來嵌套具體子View的,所以需要負責其子View的測量,在ViewGroup中定義了
measureChildren(int widthMeasureSpec, int heightMeasureSpec)
measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec)
以及measureChildWithMargins(View child,int parentWidthMeasureSpec, int widthUsed,int parentHeightMeasureSpec, int heightUsed)
三個方法來供其子類調用對具體子View進行測量。
measureChildren,measureChild源碼如下:
 1 protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
 2         final int size = mChildrenCount;
 3         final View[] children = mChildren;
 4         for (int i = 0; i < size; ++i) {
 5             final View child = children[i];
 6             if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
 7                 measureChild(child, widthMeasureSpec, heightMeasureSpec);
 8             }
 9         }
10}
 
        
 1 protected void measureChild(View child, int parentWidthMeasureSpec,
 2             int parentHeightMeasureSpec) {
 3         final LayoutParams lp = child.getLayoutParams();
 4 
 5         final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
 6                 mPaddingLeft + mPaddingRight, lp.width);
 7         final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
 8                 mPaddingTop + mPaddingBottom, lp.height);
 9 
10         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
11}

看到了吧,measureChildren只是循環調用measureChild方法,而measureChild方法中會根據父類提供的測量規格parentXXXMeasureSpec一級子類自己LayoutParams調用getChildMeasureSpec方法生成子類自己具體的測量規格。(getChildMeasureSpec稍后會具體分析)

接下來我們看下measureChildWithMargins方法源碼:

 

 1 protected void measureChildWithMargins(View child,
 2             int parentWidthMeasureSpec, int widthUsed,
 3             int parentHeightMeasureSpec, int heightUsed) {
 4         final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
 5 
 6         final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
 7                 mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
 8                         + widthUsed, lp.width);
 9         final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
10                 mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
11                         + heightUsed, lp.height);
12 
13         child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
14 }

與measureChild相比最主要的區別就是measureChildWithMargins額外將具體子View LayoutParams參數的margin也當作參數來生成測量規格。

measureChild與measureChildWithMargins均調用了getChildMeasureSpec方法來生成具體測量規格,接下來我們重點看下這個方法:

 1     public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
 2         int specMode = MeasureSpec.getMode(spec);//獲取父View的mode 3         int specSize = MeasureSpec.getSize(spec);//獲取父View的size
 4      //父View的size減去padding與0比較取其大,specSize - padding得到的值是父View可以用來盛放子View的空間大小
 5         int size = Math.max(0, specSize - padding);
 6 
 7         int resultSize = 0;
 8         int resultMode = 0;
 9 
10         switch (specMode) {
11         // Parent has imposed an exact size on us
12         case MeasureSpec.EXACTLY://父View希望子View是明確大小
13             if (childDimension >= 0) {//子View設置了明確的大小:如 10dp,20dp
14                 resultSize = childDimension;//設置子View測量規格大小為其本身設置的大小
15                 resultMode = MeasureSpec.EXACTLY;//mode設置為EXACTLY
16             } else if (childDimension == LayoutParams.MATCH_PARENT) {//子VIEW的寬或者高設置為MATCH_PARENT,表明子View想和父View一樣大小
17                 // Child wants to be our size. So be it.
18                 resultSize = size;//設置子View測量規格大小為父View可用空間的大小
19                 resultMode = MeasureSpec.EXACTLY;//mode設置為EXACTLY
20             } else if (childDimension == LayoutParams.WRAP_CONTENT) {//子VIEW的寬或者高設置為WRAP_CONTENT,表明子View大小是動態的
21                 // Child wants to determine its own size. It can't be
22                 // bigger than us.
23                 resultSize = size;//設置子View測量規格大小為父View可用空間的大小
24                 resultMode = MeasureSpec.AT_MOST;//mode設置為AT_MOST,表明子View寬高最大值不能超過resultSize 
25             }
26             break;27      //其余情況請自行分析
28         ......
29 return MeasureSpec.makeMeasureSpec(resultSize, resultMode); 30 }

想說的注釋已經給出。

上面的方法展現了根據父View的MeasureSpec和子View的LayoutParams生成子View的MeasureSpec的過程, 子View的LayoutParams表示了子View的期待大小。這個產生的MeasureSpec用於指導子View自身的測量。

在我們自定義View的時候一般會重寫onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法其中的widthMeasureSpec與heightMeasureSpec參數就是父類通過getChildMeasureSpec方法生成的。一個好的自定義View會根據父類傳遞過來的測量規格動態設置大小,而不是直接寫死其大小。

好了,到此為止View的測量過程想說的就差不多都說完了,我們稍微總結一下關鍵的部分;

  • View的measure方法是final的,不允許重載,View子類只能重載onMeasure來完成自己的測量邏輯。

  • 最頂層DecorView測量時的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法確定的。

  • ViewGroup類提供了measureChild,measureChild和measureChildWithMargins方法,以供容器類布局測量自身子View使用

  • 使用View的getMeasuredWidth()和getMeasuredHeight()方法來獲取View測量的寬高,必須保證這兩個方法在onMeasure流程之后被調用才能返回有效值,只有onMeasure流程完后mMeasuredWidth與mMeasuredHeight才會被賦值

  • View的布局大小是由父View和子View共同決定的。我們平時設置的寬高可以理解為希望的大小,具體大小還要結合父類大小來確定。

 

最后附上View繪制流程圖:相信你會理解的更加深刻:

三,View繪制流程第二步layout過程分析(API23)
performMeasure執行完,接着就會執行performLayout:
1    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
2             int desiredWindowHeight) {
3   
4         ...
5         final View host = mView;
6 
7         host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
8        ...
9     } 
mView為根View,即DecorView,DecorView是FrameLayout的子類,最終會調用ViewGroup中layout方法。
所以接下來我們看下ViewGroup中layout方法源碼:
 1 @Override
 2     public final void layout(int l, int t, int r, int b) {
 3         if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
 4             if (mTransition != null) {
 5                 mTransition.layoutChange(this);
 6             }
 7             super.layout(l, t, r, b);
 8         } else {
 9             // record the fact that we noop'd it; request layout when transition finishes
10             mLayoutCalledWhileSuppressed = true;
11         }
12     }
 
        

 第7行代碼表明又調用父類View的layout方法。所以我們看下View的layout源碼,如下:

1     public void layout(int l, int t, int r, int b) {
2     // l為本View左邊緣與父View左邊緣的距離
// t為本View上邊緣與父View上邊緣的距離
       // r為本View右邊緣與父View左邊緣的距離
      // b為本View下邊緣與父View上邊緣的距離

3     ...
4         boolean changed = isLayoutModeOptical(mParent) ?
5                 setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
6 
7         if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
8             onLayout(changed, l, t, r, b);
9      ...
10     }

4,5行代碼主要判斷View的位置是否發生變化,發生變化則changed 會為true,並且setOpticalFrame也是調用的setFrame方法

我們看下setFrame方法源碼:

 1 protected boolean setFrame(int left, int top, int right, int bottom) {
 2         boolean changed = false;
 3 
 4 
 5      ...
 6         if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
 7             changed = true;
 8     
 9             ...
10             mLeft = left;
11             mTop = top;
12             mRight = right;
13             mBottom = bottom;
14      ...
15         }
16         return changed;
17     }    

第6行代碼分別比較之前的記錄的mLeft,mRight,mTop,mBottom 與新傳入的參數如果有一個不同則進入判斷,將changed變量置為true,並且將新傳入的參數分別重新賦值給mLeft,mRight,mTop,mBottom,最后返回changed。

這里還有一點要說,getWidth()、getHeight()和getMeasuredWidth()、getMeasuredHeight()這兩對方法之間的區別,先看一下源碼;

 1    public final int getMeasuredWidth() {
 2         return mMeasuredWidth & MEASURED_SIZE_MASK;
 3     }
 4 
 5     public final int getMeasuredHeight() {
 6         return mMeasuredHeight & MEASURED_SIZE_MASK;
 7     }
 8 
 9     public final int getWidth() {
10         return mRight - mLeft;
11     }
12 
13     public final int getHeight() {
14         return mBottom - mTop;
15     }

在討論View的measure過程時提到過mMeasuredWidth與mMeasuredHeight只有測量過程完成才會被賦值,所以只有測量過程完成調用getMeasuredWidth()、getMeasuredHeight()才會獲取正確的值。

同樣getWidth()、getHeight()只有在layout過程完成時mLeft,mRight,mTop,mBottom才會被賦值,調用才會獲取正確返回值,所以二者調用時機是不同的。

繼續看View中layout源碼第7行,如果changed為true,也就是說View的位置發生了變化,或者標記為PFLAG_LAYOUT_REQUIRED則進入判斷執行onLayout方法。

我們繼續看View中onLayout方法源碼:

1     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
2     }

看到了吧,竟然是個空方法。

對比View的layout和ViewGroup的layout方法發現,View的layout方法是可以在子類重寫的,而ViewGroup的layout是不能在子類重寫的,那么容器類View是怎么對其子View進行擺放的呢?別急,在ViewGroup中同樣也有onLayout方法,源碼如下;

1 /**
2      * {@inheritDoc}
3      */
4     @Override
5     protected abstract void onLayout(boolean changed,
6             int l, int t, int r, int b);

看到了吧,還是個抽象方法,因為具體ViewGroup擺放規則不同,所以其具體子類需要重寫這個方法來實現對其子View的擺放邏輯。

既然這樣我們就只能分析一個繼承自ViewGroup的具體子類了,我們選取FrameLayout,其onLayout源碼如下:

 1     @Override
 2     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
 3         layoutChildren(left, top, right, bottom, false /* no force left gravity */);
 4     }
 5 
 6     void layoutChildren(int left, int top, int right, int bottom,
 7                                   boolean forceLeftGravity) {
 8         final int count = getChildCount();
 9 
10         ......
11 
12         for (int i = 0; i < count; i++) {
13             final View child = getChildAt(i);
14             if (child.getVisibility() != GONE) {
15                .....
16 
17                 child.layout(childLeft, childTop, childLeft + width, childTop + height);
18             }
19         }
20     }

看到了吧,onLayout方法調用layoutChildren方法,在layoutChildren方法中遍歷每個子View調用其layout方法。

好了,到此Layout過程就討論的差不多了,相比measure過程還是簡單不少的,其也是遞歸調用邏輯。如圖:

我們總結一下主要部分:

  • View.layout方法可被重寫,ViewGroup.layout為final的不可重寫,ViewGroup.onLayout為abstract的,具體ViewGroup子類必須重載來按照自己規則對子View進行擺放。

  • measure操作完成后得到的是對每個View經測量過的measuredWidth和measuredHeight,layout操作完成之后得到的是對每個View進行位置分配后的mLeft、mTop、mRight、mBottom,這些值都是相對於父View來說的。

  • 使用View的getWidth()和getHeight()方法來獲取View測量的寬高,必須保證這兩個方法在onLayout流程之后被調用才能返回有效值。

測量,擺放過程都分析完了,接下來我們分析View的draw過程。

四,View繪制流程第三步draw過程分析(API23)

performMeasure, performLayout過程執行完,接下來就執行performDraw()邏輯了,ViewGroup沒有重寫View的draw方法,最終調用的是View中的draw方法,源碼如下:

 1    public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
2   final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
     (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); 3 4 /* 5 * Draw traversal performs several drawing steps which must be executed 6 * in the appropriate order: 7 * 8 * 1. Draw the background 9 * 2. If necessary, save the canvas' layers to prepare for fading 10 * 3. Draw view's content 11 * 4. Draw children 12 * 5. If necessary, draw the fading edges and restore layers 13 * 6. Draw decorations (scrollbars for instance) 14 */ 15 16 // Step 1, draw the background, if needed 17 int saveCount; 18 19 if (!dirtyOpaque) { 20 drawBackground(canvas); 21 } 22 23 // skip step 2 & 5 if possible (common case) 24 final int viewFlags = mViewFlags; 25 boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; 26 boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; 27 if (!verticalEdges && !horizontalEdges) { 28 // Step 3, draw the content 29 if (!dirtyOpaque) onDraw(canvas); 30 31 // Step 4, draw the children 32 dispatchDraw(canvas); 33 34 // Overlay is part of the content and draws beneath Foreground 35 if (mOverlay != null && !mOverlay.isEmpty()) { 36 mOverlay.getOverlayView().dispatchDraw(canvas); 37 } 38 39 // Step 6, draw decorations (foreground, scrollbars) 40 onDrawForeground(canvas); 41 42 // we're done... 43 return; 44 } 45 ... 46 // Step 2, save the canvas' layers 47 .... 48 // Step 3, draw the content 49 if (!dirtyOpaque) onDraw(canvas); 50 51 // Step 4, draw the children 52 dispatchDraw(canvas); 53 54 // Step 5, draw the fade effect and restore layers 55 .... 56 // Step 6, draw decorations (foreground, scrollbars) 57 onDrawForeground(canvas); 58 }

5到14行注釋可以看到draw過程分為6步,再看23行提示大部分情況下跳過第2,5步。所以我們着重分析其余4步。

19-21行執行第一步,繪制背景源碼如下:

 1 private void drawBackground(Canvas canvas) {
 2         final Drawable background = mBackground;
 3         if (background == null) {
 4             return;
 5         }
 6      ....
 7         setBackgroundBounds();
 8         ....
 9         background.draw(canvas);
10        
11     }
12 
13 
14     void setBackgroundBounds() {
15         if (mBackgroundSizeChanged && mBackground != null) {
16             mBackground.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
17             mBackgroundSizeChanged = false;
18             rebuildOutline();
19         }
20     }

只要邏輯就是獲取我們在xml文件或者代碼中設置的背景,然后根據layout過程擺放的位置繪制出來。

第29行執行繪制內容邏輯,源碼如下:

1     /**
2      * Implement this to do your drawing.
3      *
4      * @param canvas the canvas on which the background will be drawn
5      */
6     protected void onDraw(Canvas canvas) {
7     }

看到了吧,是一個空方法,需要具體子類自己去實現,因為每個具體View要繪制的內容是不同的,所以子類需要實現這個方法來繪制自身的內容。

第32行執行繪制子View邏輯,源碼如下:

1 /**
2      * Called by draw to draw the child views. This may be overridden
3      * by derived classes to gain control just before its children are drawn
4      * (but after its own view has been drawn).
5      * @param canvas the canvas on which to draw the view
6      */
7     protected void dispatchDraw(Canvas canvas) {
8 
9     }

看到了吧,也是空方法,這個方法被用來繪制子View的,如果有子View則需要調用這個方法去繪制,我們知道一般只有容器類View才可以盛放子View,所以我們看下ViewGroup中有沒有相關邏輯,在
ViewGroup中果然實現了這個方法,源碼如下:

 1  @Override
 2     protected void dispatchDraw(Canvas canvas) {
 3         boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
 4         final int childrenCount = mChildrenCount;
 5         final View[] children = mChildren;
 6         .......
 7         for (int i = 0; i < childrenCount; i++) {
 8             while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
 9                 final View transientChild = mTransientViews.get(transientIndex);
10                 if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
11                         transientChild.getAnimation() != null) {
12                     more |= drawChild(canvas, transientChild, drawingTime);
13                 }
14                 .......
15             }
16           ......
17         }
18         ......   
19     }

在dispatchDraw方法中遍歷每個子View並且調用drawChild方法,接下來我們看下drawChild源碼:

1 protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
2         return child.draw(canvas, this, drawingTime);
3 }

看到了吧,最終調用每個子View的draw方法來完成自身的繪制。

接下來40行執行onDrawForeground邏輯,這一部分只要繪制一些裝飾物,比如ScrollBar。這部分就不分析了,也不是重點。

到這里,View的主要繪制流程我們也分析完了,也不復雜。

但是,但是!!!!!!細心的你有沒有發現在執行第一步,第三步的時候都有個if判斷(if (!dirtyOpaque)),也就是說只有判斷成立才會執行繪制背景和自身內容,難道還有View不繪制自身內容嗎? 這里就直接說了,ViewGroup子類默認情況下就是不執行onDraw方法的,在ViewGroup源碼中的initViewGroup()方法中設置了一個標記,源碼如下:

1 private void initViewGroup() {
2         // ViewGroup doesn't draw by default
3         if (!debugDraw()) {
4             setFlags(WILL_NOT_DRAW, DRAW_MASK);
5         }
6         ......
7 }    

看第二行注釋也知道,ViewGroup默認情況下是不會draw的。

第四行調用setFlags方法設置標記WILL_NOT_DRAW,

我們在回到View中draw方法看第2行代碼:

1  final int privateFlags = mPrivateFlags;
2 final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && 3 (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);

setFlags方法就是對View中mPrivateFlags值進行相應改變,我們設置標記WILL_NOT_DRAW那么dirtyOpaque得到的值就為true,從而if (!dirtyOpaque)不成立,也就不會執行onDraw方法。

估計這點很多同學有疑問ViewGroup默認情況下onDraw方法是不執行的???別急,動手寫個小demo驗證一下就是了。

布局如下:極其簡單

 1 <com.wanglei.clearheart.MyView xmlns:android="http://schemas.android.com/apk/res/android"
 2     xmlns:tools="http://schemas.android.com/tools"
 3     android:layout_width="match_parent"
 4     android:layout_height="match_parent"
 5     android:gravity="center"
 6     android:orientation="vertical"
 7     tools:context=".MainActivity" >
 8 
 9 
10 </com.wanglei.clearheart.MyView >

MyView源碼如下:同樣極其簡單

 1 public class MyView extends ViewGroup {
 2 
 3     private Paint mPaint;
 4     public MyView(Context context, AttributeSet attrs) {
 5         super(context, attrs);
 6         mPaint = new Paint();
 7         mPaint.setColor(Color.RED);
 8         mPaint.setStyle(Style.STROKE);
 9         mPaint.setStrokeWidth(10); 
10         
11     }
12     
13     @Override
14     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
15         
16         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
17     }
18     
19     @Override
20     protected void onLayout(boolean changed, int l, int t, int r, int b) {
21         // TODO Auto-generated method stub
22         
23     }
24     
25     @Override
26     protected void onDraw(Canvas canvas) {
27         // TODO Auto-generated method stub
28         canvas.drawCircle(getMeasuredWidth()/2, getMeasuredHeight()/2, 360, mPaint);
29     }
30 }

運行程序會看到就是一個大白板,沒有繪制出任何圖形,那我們怎么讓ViewGroup調用onDraw方法呢?

很簡單View類中給我們提供了一個方法供外部調用:

1     public void setWillNotDraw(boolean willNotDraw) {
2         setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
3     }

看到了吧,本質也是調用的setFlags方法。如果我們傳入true則繪制的時候不會調用onDraw方法,傳入false則使其調用onDraw方法。

我們修改MyView代碼:構造方法中調用setWillNotDraw(false);

1 public MyView(Context context, AttributeSet attrs) {
2         super(context, attrs);
3         mPaint = new Paint();
4         mPaint.setColor(Color.RED);
5         mPaint.setStyle(Style.STROKE);
6         mPaint.setStrokeWidth(10); 
7         setWillNotDraw(false);
8         
9 }

運行程序,會看到手機屏幕中間畫出一個紅色的圓。還有一種方法我們在布局中給MyView添加背景同樣會達到調用onDraw方法的目的。

容器類布局(ViewGroup子類)為什么默認情況下不繪制背景和自身內容呢?答案是為了性能啊,大家想想容器類布局如果沒有背景,只是用來盛放子類有必要調用onDraw方法嗎?

有什么可繪制的嗎?子類會自己實現onDraw方法繪制自己內容的。

接下來我們總結一下draw流程的重點:

  • 容器類布局需要遞歸繪制其所包含的所有子View。

  • View中onDraw默認是空方法,需要子類自己實現來完成自身容內的繪制。

  • 容器類布局默認情況下不會調用onDraw方法,我們可以為其設置背景或者調用setWillNotDraw(false)方法來使其主動調用onDraw方法

最后附上draw流程圖:

 

好了,到此本篇就該結束了,用了很長的篇幅來探討View的繪制流程,希望對大家有用,廢話就不多說了,咱們下篇見。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM