Android View的Measure測量流程全解析

舒大飛發表於2018-04-15

    相信絕大多數Android開發者都有自定義View來滿足各種各樣需求的經歷,也知道一個View的繪製展示要經過measure、layout、draw三大流程,三者中measure的過程相比是稍微複雜一點點的。這篇文章作為一個Android基礎的分享,分享一下view/viewGroup measure的過程,view/viewGroup是如何通過measure來確定自己的寬高,最後通過自定義一個流式佈局來實踐一下。

layout的過程本質上就是計算設定自己的座標或者自己child的座標,而draw需要畫布和畫筆以及提供的豐富的api來繪製你想要的效果。

本文主要分為以下三部分:

  • 什麼是MeasureSpec,它的作用是什麼
  • measure過程是什麼樣的,它是如何確定一個View/ViewGroup的寬高
  • 重寫onMeasure()方法來自定義一個流式佈局

1. 什麼是MeasureSpec,它的作用是什麼

MeasureSpec的作用

    我們知道View是通過onMeasure()來確定自己的寬高的(ViewGroup是個抽象類繼承自View,它並沒有重寫onMeasure(),所以如果自定義ViewGroup的時候沒有重寫onMeasure(),它最終調的還是View的onMeasure()方法),那麼假定我們沒有重寫onMeasure()方法,那麼View的寬高是怎樣的呢? View.onMeasure:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
複製程式碼

