View那些事兒(2) -- 理解MeasureSpec

View的繪製的三大流程的第一步就是Measure(測量),想要理解View的測量過程,必須要先理解MeasureSpec,從字面上看,MeasureSpec就是“測量規格”的意思。其實它在一定程度上決定了View的測量過程,具體來講它包含了一個View的尺寸規格信息。在系統測量View的尺寸規格的時候會將View的LayoutParams根據父容器所施加的規則轉換成對應的MeasureSpec,然後再根據MeasureSpec測量出View的寬高。

一.MeasureSpec的組成

MeasureSpec代表了一個32位的int值,高2位代表了SpecMode(測量模式),低30位代表了SpecSize(規格大小)。下面結合一段源碼來分析:

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;//二進制數左移的位數
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;//將二進制數11左移動30位
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;//將二進制數00左移動30位          
    public static final int EXACTLY= 1 << MODE_SHIFT;//將二進制數01左移動30位
    public static final int AT_MOST= 2 << MODE_SHIFT;//將二進制數10左移動30位  
    //將SpecMode和SpecSize包裝成MeasureSpec
    public static int makeMeasureSpec(int size, int mode ){
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
    //從mesureSpec中取出SpecMode
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }
    //從measureSpec中取出SpecSize
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
}

通過以上源碼,便可以很清晰地看出整個MeasureSpec的工作原理,其中涉及到了java裏的按位運算操作,“&”是按位與的意思,“~”則是按位取反的意思,“<<”是左移運算符(x<<1就表示x的值乘2),這些運算符都是在2進制數的層面上進行運算的,具體的運算規則google上面一大堆,這裏就不多說了。
MeasrureSpec類的三個常量:UNSPECIFIED、EXACTLY、AT_MOST分別代表的三種SpecMode(測量模式):

  • UNSPECIFIED(不指定大小的)
    父容器不對View有任何限制,想要多大有多大,這種情況一般屬於系統內部,表示一種測量狀態,幾乎用不到。例如:系統對ScrollView的繪製過程。
  • EXACTLY(精確的)
    父容器已經檢測出View所需要的精確的大小,這個時候View的最終大小就是SpecSize所指定的值。這裏對應於:LayoutParams中的match_parent和具體的數值,如20dp。
  • AT_MOST(最大的)
    這種模式稍微難處理一點,它指的是父容器指定了一個可用的大小(SpecSize),View的大小不能超過這個值的大小。如果超過了,就取父容器的大小,如果沒超過,就取自身的大小。這裏對應於:LayoutParams中的wrap_content模式。

全部都是些理論,沒點demo怎麼行,下面直接上一段代碼吧:

首先,要實現的效果很簡單,就是一個圓形的自定義View(我們主要是要處理wrap_content的情況下的數據,必須給它一個默認值):

public class CircleView extends View {
    Paint mPaint;//畫筆類
    public CircleView(Context context) {
        super(context);
    }

    public CircleView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public CircleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        //第一步肯定是拿到View的測量寬高(SpecSize)和測量模式(SpecMode)
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        //View顯示的時候的實際的大小
        int width = 0;
        int height = 0;
        //開始處理寬度
        //默認的寬度(在wrap_content的情況下必須有個默認的寬高)
        int defaultWidth = 200;
        //判斷SpecMode(在xml中指定View的寬度的時候就已經決定了View的SpecMode)
        switch (widthMode){
            case MeasureSpec.AT_MOST://這裏指的是wrap_content,在這個模式下不能超過父容器的寬度
                width = defaultWidth;
                break;
            case MeasureSpec.EXACTLY://這裏指的是match_parent或者具體的值,不需要做什麼處理,width直接等於widthSize就可以了
                width = widthSize;
                break;
            case MeasureSpec.UNSPECIFIED://這個模式用不到,完全可以忽略
                width = defaultWidth;
                break;
            default:
                width = defaultWidth;
                break;
        }
        //開始處理高度
        int defaultHeight = 200;
        switch (heightMode){
            case MeasureSpec.AT_MOST://這裏指的是wrap_content,在這個模式下不能超過父容器的高度
                height = defaultHeight;
                break;
            case MeasureSpec.EXACTLY://這裏指的是match_parent或者具體的值,不需要做什麼處理,height直接等於heightSize就可以了
                height = heightSize;
                break;
            case MeasureSpec.UNSPECIFIED://這個模式用不到,完全可以忽略
                height = defaultHeight;
                break;
            default:
                height = defaultHeight;
                break;
        }
        //最後必須調用父類的測量方法,來保存我們計算的寬度和高度,使得設置的測量值生效
        setMeasuredDimension(width,height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //初始化畫筆,並進行一系列的設置,如顏色等。
        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mPaint.setAntiAlias(true);//設置抗鋸齒
        //canvas.drawCircle的幾個參數分別是:圓心的x座標,y座標,半徑,畫筆
        canvas.drawCircle(getWidth()/2,getHeight()/2,Math.min(getWidth()/2,getHeight()/2),mPaint);
    }
}

