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

View的繪制的三大流程的第一步就是Measure(測(cè)量)冗尤,想要理解View的測(cè)量過程,必須要先理解MeasureSpec,從字面上看欣簇,MeasureSpec就是“測(cè)量規(guī)格”的意思巾钉。其實(shí)它在一定程度上決定了View的測(cè)量過程翘狱,具體來講它包含了一個(gè)View的尺寸規(guī)格信息。在系統(tǒng)測(cè)量View的尺寸規(guī)格的時(shí)候會(huì)將View的LayoutParams根據(jù)父容器所施加的規(guī)則轉(zhuǎn)換成對(duì)應(yīng)的MeasureSpec砰苍,然后再根據(jù)MeasureSpec測(cè)量出View的寬高潦匈。

一.MeasureSpec的組成

MeasureSpec代表了一個(gè)32位的int值,高2位代表了SpecMode(測(cè)量模式)赚导,低30位代表了SpecSize(規(guī)格大胁缢酢)。下面結(jié)合一段源碼來分析:

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;//二進(jìn)制數(shù)左移的位數(shù)
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;//將二進(jìn)制數(shù)11左移動(dòng)30位
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;//將二進(jìn)制數(shù)00左移動(dòng)30位          
    public static final int EXACTLY= 1 << MODE_SHIFT;//將二進(jìn)制數(shù)01左移動(dòng)30位
    public static final int AT_MOST= 2 << MODE_SHIFT;//將二進(jìn)制數(shù)10左移動(dòng)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);
    }
}

通過以上源碼吼旧,便可以很清晰地看出整個(gè)MeasureSpec的工作原理凰锡,其中涉及到了java里的按位運(yùn)算操作,“&”是按位與的意思圈暗,“~”則是按位取反的意思掂为,“<<”是左移運(yùn)算符(x<<1就表示x的值乘2),這些運(yùn)算符都是在2進(jìn)制數(shù)的層面上進(jìn)行運(yùn)算的员串,具體的運(yùn)算規(guī)則google上面一大堆菩掏,這里就不多說了。
MeasrureSpec類的三個(gè)常量:UNSPECIFIED昵济、EXACTLY智绸、AT_MOST分別代表的三種SpecMode(測(cè)量模式):

  • UNSPECIFIED(不指定大小的)
    父容器不對(duì)View有任何限制,想要多大有多大访忿,這種情況一般屬于系統(tǒng)內(nèi)部瞧栗,表示一種測(cè)量狀態(tài),幾乎用不到海铆。例如:系統(tǒng)對(duì)ScrollView的繪制過程迹恐。
  • EXACTLY(精確的)
    父容器已經(jīng)檢測(cè)出View所需要的精確的大小,這個(gè)時(shí)候View的最終大小就是SpecSize所指定的值卧斟。這里對(duì)應(yīng)于:LayoutParams中的match_parent和具體的數(shù)值殴边,如20dp憎茂。
  • AT_MOST(最大的)
    這種模式稍微難處理一點(diǎn),它指的是父容器指定了一個(gè)可用的大写赴丁(SpecSize)竖幔,View的大小不能超過這個(gè)值的大小。如果超過了是偷,就取父容器的大小拳氢,如果沒超過,就取自身的大小蛋铆。這里對(duì)應(yīng)于:LayoutParams中的wrap_content模式馋评。

全部都是些理論,沒點(diǎn)demo怎么行刺啦,下面直接上一段代碼吧:

