Android View的工作原理

一、繪制流程

View的繪制流程是從ViewRoot的performTraversals方法開始的,經(jīng)過measure、layout、draw三個過程才能最終將一個View繪制出來,其中measure是用來測量View的寬高,layout是用來確定View在父容器的位置,draw則負責(zé)將View繪制在屏幕上,大致流程如下:

繪制流程.png

二、measure過程

1、MeasureSpec

從上圖可以了解到View在繪制過程中會調(diào)用到View的measure()方法,measure()方法接收兩個參數(shù):widthMeasureSpecheightMeasureSpec,分別用于確定視圖的寬度和高度的規(guī)格。
MeasureSpec代表一個32位的int值,高2位代表SpecMode(測量模式),低30位代表SpecSize(在某種測量模式下的規(guī)格大小)
SpecMode有三類:

  • UNSPECIFIED
    未指定模式,父容器不對View有任何限制,一般用于系統(tǒng)內(nèi)部,開發(fā)過程中不太會用到。
  • EXACTLY
    精確模式,父容器已經(jīng)檢測出View所需要的精確大小,這個時候View的最終大小就是SpecSize所指定的值。它對應(yīng)LayoutParams中的match_parent和具體的數(shù)值這兩種模式。
  • AT_MOST
    最大模式,父容器指定了一個可用大小,即SpecSize,View的大小不能大于這個值。它對應(yīng)LayoutParams中的wrap_content。
子視圖的MeasureSpec

widthMeasureSpecheightMeasureSpec這兩個參數(shù)的值通常是由父視圖傳遞給子視圖,再經(jīng)過計算得出來的,說明父視圖會在一定程度上決定子視圖的大小。觀察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);
    }

其中childWidthMeasureSpec 與childHeightMeasureSpec 都是通過getChildMeasureSpec的計算得出的,并且與父容器的MeasureSpec和子元素本身的LayoutParams有關(guān),再看看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
        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);
    }

以上的代碼可以用一個表格來表示:

普通View的MeasureSpce創(chuàng)建規(guī)則.png

總結(jié)如下:

  • 當(dāng)View采用固定寬/高時,不管父容器的Measure是什么,View的MeasureSpec都是精確模式并且大小遵循LayoutParams中的大小。
  • 當(dāng)View的寬/高是match_parent時,如果父容器是精確模式,那么View也是精確模式并且其大小是父容器的剩余空間;如果父容器是最大模式,那么View也是最大模式并且其大小不會超過父容器的剩余空間。
  • 當(dāng)View的寬/高是wrap_content時,不管父容器是最大模式還是精確模式,View的模式總是最大模式,并且其大小不會超過父容器的剩余空間。
  • UNSPECIFIED模式主要用于系統(tǒng)內(nèi)部多次Measure的情形,一般來說,不需要關(guān)注此模式。
根視圖的MeasureSpec

最外層的根視圖的widthMeasureSpec和heightMeasureSpec是在performTraversals()方法中獲取到:

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);  
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); 

其中的lp.width和lp.height在創(chuàng)建ViewGroup實例的時候就被賦值為MATCH_PARENT了,getRootMeasureSpec的代碼如下:

    private int getRootMeasureSpec(int windowSize, int rootDimension) {  
        int measureSpec;  
        switch (rootDimension) {  
        case ViewGroup.LayoutParams.MATCH_PARENT:  
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);  
            break;  
        case ViewGroup.LayoutParams.WRAP_CONTENT:  
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);  
            break;  
        default:  
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);  
            break;  
        }  
        return measureSpec;  
    }  

由此可見,當(dāng)rootDimension等于MATCH_PARENT時,MeasureSpec的SpecMode就等于EXACTLY,當(dāng)rootDimension等于WRAP_CONTENT時,MeasureSpec的SpecMode就等于AT_MOST,當(dāng)rootDimension為具體數(shù)值時,MeasureSpec的SpecMode就等于EXACTLY,與前面描述的一致。且MATCH_PARENT和WRAP_CONTENT時的specSize都是等于windowSize的,也就意味著根視圖總是會充滿全屏的。

2、View的measure過程

View的measure過程由其measure方法來完成,而measure方法是一個final方法,這意味著子類不能重寫此方法,而measure方法中調(diào)用的onMeasure方法才是真正去測量并設(shè)置View大小的地方,默認會調(diào)用getDefaultSize方法來獲取視圖的大小:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

