View的繪製-measure流程詳解

StarkSong發表於2019-04-22

目錄

View的繪製-measure流程詳解

作用

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

基礎知識

在 measure 過程中,設計到 LayoutParams 和 MeasureSpec 這兩個知識點。 這裡我們簡單說一下,如果還有不明白之處,Google it!

LayoutParams

簡單來說就是佈局引數,包含了 View 的寬高等資訊。每一個 ViewGroup 的子類都有相對應的 LayoutParams,如:LinearLayout.LayoutParams、RelativeLayout.LayoutParams。可以看出 LayoutParams 是 ViewGroup 子類的內部類。

含義
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

View的繪製-measure流程詳解
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!

執行流程

注:以下涉及到原始碼的,都是版本27的。

我們知道,一個檢視的根 View 是 DecorView。在我們開啟一個 Activity 的時候,會將 DecorView 新增到 window 中,同時會建立一個 RootViewImpl物件,並將 RootViewImpl 物件和 DecorView 物件建立關聯。RootViewImplement 是連線 WindowManager 和 DecorView 的紐帶。具體 DecorView 詳解可以看 這篇文章

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

View的繪製-measure流程詳解

ViewGroup 中 measure 的執行流程

ViewGroup 本身是繼承 View 的,這是我們大家都知道的。在 ViewGroup 中並沒有找到 measure 方法,那麼就在它的父類 View 中找,具體原始碼如下:

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 的過程。具體總結如下:

View的繪製-measure流程詳解
以上就是 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 來生成的。

總結

View的繪製-measure流程詳解

參考文件:

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

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

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

相關文章