首先留特,要實(shí)現(xiàn)的效果很簡單,就是一個(gè)圓形的自定義View(我們主要是要處理wrap_content的情況下的數(shù)據(jù)玛瘸,必須給它一個(gè)默認(rèn)值):

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的測(cè)量寬高(SpecSize)和測(cè)量模式(SpecMode)
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        //View顯示的時(shí)候的實(shí)際的大小
        int width = 0;
        int height = 0;
        //開始處理寬度
        //默認(rèn)的寬度(在wrap_content的情況下必須有個(gè)默認(rèn)的寬高)
        int defaultWidth = 200;
        //判斷SpecMode(在xml中指定View的寬度的時(shí)候就已經(jīng)決定了View的SpecMode)
        switch (widthMode){
            case MeasureSpec.AT_MOST://這里指的是wrap_content,在這個(gè)模式下不能超過父容器的寬度
                width = defaultWidth;
                break;
            case MeasureSpec.EXACTLY://這里指的是match_parent或者具體的值磕秤,不需要做什么處理,width直接等于widthSize就可以了
                width = widthSize;
                break;
            case MeasureSpec.UNSPECIFIED://這個(gè)模式用不到捧韵,完全可以忽略
                width = defaultWidth;
                break;
            default:
                width = defaultWidth;
                break;
        }
        //開始處理高度
        int defaultHeight = 200;
        switch (heightMode){
            case MeasureSpec.AT_MOST://這里指的是wrap_content,在這個(gè)模式下不能超過父容器的高度
                height = defaultHeight;
                break;
            case MeasureSpec.EXACTLY://這里指的是match_parent或者具體的值市咆,不需要做什么處理,height直接等于heightSize就可以了
                height = heightSize;
                break;
            case MeasureSpec.UNSPECIFIED://這個(gè)模式用不到再来,完全可以忽略
                height = defaultHeight;
                break;
            default:
                height = defaultHeight;
                break;
        }
        //最后必須調(diào)用父類的測(cè)量方法蒙兰,來保存我們計(jì)算的寬度和高度,使得設(shè)置的測(cè)量值生效
        setMeasuredDimension(width,height);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //初始化畫筆芒篷,并進(jìn)行一系列的設(shè)置搜变,如顏色等。
        mPaint = new Paint();
        mPaint.setColor(Color.RED);
        mPaint.setAntiAlias(true);//設(shè)置抗鋸齒
        //canvas.drawCircle的幾個(gè)參數(shù)分別是:圓心的x坐標(biāo)针炉,y坐標(biāo)挠他,半徑,畫筆
        canvas.drawCircle(getWidth()/2,getHeight()/2,Math.min(getWidth()/2,getHeight()/2),mPaint);
    }
}

顯示效果如下:

處理wrap_content后的自定義View

大家可以試一下篡帕,如果把不在onMreasure中做以上處理(注釋掉onMeasure方法即可看見效果)殖侵,給CircleView設(shè)置的wrap_content會(huì)失效,實(shí)際的顯示效果和match_parent沒什么區(qū)別。
在這個(gè)地方提前寫了一個(gè)自定義View是為了幫助讀者理解MeasureSpec的相關(guān)用法镰烧,詳細(xì)結(jié)合前一篇文章的講解加上代碼的注釋拢军,大家還是能很容易看懂的。
當(dāng)然為了把這個(gè)過程表述清楚怔鳖,我把代碼寫得很詳細(xì)茉唉,顯得有點(diǎn)累贅,實(shí)際中其實(shí)可以不用這么詳細(xì)。

二.MeasureSpec和LayouParams的關(guān)系

上面都在講MeasureSpec是什么度陆,現(xiàn)在就該說說他是怎么來的了艾凯。
這里要分兩部分說:第一是普通的View,第二是DecorView

  • 在普通View測(cè)量過程中懂傀,系統(tǒng)會(huì)將View自身的LayoutParams在父容器的約束下轉(zhuǎn)換為對(duì)應(yīng)的MeasureSpec趾诗,然后再根據(jù)這個(gè)MeasureSpec來確定View測(cè)量后的寬和高;
  • 在DecorView(頂級(jí)View)的測(cè)量過程中鸿竖,系統(tǒng)會(huì)將View自生的LayoutParams在窗口尺寸的約束下轉(zhuǎn)換為對(duì)應(yīng)的MeasureSpec 。

1.首先铸敏,重點(diǎn)說一下普通的View

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

//對(duì)ViewGroup的子View進(jìn)行Measure的方法
protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
              int parentHeightMeasureSpec, int heightUsed) {
    //獲取子View的布局參數(shù)信息(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);
}  

結(jié)合注釋,從上面這段測(cè)量子View的MeasureSpec的源碼中可以看出蒙具,在測(cè)量的時(shí)候傳入了父容器的MeasureSpec信息和子View自身的LayoutParams信息(如margin球榆、padding),所以才說普通View的測(cè)量過程與父容器和自身的LayoutParams有關(guān)禁筏。
那么像知道測(cè)量的具體過程就得看看getChildMeasureSpec()這個(gè)方法了(看似代碼較多持钉,但是很簡單):

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
          //還是先拿到specMode和specSize信息
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
          //padding指的是父容器已經(jīng)占據(jù)的空間大小,所以篱昔,子View的大小因?yàn)楦溉萜鞯拇笮p去padding
        int size = Math.max(0, specSize - padding);
          //測(cè)量后View最終的specSize和specMode
        int resultSize = 0;
        int resultMode = 0;
          //針對(duì)三種specMode進(jìn)行判斷
        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);
    }

