參考書籍:《Android開發(fā)藝術探索》 任玉剛
如有錯漏,請批評指出!
View的工作原理其實主要就是關于View繪制的三大流程——measure、layout和draw過程,在了解這三大流程之前,我們還需要了解一些基本概念,作為鋪墊。
ViewRoot 和 DecorView
ViewRoot對應于ViewRootImpl類,它是連接WindowManager和DecorView的紐帶,View的三大流程均是通過ViewRoot來完成的。在ActivityThread中,當Activity對象被創(chuàng)建完畢后,會將DecorView添加到Window中,同時會創(chuàng)建ViewRootImpl對象,并將ViewRootImpl對象和DecorView建立關聯(lián),這個過程可參看源碼:
root = new ViewRootImpl(View.getContext(), display); root.setView(view, wparams, panelParentView);
關于Android控件架構(gòu)的內(nèi)容前面講過 Android 控件架構(gòu)與自定義控件,這里不再贅述。
View的繪制流程是從ViewRoot的 performTraversals 方法開始的,它經(jīng)過 measure、layout、draw三個過程才能最終將一個View繪制出來。其中 measure 用來測量View的寬高,layout 用來確定View在父容器中的放置位置,而 draw 則負責將 View 繪制在屏幕上。具體可看下圖:
從圖中可以看到,整個頁面的繪制是層層進行的,performMeasure 方法會調(diào)用 measure 方法,measure 方法中會調(diào)用 onMeasure 方法,在 onMeasure 方法中會對所有的子View進行 measure 過程,這個時候 measure 流程就從父容器傳遞到子元素中了,接著子 View會重復 和父容器相同的 measure 過程,這樣反復之后就完成了整個 View 樹的遍歷。performLayout 和 performDraw 的傳遞流程也是如此,唯一不同的就是 performDraw 的傳遞過程是在 draw 方法中調(diào)用 dispatchDraw 方法從而對子View進行繪制。
- measure 過程決定了View的寬高,measure完成以后,可以通過 getMeasureWidth 和 getMeasureHeight 方法來獲取到View測量后的寬 / 高,在大多情況下,它都等于View最終的寬 / 高(存在特殊情況)。
- layout 過程決定了View 的四個頂點坐標和實際的View寬高,完成之后,可以通過 getTop、getButtom、getLeft 和 getRight來拿到四個位置參數(shù),并可以通過getWidth 和 getHeight方法拿到View的最終寬高。
- draw過程決定了View的顯示,只有draw方法完成后,View的內(nèi)容才能呈現(xiàn)在屏幕上。
關于 MeasureSpec
為了更好地理解 View 的測量過程,首先得理解 MeasureSpec 是個什么概念,從字面看,就是“測量規(guī)格”,在前面的博客中也簡單的介紹過 MeasureSpec 是什么,下面從源碼的角度具體進行分析。
-
MeasureSpec 是一個32位的int值,它的高2位用來表示SpecMode,即測量模式,低30位用來表示SpecSize,即規(guī)格大小。下面來看一下MeasureSpec類中主要方法及常量的定義:
public static class MeasureSpec { // 移位 private static final int MODE_SHIFT = 30; // 3的16進制表示 左移30位 private static final int MODE_MASK = 0x3 << MODE_SHIFT; // 0 左移30位 表示 UNSPECIFIED 測量模式 public static final int UNSPECIFIED = 0 << MODE_SHIFT; // 1左移30位 表示 EXACTLY 測量模式 public static final int EXACTLY = 1 << MODE_SHIFT; // 2左移30位 表示 AT_MOST 測量模式 public static final int AT_MOST = 2 << MODE_SHIFT; public static int makeMeasureSpec(int size, int mode) { if (sUseBrokenMakeMeasureSpec) { return size + mode; } else { return (size & ~MODE_MASK) | (mode & MODE_MASK); } } @MeasureSpecMode public static int getMode(int measureSpec) { //noinspection ResourceType return (measureSpec & MODE_MASK); } public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } }Measurespec通過將SpecMode 和 SpecSize打包成一個 int 值來避免過多的對象內(nèi)存分配,并且提供了相應的打包和解包方法。 SpecSize 和 SpecMode其實都是int值,通過二進制位的方式記錄為一個 MeasureSpec 值。
SpecMode有三種,分別如下:
- UNSPECIFIED
父容器不對 View 有任何限制,要多大給多大,這種情況一般用于系統(tǒng)內(nèi)部,表示一種測量的狀態(tài)。 - EXACTLY
父容器已經(jīng)測量出View所需要的精確大小,這個時候View的最終大小就是SpecSize所指定的值。它對應于LayoutParams中的 match_parent 和 具體的數(shù)值這兩種情況。
3.AT_MOST
父容器指定了一個可用大小,View的SpecSize不能大于這個值,具體是多大要看不同View自身的實現(xiàn)。它對應于LayoutParams中的 wrap_content。
- UNSPECIFIED
-
MeasureSpec 和 LayoutParams對應關系
我們需要知道,MeasureSpec并不是唯一由LayoutParams 決定的,LayoutParams需要和父容器一起才能決定View的MeasureSpec,即在View測量的時候,系統(tǒng)會將LayoutParams在父容器的約束下轉(zhuǎn)換成對應的MeasureSpec,然后再根據(jù)這個MeasureSpec來測量View的寬高。這一點可以通過閱讀源碼來驗證,下面來進行分析。首先當然要從頂級View(即DecorView)著手,對于DecorView,它的MeasureSpec由窗口的尺寸和其自身的Layoutparams來共同確定。
在ViewRootimpl類中的 measureHierarchy 方法中有一段代碼:
int childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);這段代碼展示了DecorView的MeasureSpec的創(chuàng)建過程,其中 desiredWindowWidth 和 desiredWindowHeight 是屏幕的尺寸。
下面再來看 getRootMeasureSpec 方法的實現(xiàn):
private static int getRootMeasureSpec(int windowSize, int rootDimension) { int measureSpec; switch (rootDimension) { case ViewGroup.LayoutParams.MATCH_PARENT: // Window can't resize. Force root view to be windowSize. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); break; case ViewGroup.LayoutParams.WRAP_CONTENT: // Window can resize. Set max size for root view. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: // Window wants to be an exact size. Force root view to be that size. measureSpec = MeasureSpec .makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec; }通過這段代碼,可以根據(jù)DecorView的LayoutParams中的寬高參數(shù)來劃分:
- LayoutParams.MATCH_PARENT:DecorView的寬高就是屏幕的寬高,SpecMode為精確模式(EXACTLY)。
- LayoutParams.WRAP_CONTENT:DecorView大小不定,但是不超過屏幕尺寸,SepcMode最大模式(AT_MOST)。
- 固定值:DecorView的寬高為LayoutParams中指定的寬高,SpecMode為精確模式(EXACTLY)。
而對于普通View,它的Measurespec由其父容器的MeasureSpec和自身的LayoutParams共同決定。View的measure過程由父ViewGroup傳遞而來,因此,我們先來看ViewGroup的 measureChildWithMargins 方法:
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); }這個方法會對子元素進行 measure,在調(diào)用子元素的measure方法之前,會通過getChildMeasureSpec 方法來得到子元素的MeasureSpec。從getChildMeasureSpec 方法的參數(shù)來看,子元素的MeasureSpec的創(chuàng)建與父容器的MeasureSpec 、padding值以及子元素本身的LayoutParams和margin值有關(想想外邊距內(nèi)邊距,很好理解)。
接下來,看一下ViewGroup的 getChildMeasureSpec 方法:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) { int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; switch (specMode) { // Parent has imposed an exact size on us // LayoutParams.MATCH_PARENT = -1 // LayoutParams.WRAP_CONTENT = -2 case MeasureSpec.EXACTLY: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size. So be it. resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent has imposed a maximum size on us case MeasureSpec.AT_MOST: if (childDimension >= 0) { // Child wants a specific size... so be it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size, but our size is not fixed. // Constrain child to not be bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent asked to see how big we want to be case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // Child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size... find out how big it should // be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } //noinspection ResourceType return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }這段代碼比較長,需要仔細推敲,不過邏輯還是很好理解的,要注意這個方法傳入的參數(shù) padding 實際上是父容器的padding值與View自身的margin值的和。因此,當子View的測量模式為AT_MOST時,這個最大值,也就是子View的最大可用空間是需要用父容器的MeasureSpec減去這個padding的,即:
將這段代碼的邏輯理清,其實就是下面這張表中的內(nèi)容:int size = Math.max(0, specSize - padding);表中的parentSize是指子View的最大可用空間(其實就是上面說的size)。總結(jié)一下,其實就是4條規(guī)則:
- 當View指定固定值作為寬高時,不管父容器的MeasureSpec為什么模式,View的SpecMode精確模式,并且都是寬高是指定值;
- 當View的寬高是match_parent時,如果父容器的SpecMode是精確模式,View的SpecMode也是精確模式,并且其寬高是父容器的可用空間,如果父容器是最大模式,那么View的SpecMode也是最大模式,并且寬高不超過父容器的可用空間;
- 當子View的寬高是wrap_content時,不管父容器的SpecMode精確模式還是最大模式,View的SpecMode都是最大模式,且其寬高不會超過父容器可用空間。
- 對于UNSPECIFIED模式,主要是系統(tǒng)內(nèi)部會用到,我們暫時不需要關注。
上一篇:Android學習筆記(四)| Android 控件架構(gòu)與自定義控件
下一篇:Android學習筆記(六)| View的工作原理(下)

