View onMeasure


視圖測量的入口在ViewRootImpl類,一次performTraversals過程,測量、布局和繪制流程,從它的measureHierarchy方法開始,分析視圖測量過程。

private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, 
            final int desiredWindowHeight){
    int childWidthMeasureSpec;
    int childHeightMeasureSpec;
    boolean windowSizeMayChange = false;

    boolean goodMeasure = false;
    if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) {
        ...
    }

    if (!goodMeasure) {//第二步
        childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
        childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()){
            windowSizeMayChange = true;
        }
    }
    return windowSizeMayChange
}

樹形結(jié)構(gòu)層次測量,入?yún)?strong>desiredWindowXxx代表希望獲取的窗體寬高。若第一次觸發(fā)performTraversals方法,將其設(shè)置為屏幕像素,我使用的測試手機分辨率是1080x1920。
ViewRootImpl#performTraversals方法代碼段。

final Rect mWinFrame; //ViewRootImpl類中定義
Rect frame = mWinFrame;
if (mFirst) {
    ...
    DisplayMetrics packageMetrics = mView.getContext().
                    getResources().getDisplayMetrics();
    desiredWindowWidth = packageMetrics.widthPixels;
    desiredWindowHeight = packageMetrics.heightPixels;
}else {
    ...
    desiredWindowWidth = frame.width();
    desiredWindowHeight = frame.height();
    if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
        mFullRedrawNeeded = true;
        mLayoutRequested = true;
        windowSizeMayChange = true;
    }
}

初始寬高設(shè)置屏幕值是分辨率.PNG
由圖可見,寬高是1080x1794,因為需要去除狀態(tài)欄,高度heightPixel比1980小一些。第一次觸發(fā)時,內(nèi)部的mWidth、mHeight還未設(shè)置,都是0。返回的windowSizeMayChange是true。
第一次時,繼續(xù)relayoutWindow方法,frame初始化(0,0,1080,1920)。向mWidth與mHeight賦值。
ViewRootImpl#performTraversals方法代碼段。

//設(shè)置mWidth、mHeight與mWinFrame區(qū)域相同。
if (mWidth != frame.width() || mHeight != frame.height()) {
    mWidth = frame.width();
    mHeight = frame.height();
}

所以,在下一次時,上面代碼中mWidth與desiredWindowWidth相同,返回windowSizeMayChange是false,不會再執(zhí)行relayoutWindow,除非窗體frame又發(fā)生了變化。
繼續(xù)走代碼,此時發(fā)現(xiàn)mHeight是1920,但是測量的DecorView的measureHeight是1794,再測一次。

再測量一次.PNG
以Wms返回的窗體寬高1080x1920為準(zhǔn)。
繼續(xù)回到measureHierarchy方法分析。LayoutParams布局參數(shù),創(chuàng)建時,默認(rèn)MATCH_PARENT,直接到第二步,getRootMeasureSpec方法根據(jù)desiredWindowXxx與模式(MATCH_PARENT)獲取頂層視圖的MeasureSpec。

private static 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;
}

MeasureSpec兩部分信息模式與數(shù)值,MeasureSpec傳給子視圖,根視圖DecorView第一個子視圖。模式根據(jù)MATCH_PARENT、WRAP_CONTENT判斷。
EXACTLY模式:代表子視圖對寬高的要求是確定的,windowSize。這里就是desiredWindowXxx,DecorView就是這種模式,大小與desiredWindowXxx一致。
AT_MOST模式:代表子視圖對寬高的要求不確定,最大不能超過窗體提供的windowSize,傳給子視圖時,最大值限定為desiredWindowXxx。

ViewRootImpl構(gòu)造方法mWidth和mHeight初始化為-1。
第一次performTraversals測量,getMeasuredWidth的值與mWidth不同,返回值windowSizeMayChange設(shè)置true。
第二次performTraversals測量,mWidth已初始化窗體frame的值,desiredWindowWidth也設(shè)置frame的值,最終測量值是相同的,返回值windowSizeMayChange設(shè)置false。

在ViewRootImpl類中的測量過程比較復(fù)雜,主要涉及根據(jù)窗體大小計算頂層視圖的MeasureSpec,然后,通過頂層視圖的measure方法進(jìn)入樹形結(jié)構(gòu)測量。窗體區(qū)域由WindowSession#relayout方法遠(yuǎn)程訪問Wms服務(wù)獲取。

一、視圖樹 measure 流程

從頂層開始,ViewRootImpl 類的 performMeasure() 方法。

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
    }
}

內(nèi)部頂層視圖 DecorView measure 測量。
父類是 FrameLayout ,F(xiàn)rameLayout 和 ViewGroup 都沒有重寫 measure() 方法,基類 View 的 measure() 方法。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    onMeasure(widthMeasureSpec, heightMeasureSpec);
}

View 類的 measure() 方法,final 修飾,表示系統(tǒng)不希望子類重寫它,它提供測量的一套模版設(shè)計步驟,不可被改變,即調(diào)用 onMeasure() 方法。

子類的測量需求,都會調(diào)用 View measure() 方法。

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

子類重寫 onMeasure() 方法,實現(xiàn)滿足需要的測量方法。
容器和非容器視圖的測量方法不同,容器視圖包括子視圖測量,因此特定視圖的實際測量方法特定分析。如果不重寫,getDefaultSize() 方法,默認(rèn)測量值。建議值由系統(tǒng)背景 Drawable 和設(shè)置的最小寬高決定。

