Android中View的測量和佈局過程

xinyang_code發表於2018-07-24

一直以來只是粗略的知道View的繪製會經過measure、layout到最終的draw三個過程,但對其中詳細的measure和layout過程一無所知,很影響對一些特殊場景下的佈局。

ViewRoot和DecorView

ViewRoot

ViewRoot對應ViewRootImpl類,它是連線WindowManager和DecorView的紐帶,View的三大流程均是通過ViewRootImpl來完成的.在ActivityThread中,當Activity物件被建立完畢後,會將DecorView新增到Window中,同時會建立ViewRootImpl物件,並將ViewRootImpl物件和DecorView建立關聯,原始碼如下:

root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams,panelParentView);
複製程式碼

View的繪製流程是從ViewRoot的performTraversals方法開始的,performTraversals會依次呼叫performMeasure、performLayout和performDraw三個方法,這三個方法分別完成頂級View的measure、layout和draw三大流程. performMeasure會呼叫measure方法,在measure中又會呼叫onMeasure方法,在onMeasure中則會對所有子元素進行measure過程,這時候measure流程就從父容器傳遞到子元素中了,這樣就完成了一次measure過程.接著子元素會重複父容器的measure過程,如此返回就完成了整個View樹的遍歷. layout和draw同上.

DecorView

DecorView作為頂級View,一般情況下它內部會包含一個豎直方向的LinearLayout,LinearLayout中又包含上下兩部分,上面是標題欄,下面是內容.在Activity中我們通過setContentView所設定的佈局檔案其實就是被加到內容中的.DecorView其實是一個FrameLayout,View層的事件都是先經過DecorView,然後才傳遞給我們的VIew.

Measure

Measure過程如上所述,會由ViewRootImpl發起,從頂層DecorView一層一層傳遞到最下層.View的寬高會受到父容器限制的影響,而父容器會在呼叫子View的onMeasure方法時把對子View寬高的限制傳遞過去.要了解這種限制規則,首先要了解一個類:MeasureSpec.

MeasureSpec

在測量過程中,系統會將View的LayoutParams根據父容器所施加的規則轉換成對應的MeasureSpec,然後在根據MeasureSpec來測量出View的寬高.

MeasureSpec代表一個32位的int值,高2位代表SpecMode(測量模式),低30位代表SpecSize(規格大小),通過將SpecMode和SpecSize打包成一個int值來避免過多的物件記憶體分配,為了方便操作,也提供了打包和解包的方法.

SpecMode

  1. UNSPECIFIED 父容器不對VIew有任何限制,要多大給多大,一般用於系統內部,表示一種測量的狀態.

  2. EXACTLY 父容器已經檢測出View所需的精確大小,這個時候View的最終大小就是SpecSize所指定的值.對應於LayoutParams中的match_parent和具體的數值這兩種模式.

  3. AT_MOST 父容器指定了一個可用大小即SpecSize,View的大小不能大於這個值,具體多少要看View的具體實現.對應LayoutParams中的wrap_content.

DecorView的測量

DecorView作為頂級View,和普通View的測量有所不同,其MeasureSpec由視窗尺寸和其自身的LayoutParams來決定,並遵守以下規則:

  • LayoutParams.MATCH_PARENT: 精確模式,大小就是視窗大小;
  • LayoutParams.WRAP_CONTENT: 最大模式,大小不定,但最大不能超過視窗大小;
  • 固定大小: 精確模式:大小為LayoutParams中指定的大小.

普通View的測量

對於普通的View,這裡指我們xml佈局中的View,View的measure過程由ViewGroup傳遞而來,先看一下ViewGroup中測量子View的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,在呼叫子元素的measure方法之前會先通過getChildMeasureSpec方法來得到子元素的MeasureSpec.很顯然子元素的MeasureSpec的建立與父容器的MeasureSpec和子元素本身的LayoutParams有關,具體看一下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
        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);
    }
複製程式碼

上述方法不難理解,它主要作用是根據父容器的MeasureSpec同時結合View本身的LayoutParams來確定子元素的MeasureSpec,引數中的padding是指父容器中已佔用的空間大小,因此子元素的可用大小為父容器的尺寸減去padding.

childLayoutParams\parentSpecMode EXACTLY AT_MOST UNSPECIFIED
dp/px EXACTLY/childSize EXACTLY/childSize EXACTLY/childSize
match_parent EXACTLY/parentSize AT_MOST/parentSize UNSPECIFIED/0
wrap_content AT_MOST/parentSize AT_MOST/parentSize UNSPECIFIED/0

在ViewGroup中計算好父容器期望子View的大小後,此時就呼叫了子View的onMeasure方法,可以看到onMeasure的預設實現中只是判斷了是否有設定背景或最小尺寸限制,如果有,則在無限制模式下將尺寸替換為最小尺寸.這裡也能看出minHeight minWidth不是在任何控制元件中都管用.需要注意的是onMeasure方法沒有返回值,需要呼叫setMeasuredDimension()來儲存我們的測量結果.獲取測量結果同理,getMeasuredDimension().

ViewGroup並沒有onMeasure的實現,一般都由具體的實現類根據自己的業務來實現onMeasure方法,比如LinearLayout.