顯示效果如下:

處理wrap_content後的自定義View

大家可以試一下,如果把不在onMreasure中做以上處理(註釋掉onMeasure方法即可看見效果),給CircleView設置的wrap_content會失效,實際的顯示效果和match_parent沒什麼區別。
在這個地方提前寫了一個自定義View是爲了幫助讀者理解MeasureSpec的相關用法,詳細結合前一篇文章的講解加上代碼的註釋,大家還是能很容易看懂的。
當然爲了把這個過程表述清楚,我把代碼寫得很詳細,顯得有點累贅,實際中其實可以不用這麼詳細。

二.MeasureSpec和LayouParams的關係

上面都在講MeasureSpec是什麼,現在就該說說他是怎麼來的了。
這裏要分兩部分說:第一是普通的View,第二是DecorView

  • 在普通View測量過程中,系統會將View自身的LayoutParams在父容器的約束下轉換爲對應的MeasureSpec,然後再根據這個MeasureSpec來確定View測量後的寬和高;
  • 在DecorView(頂級View)的測量過程中,系統會將View自生的LayoutParams在窗口尺寸的約束下轉換爲對應的MeasureSpec 。

1.首先,重點說一下普通的View

對於普通的View(即在佈局文中的View)來說,它的measure過程由ViewGroup傳遞而來,所以先來看看ViewGroup的measureChildWithMargins()方法:

//對ViewGroup的子View進行Measure的方法
protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
              int parentHeightMeasureSpec, int heightUsed) {
    //獲取子View的佈局參數信息(MarginLayoutParams是繼承自ViewGroup.LayoutParmas的)
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    //獲取子View的寬度的MeasureSpec,需要傳入父容器的parentWidthMeasureSpec等信息
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    //獲取子View的高度的MeasureSpec,需要傳入父容器的parentHeightMeasureSpec等信息    
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                    + heightUsed, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}  

結合註釋,從上面這段測量子View的MeasureSpec的源碼中可以看出,在測量的時候傳入了父容器的MeasureSpec信息和子View自身的LayoutParams信息(如margin、padding),所以才說普通View的測量過程與父容器和自身的LayoutParams有關。
那麼像知道測量的具體過程就得看看getChildMeasureSpec()這個方法了(看似代碼較多,但是很簡單):

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
          //還是先拿到specMode和specSize信息
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
          //padding指的是父容器已經佔據的空間大小,所以,子View的大小因爲父容器的大小減去padding
        int size = Math.max(0, specSize - padding);
          //測量後View最終的specSize和specMode
        int resultSize = 0;
        int resultMode = 0;
          //針對三種specMode進行判斷
        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            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) {
                // 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;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

上面的代碼很清晰地展示這個測量過程,下面一個表格來梳理具體的判斷邏輯。(parentSize是指父容器目前可使用的大小)

parentSpecMode →
childLayoutParams↓
EXACTLY AT_MOST UNSPICIFIED
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

根據表格,getChildMeasureSpec()方法的判斷規則一目瞭然:

  • 當View的指定了確定的寬高的時候,無論父容器的SpecMode是什麼,它的SpecMode都是EXACTLY,並且大小遵循LayoutParams中的大小;
  • 當View的寬/高指定成match_parent的時候,它的SpecMode與父容器相同,並且大小不能超過父容器的剩餘空間大小;
  • 當View的寬/高指定成wrap_content的時候,它的SpecMode恆爲(不考慮UNSPECIFIED的情況)AT_MOST,並且大小不能超過父容器的剩餘空間大小。

由於UNSPECIFIED模式我們一般接觸不到,故在這裏不做討論
從上面的總結來看,其實普通View的MeasureSpec的LayoutParams的關係還是很容易理解與記憶的。

2.下面該來看看DecorView(頂級View)了

對於DecorView來說,MeasureSpec是由窗口尺寸和自身的LayoutParams共同決定的。
還是來看看源碼吧,在ViewRootImpl(這個類被隱藏了,需要手動搜索sdk目錄找出ViewRootImpl.java才能看見源碼)有一個meaureHierarchy()方法,其中有下面這段代碼:

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

其中,desiredWindowWidth和desiredWindowHeight分別指的是屏幕的寬高。
看到這裏就隱約感覺到DecorView的MeasureSpec會和窗口尺寸有關,再來看看getRootMeasureSpec就更明瞭了:

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

看到這裏就豁然開朗了,DecorView的MeasureSpec的確和窗口尺寸有關。

所以,Decor的MeasureSpec根據它的LayoutParams遵循以下規則:

  • LayoutParams.MATCH_PARENT: EXACTLY(精準模式),大小就是窗口的大小;
  • LayoutParams.WRAP_CONTENT: AT_MOST(最大模式),大小不定,但是不能超過窗口的大小;
  • 固定的大小(如200dp): EXACTLY(精準模式),大小爲LayoutParams所制定的大小。

至此,對MeasureSpec的理解就結束了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章