Android學習筆記(五)| View的工作原理(上)

參考書籍:《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進行繪制。

  1. measure 過程決定了View的寬高,measure完成以后,可以通過 getMeasureWidth 和 getMeasureHeight 方法來獲取到View測量后的寬 / 高,在大多情況下,它都等于View最終的寬 / 高(存在特殊情況)。
  2. layout 過程決定了View 的四個頂點坐標和實際的View寬高,完成之后,可以通過 getTop、getButtom、getLeft 和 getRight來拿到四個位置參數(shù),并可以通過getWidth 和 getHeight方法拿到View的最終寬高。
  3. 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有三種,分別如下:

    1. UNSPECIFIED
      父容器不對 View 有任何限制,要多大給多大,這種情況一般用于系統(tǒng)內(nèi)部,表示一種測量的狀態(tài)。
    2. EXACTLY
      父容器已經(jīng)測量出View所需要的精確大小,這個時候View的最終大小就是SpecSize所指定的值。它對應于LayoutParams中的 match_parent 和 具體的數(shù)值這兩種情況。
      3.AT_MOST
      父容器指定了一個可用大小,View的SpecSize不能大于這個值,具體是多大要看不同View自身的實現(xiàn)。它對應于LayoutParams中的 wrap_content。
  • 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ù)來劃分:

    1. LayoutParams.MATCH_PARENT:DecorView的寬高就是屏幕的寬高,SpecMode為精確模式(EXACTLY)。
    2. LayoutParams.WRAP_CONTENT:DecorView大小不定,但是不超過屏幕尺寸,SepcMode最大模式(AT_MOST)。
    3. 固定值: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的,即:

    int size = Math.max(0, specSize - padding);
    

    將這段代碼的邏輯理清,其實就是下面這張表中的內(nèi)容:

    表中的parentSize是指子View的最大可用空間(其實就是上面說的size)。總結(jié)一下,其實就是4條規(guī)則:

    1. 當View指定固定值作為寬高時,不管父容器的MeasureSpec為什么模式,View的SpecMode精確模式,并且都是寬高是指定值;
    2. 當View的寬高是match_parent時,如果父容器的SpecMode是精確模式,View的SpecMode也是精確模式,并且其寬高是父容器的可用空間,如果父容器是最大模式,那么View的SpecMode也是最大模式,并且寬高不超過父容器的可用空間;
    3. 當子View的寬高是wrap_content時,不管父容器的SpecMode精確模式還是最大模式,View的SpecMode都是最大模式,且其寬高不會超過父容器可用空間。
    4. 對于UNSPECIFIED模式,主要是系統(tǒng)內(nèi)部會用到,我們暫時不需要關注。

上一篇:Android學習筆記(四)| Android 控件架構(gòu)與自定義控件
下一篇:Android學習筆記(六)| View的工作原理(下)

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容