View的繪製-measure流程詳解

目錄

做用

用於測量View的寬高,在執行 layout 的時候,根據測量的寬高去肯定自身和子 View 的位置。java

基礎知識

在 measure 過程當中,設計到 LayoutParams 和 MeasureSpec 這兩個知識點。 這裏咱們簡單說一下,若是還有不明白之處,Google it!android

LayoutParams

簡單來講就是佈局參數,包含了 View 的寬高等信息。每個 ViewGroup 的子類都有相對應的 LayoutParams,如:LinearLayout.LayoutParams、RelativeLayout.LayoutParams。能夠看出 LayoutParams 是 ViewGroup 子類的內部類。ide

含義
LayoutParams.MATCH_PARENT 等同於在 xml 中設置 View 的屬性爲 match_parent 和 fill_parent
LayoutParams.WRAP_CONTENT 等同於在 xml 中設置 View 的屬性爲 wrap_content

MeasureSpec

MeasureSpec 是 View 的測量規則。一般父控件要測量子控件的時候,會傳給子控件 widthMeasureSpec 和 heightMeasureSpec 這兩個 int 類型的值。這個值裏面包含兩個信息,SpecModeSpecSize。一個 int 值怎麼會包含兩個信息呢?咱們知道 int 是一個4字節32位的數據,在這兩個 int 類型的數據中,前面高2位是 SpecMode ,後面低30位表明了 SpecSize源碼分析

mode 有三種類型: UNSPECIFIEDEXACTLYAT_MOST

測量模式 應用
EXACTLY 精準模式,當 width 或 height 爲固定 xxdp 或者爲 MACH_PARENT 的時候,是這種測量模式
AT_MOST 當 width 或 height 設置爲 warp_content 的時候,是這種測量模式
UNSPECIFIED 父容器對當前 View 沒有任何顯示,子 View 能夠取任意大小。通常用在系統內部,好比:Scrollview、ListView。

咱們怎麼從一個 int 值裏面取出兩個信息呢?別擔憂,在 View 內部有一個 MeasureSpec 類。這個類已經給咱們封裝好了各類方法:佈局

//將 Size 和 mode 組合成一個 int 值
int measureSpec = MeasureSpec.makeMeasureSpec(size,mode);
//獲取 size 大小
int size = MeasureSpec.getSize(measureSpec);
//獲取 mode 類型
int mode = MeasureSpec.getMode(measureSpec);
複製代碼

具體實現細節,能夠查看源碼,or Google it!this

執行流程

注:如下涉及到源碼的,都是版本27的。spa

咱們知道,一個視圖的根 View 是 DecorView。在咱們開啓一個 Activity 的時候,會將 DecorView 添加到 window 中,同時會建立一個 RootViewImpl對象,並將 RootViewImpl 對象和 DecorView 對象創建關聯。RootViewImpl 是鏈接 WindowManager 和 DecorView 的紐帶。具體 DecorView 詳解能夠看 這篇文章設計

View的繪製流程就是從 RootViewImpl 開始的。在它的 performTraversals()方法中執行了 performMeasure()performLayoutperformDraw方法。而這三個方法又分別執行了view.measure()view.layout()view.draw()方法,從而開始執行整個 View 樹的繪製流程 3d

ViewGroup 中 measure 的執行流程

ViewGroup 自己是繼承 View 的,這是咱們你們都知道的。在 ViewGroup 中並無找到 measure 方法,那麼就在它的父類 View 中找,具體源碼以下:code

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    /*....省略代碼....*/
    if (forceLayout || needsLayout) {
     /*....省略代碼....*/
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            // measure ourselves, this should set the measured dimension flag back
            //執行 onMeasure 方法
            onMeasure(widthMeasureSpec, heightMeasureSpec);
        }
        /*....省略代碼....*/
     
    }
    /*....省略代碼....*/
}
複製代碼

咱們能夠看出,measure 方法是被 final 修飾了,子類不能重寫。measure 方法中調用了 onMeasure 方法。

而後咱們繼續尋找 onMeasure 方法,會發如今 ViewGroup 中並無實現 onMeasure 方法,只有在 View 中發現了 onMeasure 方法。WTF?難道 ViewGroup 的 onMeasure 也會走 View 中的方法?並非的,ViewGroup 自己是一個抽象類,在 Android SDK 中有不少它的子類,如:LinearLayout、RelativeLayout、FrameLayout等等,這些控件的特性都是不同的,測量規則天然也都不同。它們都各自實現了 onMeasure 方法,而後去根據本身的特定測量規則進行控件的測量。(PS:若是咱們的自定義控件繼承 ViewGroup 的時候,必定要重寫 onMeasure 方法的,根據需求來制定測量規則)

這裏咱們以 LinearLayout 爲例,來進行源碼分析:

//LinearLayout 類
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (mOrientation == VERTICAL) {
    //若是方向是垂直方向,就進行垂直方向的測量
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
    //進行水平方向的測量
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}
複製代碼

measureVertical 和 measureHorizontal 過程相似,咱們對 measureVertical 進行分析。(如下源碼有所刪減)

