View工作原理之measure

MeasureSpec

基本概念

MeasureSpec參與了View的measure過程,系統(tǒng)根據(jù)MeasureSpec來測量出View的測量寬/高

  • 對于DecorView,其MeasureSpec是由窗口尺寸的大小和自身的LayoutParams來共同確定的
  • 對于普通的View,是由父容器的MeasureSpec和自身的LayoutParams共同決定的

一個MeasureSpec由MeasureMode和MeasureSize組成,分別指測量模式和規(guī)格大小。

public static class MeasureSpec {
    //偏移量
    private static final int MODE_SHIFT = 30;
    //1100 0000 0000 0000 0000 0000 0000 0000
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    public static final int AT_MOST     = 2 << MODE_SHIFT;
    
    //合并SpecSize和SpecMode生成MeasureSpec
    public static int makeMeasureSpec(int size,int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
    
    public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
    }
    
    public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
    }
}

從上面的源碼可以看到MeasureSpec是一個32位int值,高兩位代表SpecMode,低30位代表SpecSize。SpecMode有三種模式:

  • UNSPECIFIED:父容器不對View有任何的限制,要多大給多大,這種情況一般用于系統(tǒng)內(nèi)部,表示一種測量的狀態(tài)。
  • EXACTLY:父容器已經(jīng)檢測出View所需要的精確大小,這個時候View的最終大小就是SpecSize指定的值。他對應(yīng)于LayoutParams中的match_parent和具體的數(shù)值這兩種情況。
  • AT_MOST:父容器指定了一個可用大小,View的大小不能超過父容器的SpecSize。具體的值要看不同View的具體實現(xiàn),它對應(yīng)于LayoutParams中的wrap_content。

DecorView的MeasureSpec創(chuàng)建過程

在ViewRootImpl中的measureHierarchy方法中有如下一段代碼:

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

其中傳入的參數(shù)desiredWindowWidth、desiredWindowHeight是屏幕的寬度和高度,接著再看下getRootMeasureSpec方法的實現(xiàn):

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

根據(jù)LayoutParams中的寬/高的參數(shù)來劃分:

  • LayoutParams.MATCH_PARENT:精確模式,大小就是窗口的大??;
  • LayoutParams.WTAP_CONTENT:最大模式,大小不定,但是不能超過窗口的大小。
  • 固定大?。壕_模式,大小為LayoutParams中指定的大小。

普通View的MeasureSpec創(chuàng)建過程

這里的View指的是我們布局中的View,View的measure過程由ViewGroup傳遞而來,先看下ViewGroup的measureChildWithMargins方法:

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    //獲取子元素的布局參數(shù)
    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);
}

從上面的代碼看出,子元素的MeasureSpec的創(chuàng)建與父容器的MeasureSpec和子元素本身的LayoutParams有關(guān)。得到子元素MeasureSpec的具體情況可以看下getChildMeasureSpec()方法:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    //獲取子view的真實寬度
    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // 若當前view的SpecMode是EXACTLY
    case MeasureSpec.EXACTLY:
        //寬/高是固定的數(shù)據(jù)
        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) {
           //如果設(shè)置了sUseZeroUnspecifiedMeasureSpec,大小就是0
            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;
    }
    //合并得到MeasureSpec
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

上述代碼主要作用是根據(jù)父容器的MeasureSpec同時結(jié)合View本身的LayoutParams來確定子元素的MeasureSpec。

child布局參數(shù)/父元素Mode EXACTLY AT_MOST USPECIFIED
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

總結(jié)

  • Activity對象創(chuàng)建完畢后,會將DecorView添加到Window中,同時創(chuàng)建ViewRootImpl對象與DecorView創(chuàng)建聯(lián)系
  • View的繪制流程從ViewRootImpl的PerformTraversals()方法開始。performTraversals()先計算出窗口的大小,再通過getRootMeasure()方法,計算出DecorView的寬高,傳入performMeasure()方法中
private void performTraversals() {
  ...
  if (!mStopped) {
    boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
            (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
    if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
            || mHeight != host.getMeasuredHeight() || contentInsetsChanged) {
        
        // 獲取測量規(guī)格,mWidth 和 mHeight 當前視圖 frame 的大小
        // lp是WindowManager.LayoutParams
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

         // Ask host how big it wants to be
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ...
      }
      ...
    }
}
  • 開始view的measure,起作用的其實是onMeasure()方法

View的measure過程

View的measure方法中回去調(diào)用View的onMeasure方法,如下所示:

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

setMeasuredDimension()會設(shè)置View寬高的測量值

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:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

當AT_MOST和EXACTLY這兩種情況時,返回的值都是measureSpec的specSize,specSize是測量后的大小。

UNSPECIFIED這種情況,一般用于系統(tǒng)內(nèi)部的測量,在這種情況下,View的大小為getDefaultSize的第一個參數(shù)size,寬高分別為getSuggestedMinimumWidth()和getSuggestedMinimumHeight()的返回值。

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

 protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

