版權聲明:本文出自汪磊的博客,轉載請務必注明出處。
對於稍有自定義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的繪制流程,希望對大家有用,廢話就不多說了,咱們下篇見。