顯然是通過setMeasureDimension()方法來確定寬高的,來看一下這個方法:

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        ```
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
複製程式碼
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        //賦值給寬高成員變數,寬高確定,此時外界就可以通過getMeasuredWidth()、getMeasuredHeight()來獲取View的寬高了
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }
複製程式碼

    通過以上程式碼我們可以知道,View的onMeasure()裡通過setMeasuredDimension(int measuredWidth, int measuredHeight)方法就可以設定View的寬高,那麼裡面的兩個寬高引數是怎麼來的呢?接著看getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec):

public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        //取出MeasureSpec中的specMode和specSize
        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:
            //將specSize賦值給result
            result = specSize;
            break;
        }
        return result;
    }
複製程式碼

    終於我們的主角MeasureSpec登場了,從程式碼可以看出,最終設定的寬高也就是從MeasureSpec中取出的specSize。所以綜上所訴,在沒有重寫onMeasure()方法的情況下,MeasureSpec就決定了View/ViewGroup的寬高。

MeasureSpec基礎知識

接下來介紹一些MeasureSpec的基礎知識,讓我們更加了解什麼是MeasureSpec:

從getDefaultSize方法中也可以看出,MeasureSpec中包含兩部分,一個是SpecMode(測量模式),一個是SpecSize(某種測量模式下的規格大小)。它是用一個32位的int值來表示的,高2位程式碼SpecMode,低30位代表SpecSize。

specMode主要分為三種:

模式 說明
EXACTLY 設定了精確的寬高。如width、height設定了具體值或者設定為 match_parent,都屬於這種模式
AT_MOST width、height設定為wrap_content則屬於這種模式
UNSPECIFIED 以上兩種模式是我們佈局裡常見的,最大也不會大過父佈局,而這種模式一般用於系統, 父容器不對View有任何限制

MeasureSpec是如何生成的

    通過上面我們知道了在不重寫onMeasure()的情況下,一個View/ViewGroup的MeasureSpec就決定了這個View/ViewGroup的寬高,顯然這個MeasureSpec是這個View/ViewGroup的父容器在呼叫子View的measure()方法時傳進來的,也就是說一個View/ViewGroup的MeasureSpec是由其父容器生成的,那麼是怎麼生成的呢?裡面的SpecSize和SpecMode是由什麼決定的呢?

    這裡由於程式碼比較多就不貼了,父容器通過調ViewGroup中的getChildMeasureSpec()來生成子View的MeasureSpec。getChildMeasureSpec()中主要是通過父容器的MeasureSpec以及子Views設定的寬高來共同決定子View的MeasureSpec中的SpecMode和SpecSize。

getChildMeasureSpec()程式碼裡的生成規則:

1.當子View的寬高設定的是具體數值時,顯然我們可以直接拿到子View的寬高,則子View寬高就確定了,不用再去考慮父容器的SpecMode了,此時子View的SpecMode為EXACTLY,SpecSize就是設定的寬高。

2.當子View的寬高設定的是match_parent, 則不管父容器的SpecMode是什麼模式,子View的SpecSize就等於父容器的寬高,而子View的SpecMode隨父容器的SpecMode。(這裡沒有考慮UNSPECIFIED模式,如果父容器是UNSPECIFIED模式,則子View SpecSize為0,SpecMode為UNSPECIFIED)

3.當子View的寬高設定的是wrap_content,因為這種情況父容器實在不知道子View應該多寬多高,所以子View的SpecSize給的是父容器的寬高,也就是說只是給子View限制了一個最大寬高,而子View的SpecMode是AT_MOST模式。(這裡沒有考慮UNSPECIFIED模式,如果父容器是UNSPECIFIED模式,則子View SpecSize為0,SpecMode為UNSPECIFIED)。

    通過上面的解析我們可以知道,當你給一個View/ViewGroup設定寬高為具體數值或者match_parent,它都能正確的顯示,但是如果你設定的是wrap_content,則預設顯示出來是其父容器的大小,如果你想要它正常的顯示為wrap_content,則你就要自己重寫onMeasure()來自己計算它的寬高度並設定。所以我們平常自定義View/ViewGroup的時候之所以要重寫onMeasure(),就是為了能讓wrap_content達到效果。

2. measure過程是什麼樣的,它是如何確定一個View/ViewGroup的寬高

    我們知道,整個繪製流程是從ViewRootImpl類中performTraversals()開始的,這裡面分別執行performMeasure、performLayout、performDraw來完成整個繪製的三大流程。而三大流程都是至頂向下,今天這裡只說measure的過程。

    這裡以DecorView(根View)面放著一個ViewGroup(ViewGroupA)ViewGroup裡面放著一個View(ViewB)為例來說明整個測量的流程:

1. ViewRootImpl.performTraversals()->performMeasure():

這裡面會調getRootMeasureSpec()根據手機螢幕的寬高和DecorView的LayoutParams生成DecorView的MeasureSpec,然後呼叫DecorView的measure()開始DecorView的測量

2.DecorView.measure()->onMeasure():

DecorView繼承自FrameLayout,所以會走到FrameLayout的onMeasure(),onMeasure()裡調measureChild()來根據上面說的規則為ViewGroupA生成MeasureSpec,並通過ViewGroupA.measure()開始ViewGroupA的測量

3.ViewGroupA.measure()->onMeasure():

這是我們自定義的一個ViewGroup(繼承自ViewGroup) 假如我們沒有重寫onMeasure()的話,則預設調的是View.onMeasure(),則不會發起對子View的measure,它裡面的子View也就不會被測量(0),而這個ViewGroup如果沒有設定具體寬高的話,(wrap_content)則ViewGroup展示的就是父容器的寬高(根據上面說的MeasureSpec生成規則)。

    所以如果我們繼承自ViewGroup來自定義一個ViewGroup的話,是肯定要重寫onMeasure()的,裡面要呼叫measureChild()來為子View生成MeasureSpec並調child.measure()開始對child的測量,這樣子View才能被測量顯示。而如果我們要使設定的wrap_content生效,還要根據子View測量結果進行計算從而得到自己的寬高,最後通過調setMeasuredDimension(int measuredWidth, int measuredHeight)來設定自己的寬高,從而達到wrap_content的效果。

4. ViewB.measure()->onMeasure():

View的測量相對於ViewGroup要簡單點,因為不用去Measure child,但是一樣的,如果要使wrap_conten生效需自己重寫onMeasure()計算。

3.重寫onMeasure()方法來自定義一個流式佈局

關於流式佈局,也叫自動換行佈局,一行放不下時會自動折行,效果如下:

Android View的Measure測量流程全解析

具體實現地址:github.com/zhengcx/Lin…

    通過上面的measure流程分析,這個效果應該還是比較好實現的,寬度測量上其實不需要我們太關心,直接拿MeasureSpec中的SpecSize就行了,所以主要是高度上的測量,我們要做的其實也是上面說的幾點:

1.根據規則為子View生成MeasureSpec,並迴圈測量子View (ViewGroup.measureChild()裡做的事)

2.拿到測量好的子View的寬高,來計算自己wrap_content時應該呈現的寬高。像這個例子主要是根據子View的寬度來判斷是否需要折行,如果折行,則ViewGroup的高度也就隨之變大,總之需要去計算ViewGroup應該展示的高度。

3.通過調setMeasuredDimension(int measuredWidth, int measuredHeight)來設定自己的寬高。

具體實現見github:github.com/zhengcx/Lin…

相關文章