//LinearLayout 類
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    mTotalLength = 0;
    float totalWeight = 0;

    final int count = getVirtualChildCount();
    //獲取 LinearLayout 的寬高模式 SpecMode
    final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    final int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    boolean skippedMeasure = false;

    // See how tall everyone is. Also remember max width.
    //遍歷子 View ,查看每個子類有多高,而且記住最大的寬度。
    for (int i = 0; i < count; ++i) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
        //measureNullChild() 恆返回 0,
            mTotalLength += measureNullChild (i);
            continue;
        }
        //若是子控件時 GONE 狀態,就跳過,不進行測量。
        //也能夠看出,若是子 View 是 INVISIBLE 也是要測量大小的。
        if (child.getVisibility() == View.GONE) {
        //getChildrenSkipCount 也是恆返回爲 0 的。
           i += getChildrenSkipCount(child, i);
           continue;
        }

        //獲取子控件的參數信息。
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();

        totalWeight += lp.weight;
        //子控件是否設置了權重 weight 
        final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
        if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
            final int totalLength = mTotalLength;
            mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
            //若是設置了權重,就將 skippedMeasure 標記爲 true。
            //後面會根據 skippedMeasure 的值和其餘條件來決定是否進行從新繪製。
            //因此說,在 LinearLayout 中使用了 weight 權重,會致使測量兩次,比較耗時。
            //能夠考慮使用 RelativeLayout 或者 ConstraintLayout
            skippedMeasure = true;
        } else {
            if (useExcessSpace) {
                lp.height = LayoutParams.WRAP_CONTENT;
            }

           //計算已經使用過的高度
            final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
            /*這句代碼是關鍵,從字面意思就能夠理解出,該方法是在 layout 以前進行子 View 的測量。*/
            measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                    heightMeasureSpec, usedHeight);
        }
    }
}
複製代碼

那麼咱們在查看 measureChildBeforeLayout 方法:

//LinearLayout 類
void measureChildBeforeLayout(View child, int childIndex, int widthMeasureSpec, int totalWidth, int heightMeasureSpec, int totalHeight) {
    measureChildWithMargins(child, widthMeasureSpec, totalWidth,
            heightMeasureSpec, totalHeight);
}
複製代碼

再查看 measureChildWithMargins 方法,最終來到了 ViewGroup 類:

//ViewGroup 類
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) {
        /*獲取子 View 的佈局參數 MarginLayoutParams 能夠獲取子 View 設置的 margin 屬性。*/
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    //獲取子 View 寬度的 MeasureSpec 值。
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    //獲取子 View 高度的 MeasureSpec 值。
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製代碼

在 ViewGroup 中還有一個方法爲 measureChild(int widthMeasureSpec, int heightMeasureSpec)。這個方法和 measureChildWithMargins 做用一致,都是生成子 View 的 measureSpec。只是傳參不一樣。

裏面在獲取子 View 寬高屬性的時候,都是經過 getChildMeasureSpec 方法來獲取的。這個方法是 ViewGroup 具體實現根據自身的 measureSpec 和子 View 的 LayoutParams 來設置子 View 的 measureSpec 的主要過程。

//ViewGroup 類
/** * @param spec 父類的 measureSpec * @param padding 父類的 padding + 子類的 margin * @param childDimension 子 View 的 LayoutParams.width/LayoutParams.height 屬性 */
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    //獲取父控件的測量模式 specMode
    int specMode = MeasureSpec.getMode(spec);
    //獲取父控件的測量大小 SpecSize
    int specSize = MeasureSpec.getSize(spec);
    //獲取父控件剩餘的寬度/高度大小
    int size = Math.max(0, specSize - padding);
    //子 View 的測量大小
    int resultSize = 0;
    //子 View 的測量模式
    int resultMode = 0;

    switch (specMode) {
    // 父控件的寬高模式是精準模式 EXACTLY
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            //若是子 View 的寬/高是具體的值(具體的 xxdp/px)
            //模式 mode 就設置爲精準模式 EXACTLY,大小 size 就是具體設置的大小
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            //若是子 View 的寬/高是 MATCH_PARENT
            //模式 mode 就設置爲精準模式 EXACTLY,大小 size 就是父控件剩餘的空間
            resultSize = size;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            //若是子 View 的寬/高是 WRAP_CONTENT
            /*模式 mode 就設置爲精準模式 AT_MOST,大小 size 就是父控件剩餘的空間, 子控件能夠在在這個size大小範圍內設置寬高*/
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent has imposed a maximum size on us
    //父控件測量模式爲 AT_MOST,會給子 View 一個最大的值
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            //若是子 View 的寬/高是具體的值(具體的 xxdp/px)
            //模式 mode 就設置爲精準模式 EXACTLY,大小 size 就是具體設置的大小
            resultSize = childDimension;
            resultMode = MeasureSpec.EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            //若是子 View 的寬/高是 MATCH_PARENT
            /*模式 mode 就設置爲精準模式 AT_MOST,大小 size 就是父控件剩餘的空間, 子控件能夠在在這個size大小範圍內設置寬高*/
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            //若是子 View 的寬/高是 MATCH_PARENT
            /*模式 mode 就設置爲精準模式 AT_MOST,大小 size 就是父控件剩餘的空間, 子控件能夠在在這個size大小範圍內設置寬高*/
            resultSize = size;
            resultMode = MeasureSpec.AT_MOST;
        }
        break;

    // Parent asked to see how big we want to be
    //父控件不限制子 View 的寬高,通常用於 ListView、Scrollview
    //平時基本不用,暫不分析
    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;
    }
    //生成子 View 的 measSpec
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
複製代碼