public static int getDefaultSize(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:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

這里的MeasureSpec是由measure方法傳遞下來的,測量后調(diào)用setMeasuredDimension方法來設(shè)定測量后的大小,這樣一次measure過程就結(jié)束了,這是系統(tǒng)的默認測量方式,實際上我們可以重寫這個方法來改變測量方式,從而實現(xiàn)自定義View的測量。
值得注意的是,在重寫onMeasure方法的時候,需要注意設(shè)置好View的warp_content情況,按照自身情況來測量出實際所需大小,否則在布局中使用wrap_content就相當(dāng)于使用match_parent,從代碼可以看出,如果View在布局中使用wrap_content,那么它的specMode是AT_MOST,則寬/高等于specSize,從上面的“普通View的MeasureSpce創(chuàng)建規(guī)則”表中可知,這種情況下View的specSize是parentSize,即父容器當(dāng)前剩余空間大小,與使用match_parent效果一致。因此需要根據(jù)需求來判斷解決這個問題,例如使用默認大小等。

3、ViewGroup的measure過程

對于ViewGroup來說,除了完成自己的measure過程以外,還會去遍歷調(diào)用所有子元素的measure方法,各個子元素再遞歸去執(zhí)行這個過程。與View不同的是,ViewGroup是一個抽象類,并沒有定義其測量的具體過程,畢竟不同ViewGroup的子類有不同的布局特性,如RelativeLayout和LinearLayout,因此需要子類自己去實現(xiàn)ViewGroup提供了一個叫measureChildren的方法:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

measureChild與measureChildWithMargins不同的地方在于,measureChild沒有測量自己的margin屬性,而measureChildWithMargins有,當(dāng)需要使用到margin屬性時,還是需要使用measureChildWithMargins來測量。

4、測量結(jié)束

measure完成后,通過getMeasuredWidth/getMeasuredHeight方法就可以正確地獲取到View的測量寬/高,但是在這種情形下,在onMeasure方法中拿到的測量寬/高很可能是不準(zhǔn)確的,因為View需要多次measure才能確定自己的寬/高,前幾次測量過程中,得出的測量結(jié)果可能與最終結(jié)果不一致,因此最好還是在onLayout方法中去獲取View的測量寬/高或者最終寬/高。

三、layout過程

measure結(jié)束后,視圖的大小就已經(jīng)測量好了,接下來就是layout過程了。layout的作用是給視圖進行布局的,也就是確定視圖的位置。ViewRootd的performTraversals方法會在measure結(jié)束后繼續(xù)執(zhí)行,并調(diào)用layout方法來執(zhí)行此過程:

host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);  

layout方法接收四個參數(shù),分別代表著相對于當(dāng)前視圖的父視圖而言的左、上、右、下的坐標(biāo),在layout中會調(diào)用onLayout方法,但是,View的onLayout是一個空方法,因為View的位置應(yīng)該由父視圖ViewGroup來決定的,而ViewGroup中的onLayout方法是一個抽象方法,這是由于每個ViewGroup的布局方式不同,因此需要重寫這個方法來確定子元素的位置。
layout結(jié)束后,就可以通過getWidth和getHeight來得到其最終寬/高:

public final int getWidth() {
        return mRight - mLeft;
    }

public final int getHeight() {
        return mBottom - mTop;
    }

四、draw過程

draw過程比較簡單,它的作用是將View繪制到屏幕上面。View的繪制過程遵循如下幾步:

  • 繪制背景background.draw(canvas)
  • 繪制自己(onDraw)
  • 繪制children(disptchDraw)
  • 繪制裝飾(onDrawScrollBars)
    首先繪制背景,其實就是在XML中通過android:background屬性設(shè)置的圖片或顏色,當(dāng)然也可以在代碼中通過setBackgroundColor()、setBackgroundResource()等方法進行賦值;
    接下來是繪制自己,調(diào)用onDraw方法,使用畫布來繪制自己的內(nèi)容,自定義View的時候主要就是重寫這一個方法;
    接下來是繪制children,調(diào)用disptchDraw來繪制所有的子元素;
    最后是繪制裝飾,這一步的作用是對視圖的滾動條進行繪制,每一個View其實都有滾動條,只是有些控件沒有顯示出來。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容