1. 程式人生 > >自定義View之onMeasure()方法

自定義View之onMeasure()方法

前言

一個View從建立到被繪製到螢幕上,需要完成measure(測量)、layout(佈置)、draw(繪製)三個步驟,分別對應View中的measure()、layout()、draw()三個方法。網上關於這三個方法的原始碼解析文章有很多,而且一般情況下也不會去重寫它們(measure()方法還無法覆蓋),因此本文不打算將其作為重點。本文以及接下來的幾篇文章會詳細介紹和程式設計人員關係更大的onMeasure()、onLayout()與onDraw()的具體實現方法,以及過程中會涉及到的一些知識。

MeasureSpec

View中的onMeasure()方法是這樣的:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {...}
  • 1

可以看到,它的引數是widthMeasureSpec與heightMeasureSpec兩個int值。這兩個引數實質上是由View的靜態內部類MeasureSpec管理的兩個特殊的“物件”(並非真正的物件),包含了父view關於子view應當如何測量自身給出的“指示”。為了提升效率,Android系統採用位運算的方式,將模式SpecMode(2位)與尺寸SpecSize(30位)拼接成了一個int值,並傳遞這個int值作為測量時使用的引數。 
MeasureSpec類的實現基本都是依靠位運算,沒什麼實質性內容。直接上一些結論: 
(1)SpecMode分三種:UNSPECIFIED、EXACTLY、AT_MOST。UNSPECIFIED表示不指定具體測量模式,EXACTLY表示父View希望子view的尺寸取精確值(即等於SpecSize),AT_MOST表示父View希望子view的尺寸不超過SpecSize。 
(2)使用MeasureSpec.getMode(int measureSpec)與MeasureSpec.getSize(int measureSpec)獲取SpecMode與SpecSize。 
(3)使用MeasureSpec.makeMeasureSpec(int size,int mode)生成一個measureSpec值。

onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法

下面回到onMeasure()方法。顧名思義,這個方法是在該view需要測量自身時呼叫的。具體來說,當這個view的父view對其呼叫measure()方法時,onMeasure()方法會在過程中被呼叫。下面分別看看View與ViewGroup分別應當怎麼實現這個方法。

View的onMeasure()實現

view只需要根據自身情況,計算出自己的尺寸就可以了。步驟如下: 
(1)使用MeasureSpec.getMode()與MeasureSpec.getSize()獲取父view要求的SpecMode與SpecSize。 
(2)根據上面的引數確定自己的實際尺寸(width與height)。一般來說,如果SpecMode是EXACTLY,那麼直接取尺寸值=SpecSize即可。如果SpecMode是AT_MOST,那麼就需要根據自身特點計算出一個尺寸值,並保證最終尺寸值不超過SpecSize。當然了,你也可以完全無視父view的要求,自顧自地進行測量,不過這種方式顯然是不推薦的。 
(3)使用setMeasuredDimension(int measuredWidth, int measuredHeight)設定最終測量尺寸。這個方法被呼叫之後,view的getMeasuredWidth()方法與getMeasuredHeight()方法才能生效(在之前呼叫會返回0)。 
實際上,對於不那麼複雜的自定義view,View類提供的預設實現已經可以滿足大部分需求了。下面看看它是怎麼做的:

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

getSuggestedMinimumWidth()與getSuggestedMinimumHeight()是根據view是否設定了BackgroundDrawable確定一個最小尺寸值。重點看一下getDefaultSize()方法:

public static int getDefaultSize(int size, int measureSpec) {
//size是view根據自身需求提供的一個尺寸值,measureSpec來自父view。
    //最終尺寸值
    int result = size;
    //獲取父佈局要求的測量模式與測量尺寸
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    //如果模式為UNSPECIFIED則不作限制
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    //AT_MOST直接當做EXACTLY處理
    case MeasureSpec.AT_MOST:
    //如果模式為EXACTLY,則根據父佈局要求確定最終尺寸值
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}
  •  

解釋見程式碼註釋。這裡需要記住的是,如果使用這個方法計算尺寸值的話,AT_MOST模式不會生效。AT_MOST一般是在view的layout_width與layout_height為WRAP_CONTENT時使用的。因此,如果想要自定義view支援WRAP_CONTENT屬性,就必須自己對AT_MOST的情況作出處理。

ViewGroup的onMeasure()實現

不同於View,ViewGroup需要負責子view的測量。具體來講,就是為子view提供合適的MeasureSpec,並呼叫子view的measure()(注意,不是onMeasure())方法。至於自身的尺寸,則需要結合更高一層的父view的指示以及子view的情況來確定。 
為了給子view提供合適的MeasureSpec,ViewGroup中提供了一個getChildMeasureSpec()方法,下面看看它的實現:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

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

英文註釋是自帶的,已經很詳細了。引數spec是高層view提供的MeasureSpec,padding為需要扣除的padding部分尺寸,childDimension為子view需求的尺寸(一般直接傳MarginLayoutParams.width)。實質上,這個方法就是綜合考慮了高層view的指示以及低層view的需求,分9種情況構建了一個合適的MeasureSpec。下面的圖來自Android View系統解析(下) ,任玉剛總結

總結

關於onMeasure()方法的實現差不多就這些內容了。可以看出,MeasureSpec是父view與子view溝通的橋樑。實現onMeasure()方法的關鍵點就在於如何響應父view的MeasureSpec,以及如何為子view構建合適的MeasureSpec。