以上就是 ViewGroup 根據自身 measureSpec 和 子 View 的 LayoutParams 生成子 View 的 measureSpec 的過程。具體總結以下:

以上就是 LinearLayout 測量子控件寬高的過程。

從上述表格咱們也能夠看出,當咱們在自定義控件繼承 View 的時候,仍是要重寫 View 的 onMeasure 方法來處理 wrap_content 的狀況,若是不處理 wrap_content 的狀況,wrap_content 的效果是和 match_parent 同樣的,都是填充滿父控件。能夠在 xml 佈局中直接添加一個 <View android:layout_width="match_parent" android:layout_height="wrap_content"/> 控件自行感覺一下。

LinearLayout 測量完子控件後,根據子控件的寬高來設置自身的寬高:

void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
    // Add in our padding
    //添加自身的 padding 值
    mTotalLength += mPaddingTop + mPaddingBottom;

    int heightSize = mTotalLength;

    // Check against our minimum height
    //從 最小建議高度 和 heightSize 中取最大值,getSuggestedMinimumHeight 在後面有分析
    heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

    int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
    /*....省略代碼....*/
    //遍歷完子控件後,來設置自身的寬高
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            heightSizeAndState);
}
複製代碼
//若是 LinearLayout 高爲具體值,heightSizeAndState 就是具體的值
//不然是 子控件 的高度之和,可是也不能超過它的父容器的剩餘空間。
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    final int result;
    switch (specMode) {
        case MeasureSpec.AT_MOST:
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        case MeasureSpec.UNSPECIFIED:
        default:
            result = size;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}
複製代碼

至此,咱們能夠得知,當 ViewGroup 生成子 View 寬/高的 measureSpec 後,開始調用子 View 進行測量。若是子 View 繼承了 ViewGroup 就重複執行上述流程(各個不一樣的 ViewGroup 子類執行各自的 onMeasure 方法);若是是具體的 View,就開始執行具體 View 的 measure 過程。最後根據子控件的寬高和其餘條件來決定自身的寬高。

View 中 measure 的執行流程

View 的 measure 具體源碼在 ViewGroup 中已經分析過,這裏主要分析 View 的 onMeasure 過程。

//View 類
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //經過 getDefaultSize 獲取寬高大小,設置爲測量值。
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
複製代碼

getDefaultSize 具體源碼

//View 類
/** * @param size 經過 getSuggestedMinimumWidth 獲取的建議最小寬度 * @param measureSpec 經過父控件生成的 measureSpec */
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:
    //若是是 UNSPECIFIED 就設置爲建議最小值
        result = size;
        break;
    /*不然就都設置爲經過父控件生成的值(若是子控件爲具體的 xxdp/px值,就是具體的值,若是不是就是父控件的剩餘空間。具體能夠查看上面的分析)*/
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}
複製代碼

//建議最小的值

//View 類
protected int getSuggestedMinimumWidth() {
    //判斷是否有設置背景 Background 若是沒有,建議最小值就是設置的 minWidth;
    //若是有,就取 mMinWidth 和 背景最小值 二者的最大值。
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
複製代碼

背景最小值是多少呢?點擊查看源碼,就來到了 Drawable 類。

//Drawable 類
public int getMinimumWidth() {
    //首先獲取 Drawable 的原始寬度
    final int intrinsicWidth = getIntrinsicWidth();
    //若是有原始寬度,就返回原始寬度;若是沒有,就返回 0
    //注: 好比 ShapeDrawable 就沒有原始寬度,BitmapDrawable 有原始寬高(圖片尺寸)
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
複製代碼

至此,View的 measure 就分析完了。

DecorView 的 measureSpec 計算邏輯

可能咱們會有疑問,若是全部子控件的 measureSpec 都是父控件結合自身的 measureSpec 和子 View 的 LayoutParams 來生成的。那麼做爲視圖的頂級父類 DecorView 怎麼獲取本身的 measureSpec 呢?下面咱們來分析源碼:(如下源碼有所刪減)

//ViewRootImpl 類
private void performTraversals() {
    //獲取 DecorView 寬度的 measureSpec 
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    //獲取 DecorView 高度的 measureSpec
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    // Ask host how big it wants to be
    //開始執行測量
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製代碼
//ViewRootImpl 類
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;
}
複製代碼

windowSize 是 widow 的寬高大小,因此咱們能夠看出 DecorView 的 measureSpec 是根據 window 的寬高大小和自身的 LayoutParams 來生成的。

總結

參考文檔:

《Android開發藝術探索》第四章-View的工做原理

自定義View Measure過程 - 最易懂的自定義View原理系列(2)

圖解View測量、佈局及繪製原理

相關文章
相關標籤/搜索