上面的代碼很清晰地展示這個(gè)測(cè)量過程每强,下面一個(gè)表格來梳理具體的判斷邏輯。(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

根據(jù)表格空执,getChildMeasureSpec()方法的判斷規(guī)則一目了然:

  • 當(dāng)View的指定了確定的寬高的時(shí)候,無論父容器的SpecMode是什么穗椅,它的SpecMode都是EXACTLY辨绊,并且大小遵循LayoutParams中的大小匹表;
  • 當(dāng)View的寬/高指定成match_parent的時(shí)候门坷,它的SpecMode與父容器相同,并且大小不能超過父容器的剩余空間大信鄱啤拜鹤;
  • 當(dāng)View的寬/高指定成wrap_content的時(shí)候,它的SpecMode恒為(不考慮UNSPECIFIED的情況)AT_MOST流椒,并且大小不能超過父容器的剩余空間大小敏簿。

由于UNSPECIFIED模式我們一般接觸不到,故在這里不做討論
從上面的總結(jié)來看,其實(shí)普通View的MeasureSpec的LayoutParams的關(guān)系還是很容易理解與記憶的惯裕。

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

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

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

其中握玛,desiredWindowWidth和desiredWindowHeight分別指的是屏幕的寬高够傍。
看到這里就隱約感覺到DecorView的MeasureSpec會(huì)和窗口尺寸有關(guān),再來看看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的確和窗口尺寸有關(guān)冕屯。

所以,Decor的MeasureSpec根據(jù)它的LayoutParams遵循以下規(guī)則:

  • LayoutParams.MATCH_PARENT: EXACTLY(精準(zhǔn)模式)拂苹,大小就是窗口的大邪财浮;
  • LayoutParams.WRAP_CONTENT: AT_MOST(最大模式)瓢棒,大小不定浴韭,但是不能超過窗口的大小脯宿;
  • 固定的大心罹薄(如200dp): EXACTLY(精準(zhǔn)模式),大小為LayoutParams所制定的大小连霉。

至此舍肠,對(duì)MeasureSpec的理解就結(jié)束了

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市窘面,隨后出現(xiàn)的幾起案子翠语,更是在濱河造成了極大的恐慌,老刑警劉巖财边,帶你破解...
    沈念sama閱讀 211,884評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肌括,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡酣难,警方通過查閱死者的電腦和手機(jī)谍夭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,347評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來憨募,“玉大人紧索,你說我怎么就攤上這事〔艘ィ” “怎么了珠漂?”我有些...
    開封第一講書人閱讀 157,435評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵晚缩,是天一觀的道長。 經(jīng)常有香客問我媳危,道長荞彼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,509評(píng)論 1 284
  • 正文 為了忘掉前任待笑,我火速辦了婚禮鸣皂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘暮蹂。我一直安慰自己寞缝,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,611評(píng)論 6 386
  • 文/花漫 我一把揭開白布仰泻。 她就那樣靜靜地躺著荆陆,像睡著了一般。 火紅的嫁衣襯著肌膚如雪我纪。 梳的紋絲不亂的頭發(fā)上慎宾,一...
    開封第一講書人閱讀 49,837評(píng)論 1 290
  • 那天丐吓,我揣著相機(jī)與錄音浅悉,去河邊找鬼。 笑死券犁,一個(gè)胖子當(dāng)著我的面吹牛术健,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播粘衬,決...
    沈念sama閱讀 38,987評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼荞估,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了稚新?” 一聲冷哼從身側(cè)響起勘伺,我...
    開封第一講書人閱讀 37,730評(píng)論 0 267
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎褂删,沒想到半個(gè)月后飞醉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,194評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡屯阀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,525評(píng)論 2 327
  • 正文 我和宋清朗相戀三年缅帘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片难衰。...
    茶點(diǎn)故事閱讀 38,664評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡钦无,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出盖袭,到底是詐尸還是另有隱情失暂,我是刑警寧澤彼宠,帶...
    沈念sama閱讀 34,334評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站趣席,受9級(jí)特大地震影響兵志,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜宣肚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,944評(píng)論 3 313
  • 文/蒙蒙 一想罕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧霉涨,春花似錦按价、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,764評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至往枷,卻和暖如春框产,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背错洁。 一陣腳步聲響...
    開封第一講書人閱讀 31,997評(píng)論 1 266
  • 我被黑心中介騙來泰國打工秉宿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人屯碴。 一個(gè)月前我還...
    沈念sama閱讀 46,389評(píng)論 2 360
  • 正文 我出身青樓描睦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親导而。 傳聞我的和親對(duì)象是個(gè)殘疾皇子忱叭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,554評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容