Measure流程總結

  1. 通過自身的MeasureSpec和子view的LayuoutParams,生成子view的MeasureSpec。這一步呼叫的是getChildMeasureSpec(int spec, int padding, int childDimension)方法。

  2. 呼叫子view的measure(int widthMeasureSpec, int heightMeasureSpec)方法,來測量子view的寬高。

  3. 在子view測量結束之後,根據情況來計算自身的寬高。假如自己的MeasureSpec是Exactly的,那麼可以直接將SpecSize中的大小作為自己的寬或高;如果是wrap_content或者其他的,那麼就需要在每一個子view測量完之後,呼叫子view的getMeasuredHeight()和getMeasuredWidth()來獲得子view測量的結果,然後根據情況計算自己的寬高。

  4. 使用setMeasuredDimension(int measuredWidth, int measuredHeight)方法儲存測量的結果。

自定義Measure

我們如果要自定義View的onMeasure過程的話一般有兩種方式:

  • 修改測量結果 直接重寫onMeasure方法,在super.onMeasure之後修改我們期望的尺寸並儲存就好;

  • 自定義測量過程 不呼叫super.onMeasure,直接計算我們期望的尺寸,並呼叫resolveSize()來讓計算出的尺寸符合父容器的要求,最後別忘了儲存.

Layout

Layout過程相對於Measure來說就比較簡單了,同樣只需要關注Layout()onLayout()方法即可,可以簡單理解為在layout方法中確定自己的位置,onLayout方法中確定所有子元素的位置.View和ViewGroup中都有layout(),但都沒有實現onLayout(),因為不同型別的佈局對onLayout()的要求是不一樣的.

當我們自定義了一個ViewGroup的時候,會先確定這個ViewGroup的位置,然後,通過重寫 onLayout() 方法,遍歷所有的子元素並呼叫其 layout() 方法,在layout()方法中onLayout()方法又會被呼叫。ViewGroup就是通過這個過程,遞迴地對所有子View進行了佈局。來看一下View類中的layout()方法的原始碼:

/**
 * 本方法用來給一個View和它的所有子View設定尺寸和位置;
 * 這是Android佈局機制的第二個階段(第一個階段是測量);
 * 在這個階段中,每個父容器都呼叫layout()方法來定位它的子View;
 * 子類不能重寫這個方法,而應該重寫onLayout()方法;
 * 在onLayout()方法中呼叫layout()方法來設定每個子View的位置。
 *
 * @param l 相對於父容器左邊的距離
 * @param t 相對於父容器上邊的距離
 * @param r 相對於父容器右邊的距離
 * @param b 相對於父容器下邊的距離
 */
@SuppressWarnings({"unchecked"})
public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }

    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;

    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);

        if (shouldDrawRoundScrollbar()) {
            if(mRoundScrollbarRenderer == null) {
                mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
            }
        } else {
            mRoundScrollbarRenderer = null;
        }

        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLayoutChangeListeners != null) {
            ArrayList<View.OnLayoutChangeListener> listenersCopy =
                    (ArrayList<View.OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
            int numListeners = listenersCopy.size();
            for (int i = 0; i < numListeners; ++i) {
                listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
            }
        }
    }
    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
複製程式碼

從原始碼中可以看出這個方法的大致流程:首先通過setFrame()方法來設定View的四個位置元素的位置,即初始化mLeft、mTop、mRight和mBottom這四個值。View的四個頂點一旦確定,那麼View在父容器中的位置也就確定了;接著會呼叫 onLayout() 方法,這個方法的用途是父容器確定子元素的位置。

在自定義佈局的時候,我們的任務就是:遍歷所有的子元素,確定它們的大小和位置(大小主要是通過 getMeasuredWidth() 和 getMeasuredHeight() 兩個方法,取出在 onMeasure() 方法中測量得到的寬/高;位置需要自行設定),然後呼叫 view.layout() 方法或直接呼叫ViewGroup中的方法 setChildFrame() 方法(setChildFrame()方法內部呼叫的就是view.layout()方法),將子元素佈局到這個ViewGroup中。

 最後還需要說明一點,“測量寬/高” 和 “最終寬/高” 是兩個不同的概念。測量寬/高是在onMeasure()方法中測量得到的寬度或高度,而最終寬/高是在onLayout()方法中最終放置的子元素的寬度或高度。在View的預設實現中,View的測量寬/高和最終寬/高是相等的,但是測量寬/高的賦值時機較早。

Layout總結

  1. 父layout在自己的onLayout()函式中負責對子view進行佈局,安排子view的位置,並且將測量好的位置(上下左右位置)傳給子view的layout()函式。

  2. 子view在自己的layout()函式中使用setFrame()函式將位置應用到檢視上,並且將新位置和舊位置比較來得出自己的位置和大小是否發生了變化(changed),之後再呼叫onLayout()回撥函式。

  3. 如果此時子view中還有其他view,那麼就在自己的onLayout()函式中對自己的子view進行第1補的佈局操作,如此迴圈,只到最後的子view中沒有其他view,這樣就完成了所有view的佈局。

參考:

《Android開發藝術探索》

Android自定義view之measure、layout、draw三大流程

【Android - 自定義View】之View的layout過程解析

相關文章