這里判斷了View是否設(shè)置了背景,若設(shè)置了背景,View的寬度為mMinWidth。mMinWidth對應(yīng)于android:minWidth這個屬性所指的值,若不設(shè)置默認為0。如果View設(shè)置了背景,則View寬度為max(mMinHeight, mBackground.getMinimumHeight()):

public int getMinimumHeight() {
    final int intrinsicHeight = getIntrinsicHeight();
    return intrinsicHeight > 0 ? intrinsicHeight : 0;
}
//返回drawable內(nèi)在高度,如果drawable沒有內(nèi)在高度返回-1
public int getIntrinsicHeight() {
        return -1;
}

根據(jù)上述代碼,得到getMinimumHeight()返回的就是drawable的原始高度,前提是drawable有原始高度,沒有就返回0。例如:ShapeDrawable無原始寬/高,BitmapDrawable有原始寬/高。

wrap_content和自定義問題

從getDefaultSize()方法來看,若按照onMeasure的默認方法實現(xiàn),當寬/高設(shè)置成wrap_content或match_parent時,最后的結(jié)果都是specSize,也就是兩種情況下都是match_parent的效果。所以在自定義view下的時候需要對wrap_content進行處理。

在自定義view的時候,我們通過判定寬高是否是wrap_content來給寬/高是wrap_content的情況設(shè)定一個默認的內(nèi)部寬/高。像TextView、ImageView都對這里做了處理。

View的Measure過程總結(jié)

  • 從ViewGroup#measureChildWidthMargins開始,計算View的MeasureSpec
  • measureChildWidthMargins()調(diào)用child.measure()開始測量過程
  • View#measure()調(diào)用onMeasure()
  • onMeasure()方法測量View寬高
    • 根據(jù)是否有背景圖來確定View的默認大小
    • 根據(jù)SpecMode來確定View測量后的大小是SpecSize還是默認大小
  • onMeasure()測量后的數(shù)據(jù)給全局變量mMeasuredWidth和mMeasuredHeight

ViewGroup的measure過程

對于ViewGroup來說,除了完成自己的measure過程外,還會遍歷去調(diào)用所有子元素的measure方法,各個子元素再去遞歸執(zhí)行這個過程。

ViewGroup#measureChildren

遍歷子view調(diào)用measureChild()

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        //當child不是GONE時,就去測量
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

ViewGroup#measureChild()

  • 前面提到過的measureChildWithMargins()中的getChildMeasureSpec(),開始測量
  • 調(diào)用子view向下遞歸測量
protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

ViewGroup沒有定義測量的具體過程,因為ViewGroup是一個抽象類,其測量過程的onMeasure方法需要各個子類去具體實現(xiàn)。

獲得View寬高的四種方法

在實際操作中我們可能回去獲取某個view的寬和高,但是在onCreate() onResume()方法中一般是獲取不到的,因為View的measure過程和Activity生命周期方法不是同步執(zhí)行的。

1.Activity/View#onWindowFocusChanged

onWindowFocusChanged這個方法的含義是:View已經(jīng)初始化完畢了,寬高已經(jīng)準備好了。當Activity窗口得到焦點和失去焦點是均會被調(diào)用一次,當頻繁進行onPause和onPause,會被頻繁調(diào)用。

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if (hasFocus) {
        int width = view.getMeasuredWidth();
        int height = view.getMeasuredHeight();
    }
}

2.view.post(runnable)

通過post可以將一個runnable投遞到消息隊列的尾部,然后等待Looper調(diào)用此runnable的時候,View也已經(jīng)初始化好了

@Override
protected void onStart() {
    super.onStart();
    view.post(new Runnable() {
        @Override
        public void run() {
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    });
}

3.ViewTreeObserver

可以通過ViewTreeObserver添加監(jiān)聽來監(jiān)聽View的各種動態(tài)。onGlobalLayout()會在View樹發(fā)生改變或是View樹內(nèi)部的View的可見性發(fā)生改變時調(diào)用。

@Override
protected void onStart() {
    super.onStart();
    ViewTreeObserver observer = view.getViewTreeObserver();
    observer.addOnGlobalFocusChangeListener(new ViewTreeObserver.OnGlobalFocusChangeListener() {
        @Override
        public void onGlobalFocusChanged(View oldFocus, View newFocus) {
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    });
}

4.手動measure(view.measure(int widthMeasureSpec,int heightMeasureSpec))

對view進行measure得到view的寬 高,需要根據(jù)view的LayoutParams來分:

match_parent

無法measure出具體寬高。因為要測量出當前view尺寸,需要知道父容器的屬于空間,無法知道parentSize。

wrap_content

((1 << 30) - 1)是specMode支持的最大值

int widthSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.EXACTLY);
view.measure(widthSpec, heightSpec);

具體數(shù)值

LayoutParams lp = view.getLayoutParams();
int widthSpec = View.MeasureSpec.makeMeasureSpec(lp.width, View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(lp.height, View.MeasureSpec.EXACTLY);
view.measure(widthSpec, heightSpec);
最后編輯于
?著作權(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ù)。

相關(guān)閱讀更多精彩內(nèi)容

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