DecorView 視圖的 onMeasure() 方法,調(diào)用父類 FrameLayout 的 onMeasure() 方法。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    //遍歷子視圖
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            //測量。
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            maxWidth = Math.max(maxWidth,//記錄最大寬度
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            maxHeight = Math.max(maxHeight,//記錄最大高度
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
            childState = combineMeasuredStates(childState, child.getMeasuredState());
            if (measureMatchParentChildren) {
                if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                    mMatchParentChildren.add(child);
                }
            }
        }
    }
    maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
    maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
    // Check against our minimum height and width
    maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
    ...
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
}

遍歷子視圖,觸發(fā)非GONE子視圖 measure() 方法。

如果子視圖是容器視圖,遞歸遍歷它的子視圖,完成整個樹形結(jié)構(gòu)的測量。

樹形視圖結(jié)構(gòu)測量順序圖

頂層視圖—>>節(jié)點1—>>節(jié)點4—>>節(jié)點5—>>節(jié)點8—>>節(jié)點9—>>節(jié)點2—>>節(jié)點6—>>節(jié)點3—>>節(jié)點7

遍歷子視圖后,執(zhí)行幾次 measureChildWithMargins()方法,記錄子視圖最大測量寬高,F(xiàn)rameLayout 類布局的子視圖是層疊覆蓋的,最大測量值可以反映出子視圖占用區(qū)域。

最后,resolveSizeAndState() 方法,根據(jù)這個最大值 maxXxx 和傳入的 MeasureSpec 計算父容器 Size,即 DecorView 的 Size。

DecorView 是 MATCH_PARENT,屬于 EXACTLY 模式,根據(jù)傳入的 XxxMeasureSpec 計算出 Size,確定 DecorView 寬高值,其他 FrameLayout 布局若設(shè)置 WRAP_CONTENT,即 AT_MOST 模式,由子視圖占據(jù)區(qū)域最大值計算,要比較 Size 和 maxXxx。

setMeasuredDimension() 方法,將測量值交給視圖內(nèi)部的 mMeasuredXxx,代表測量寬高。

二、如何計算子視圖MeasureSpec

根據(jù)子視圖LayoutParams記錄的寬高和父容器對他的區(qū)域限制規(guī)則一,在ViewGroup的measureChildWithMargins方法,計算MeasureSpec。

MeasureSpec一共32位,模式+數(shù)值。高兩位xx代表模式,低30位代表數(shù)值。
模式:
EXACTLY:視圖精確大小,也是最終測量值,固定值和match_parent是這種模式。
AT_MOST:視圖大小不能大于父視圖指定的值。
UNSPECIFIED:無限制。

上面FrameLayout的onMeasure方法中,測量子視圖,需要計算子視圖的MeasureSpec,然后在調(diào)用子視圖measure方法時傳遞給它。

protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    //計算子視圖MeasureSpec。
    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);
}

讀取子視圖LayoutParams的寬/高,子視圖margin+父容器padding,根據(jù)父視圖Spec模式和數(shù)值,一起計算childXxxMeasureSpec。有以下幾種情況。
父視圖模式是EXACTLY

子視圖的lp.width是>0的精確值,模式EXACTLY,SpecSize是lp.width。
子視圖的lp.width是MATCH_PARENT,和父視圖一致,模式EXACTLY,SpecSize設(shè)置是父MeasureSpec獲取的Size。
子視圖的lp.width是WRAP_CONTENT,子視圖模式AT_MOST,SpecSize設(shè)置為父MeasureSpec獲取的Size,表示子視圖不確定,但最大不可超過Size。

父視圖模式是AT_MOST

子視圖的lp.width是>0的精確值,模式EXACTLY,SpecSize是lp.width。
子視圖的lp.width是MATCH_PARENT,和父視圖一致,但父視圖Size不確定,只有一個最大Size,所以子視圖模式AT_MOST,SpecSize設(shè)置父MeasureSpec獲取的最大Size,即與父保持一樣的最大Size。
子視圖的lp.width是WRAP_CONTENT,子視圖模式AT_MOST,Size設(shè)置為父MeasureSpec獲取的Size,表示子視圖不確定,最大不可超過Size。

總結(jié)

1,從頂層視圖開始測量,單個視圖的測量入口都是View的measure方法,View和ViewGroup都是這樣。
2,每個子視圖測量方法不同,需要重寫onMeasure方法,測量具體值,并設(shè)置,不重寫時,將按照View中默認(rèn)建議的值setMeasuredDimension方法設(shè)置。
3,容器視圖onMeasure方法,不僅測量自己,還需要measureChildren方法考慮子視圖。
4,父視圖根據(jù)自己的MeasureSpec和子視圖LayoutParams,計算子視圖的MeasureSpec,在測量時,MeasureSpec層層傳遞,貫穿始終。
5,視圖提供的measure方法,是一個確定的測量步驟,子類不能重寫,一旦視圖有測量需求,就會到這個方法中,模版設(shè)計模式。
6,當(dāng)執(zhí)行了setMeasuredDimension方法,就設(shè)置了測量值,可以通過getMeasuredHeight方法獲取了。


任重而道遠(yuǎn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容