Android:自定義view之onMeasure

本篇會講解view的onMeasure的詳細流程

onMeasure什么時候會被調用

onMeasure方法的作用是測量控件的大小,當我們創建一個View(執行構造方法)的時候不需要測量控件的大小,只有將這個view放入一個容器(父控件)中的時候才需要測量,而這個測量方法就是父控件喚起調用的。當控件的父控件要放置該控件的時候,父控件會調用子控件的onMeasure方法詢問子控件:“你有多大的尺寸,我要給你多大的地方才能容納你?”,然后傳入兩個參數(widthMeasureSpec和heightMeasureSpec),這兩個參數就是父控件告訴子控件可獲得的空間以及關于這個空間的約束條件,子控件拿著這些條件就能正確的測量自身的寬高了。

onMeasure方法執行流程

上面說到onMeasure方法是由父控件調用的,所有父控件都是ViewGroup的子類,ViewGroup是一個抽象類,它里面有一個抽象方法onLayout,這個方法的作用就是擺放它所有的子控件(安排位置),因為是抽象類,不能直接new對象,所以我們在布局文件中可以使用View但是不能直接使用 ViewGroup(代碼中也一樣)。
??在給子控件確定位置之前,必須要獲取到子控件的大小,而ViewGroup并沒有重寫View的onMeasure方法,只是提供了(measureChildren\measureChild\measureChildWithMargins)等測量子view的相關方法.這樣做也是因為不同的容器擺放子控件的方式不同,比如RelativeLayout,LinearLayout這兩個ViewGroup的子類,它們擺放子控件的方式不同,有的是線性擺放,而有的是疊加擺放,這就導致測量子控件的方式會有所差別,所以ViewGroup就干脆不直接測量子控件,而是叫他的子類根據自己的布局特性重寫onMeasure方法去測量。最后通過使用ViewGroup的measureChildxxx系列方法得到最終的子控件大小。
??測量的時候父控件的onMeasure方法會遍歷他所有的子控件,挨個調用子控件的measure方法,measure方法會調用onMeasure,然后會調用setMeasureDimension方法保存測量的大小,一次遍歷下來,第一個子控件以及這個子控件中的所有子控件都會完成測量工作;然后開始測量第二個子控件…;最后父控件所有的子控件都完成測量以后會調用setMeasureDimension方法保存自己的測量大小。值得注意的是,這個過程不只執行一次,也就是說有可能重復執行,因為有的時候,一輪測量下來,父控件發現某一個子控件的尺寸不符合要求,就會重新測量一遍。
舉個栗子,看下圖:


下面是測量的時序圖:


MeasureSpec類

MeasureSpec約束是由父控件傳遞給子控件的,我們看一看源碼:

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    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;

    /**
     * 根據所提供的大小和模式創建一個測量規范
     */
    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) {
        return (measureSpec & MODE_MASK);
    }
    /**
     * 從所提供的測量規范中提取尺寸
     */
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
    ...
}

從源碼中我們知道,MeasureSpec其實就是尺寸和模式通過各種位運算計算出的一個整型值,它提供了三種模式(UNSPECIFIED ,EXACTLY ,AT_MOST )

約束:UNSPECIFIED(未指定)
布局參數:
值:0(00000000000000000000000000000000)
說明:父控件沒有對子控件施加任何約束,子控件可以得到任意想要的大小(使用較少)。
約束:EXACTLY(完全)
布局參數:match_parent/具體寬高值
值:1073741824(01000000000000000000000000000000)
說明:父控件給子控件決定了確切大小。子控件將被限定在給定的邊界里而忽略它本身大小。怎樣理解忽略它本身大小呢?假設給自定義控件設置的大小為1dp,那可想而知,控件就看不到了,相比而言match_parent也是如此,假設父控件只有1dp,那子控件也就看不到了,所以這個模式子控件將被限定在給定的邊界里而忽略它本身大小.

  • 如果是match_parent,說明父控件已經明確知道子控件想要多大的尺寸了(就是剩余的空間都要了)
  • 如果是設置的具體的值,那更應該說明父控件已經知道子控件大小了(具體的值)

約束:AT_MOST(至多)
布局參數:wrap-content
值:-2147483648(10000000000000000000000000000000)
說明:子控件至多達到指定大小的值。這種模式下父控件無法確定子控件的尺寸,這種情況下子控件要自己根據需求去設置大小.(我在模仿貪吃蛇的控件的時候默認給的是450px)

針對這幾種模式,我在自定義控件中也定義了相應的尺寸(至于為什么要重寫onMeasure我在下文中有提及):

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int width = getMySize(widthMeasureSpec);
        final int height = getMySize(heightMeasureSpec);
        final int min = Math.min(width, height);//保證控件為方形
        setMeasuredDimension(min, min);
    }
 /**
     * 獲取測量大小
     */
    private int getMySize(int measureSpec) {
        int result;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            result = specSize;//確切大小,所以將得到的尺寸給view
        } else if (specMode == MeasureSpec.AT_MOST) {
            //默認值為450px,此處要結合父控件給子控件的最多大小(要不然會填充父控件),所以采用最小值
            result = Math.min(DEFAULT_SIZE, specSize);
        } else {
            result = DEFAULT_SIZE;
        }
        return result;
    }
從ViewGroup的onMeasure到View的onMeasure
  • ViewGroup中三個測量子控件的方法:
    通過上面的介紹,我們知道,如果要自定義ViewGroup就必須重寫onMeasure方法,在這里測量子控件的尺寸。子控件的尺寸怎么測量呢?ViewGroup中提供了三個關于測量子控件的方法:
/**
     * 遍歷ViewGroup中所有的子控件,調用measuireChild測量寬高
     */
    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];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                //測量某一個子控件寬高
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

    /**
     * 測量某一個child的寬高
     */
    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);
    }
    /**
     * 測量某一個child的寬高,考慮margin值
     */
    protected void measureChildWithMargins(View child,
                                           int parentWidthMeasureSpec, int widthUsed,
                                           int parentHeightMeasureSpec, int heightUsed) {
        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);
    }

其中measureChildren就是遍歷所有子控件挨個測量,最終測量子控件的方法就是measureChildmeasureChildWithMargins 了,measureChildWithMarginsmeasureChild的區別就是父控件支持margin屬性對子控件的測量有影響,比如我們的屏幕是1080x1920的,子控件的寬度為填充父窗體,如果使用了marginLeft并設置值為100; 在測量子控件的時候,如果用measureChild,計算的寬度是1080,而如果是使用measureChildWithMargins,計算的寬度是1080-100 = 980。

ViewGroup支持margin屬性

ViewGroup中有兩個內部類ViewGroup.LayoutParams和ViewGroup. MarginLayoutParams,MarginLayoutParams繼承自LayoutParams ,這兩個內部類就是VIewGroup的布局參數類,比如我們在LinearLayout等布局中使用的layout_width\layout_hight等以“layout_ ”開頭的屬性都是布局屬性,最后要通過LayoutParams獲取相應的屬性。在View中有一個mLayoutParams的變量用來保存這個View的所有布局屬性。
LayoutParams和MarginLayoutParams 的關系:
LayoutParams 中定義了兩個屬性(也就是layout_width\layout_hight的來頭):

<declare-styleable name= "ViewGroup_Layout">
    <attr name ="layout_width" format="dimension">
        <enum name ="fill_parent" value="-1" />
        <enum name ="match_parent" value="-1" />
        <enum name ="wrap_content" value="-2" />
    </attr >
    <attr name ="layout_height" format="dimension">
        <enum name ="fill_parent" value="-1" />
        <enum name ="match_parent" value="-1" />
        <enum name ="wrap_content" value="-2" />
    </attr >
</declare-styleable >

MarginLayoutParams 是LayoutParams的子類,它當然也延續了layout_width\layout_hight 屬性,但是它擴充了其他屬性:

< declare-styleable name ="ViewGroup_MarginLayout">
    <attr name ="layout_width" />   <!--使用已經定義過的屬性-->
    <attr name ="layout_height" />
    <attr name ="layout_margin" format="dimension"  />
    <attr name ="layout_marginLeft" format= "dimension"  />
    <attr name ="layout_marginTop" format= "dimension" />
    <attr name ="layout_marginRight" format= "dimension"  />
    <attr name ="layout_marginBottom" format= "dimension"  />
    <attr name ="layout_marginStart" format= "dimension"  />
    <attr name ="layout_marginEnd" format= "dimension"  />
</declare-styleable >

這就是我們使用的margin屬性的由來。

  • 為什么LayoutParams 類要定義在ViewGroup中?
    ??眾所周知ViewGroup是所有容器的基類,一個控件需要被包裹在一個容器中,這個容器必須提供一種規則控制子控件的擺放,所以ViewGroup提供一個布局屬性類,用于控制子控件的布局(layout_)屬性。

  • 為什么View中會有一個mLayoutParams 變量?
    ??在上篇自定義屬性中我們在構造方法中初始化布局文件中的屬性值,我們把屬性分為兩種。一種是本View的繪制屬性,比如TextView的文本、文字顏色、背景等,這些屬性是跟View的繪制相關的。另一種就是以“layout_”打頭的叫做布局屬性,這些屬性是父控件對子控件的大小及位置的一些描述屬性,這些屬性都是用ViewGroup.LayoutParams定義的,所以用一個變量(mLayoutParams )保存著。

getChildMeasureSpec方法

measureChildWithMargins跟measureChild 都調用了這個方法,其作用就是通過父控件的寬高約束規則父控件加在子控件上的寬高布局參數生成一個子控件的約束。我們知道View的onMeasure方法需要兩個參數(父控件對View的寬高約束),這個寬高約束就是通過這個方法生成的。為什么通過父類布局的約束生成子view約束呢?打個比方,父控件的寬高約束為wrap_content,而子控件為match_perent,是不是很有意思,父控件說我的寬高就是包裹我的子控件,而子控件說我的寬高填充父窗體。最后該怎么確定大小呢?所以我們需要為子控件重新生成一個新的約束規則。只要記住,子控件的寬高約束規則是父控件調用getChildMeasureSpec方法生成。
measureChild源碼,measureChildWithMargins類似

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        //獲取了子控件的layout_布局屬性
        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);
    }

getChildMeasureSpec方法代碼不多,也比較簡單,就是幾個switch將各種情況考慮后生成一個子控件的新的寬高約束,這個方法的結果能夠用一個表來概括:



進行了上面的步驟,接下來就是在measureChildWithMargins或者measureChild中 調用子控件的measure方法測量子控件的尺寸了。

View的onMeasure

View中onMeasure方法已經默認為我們的控件測量了寬高,我們看看它做了什么工作:

protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension( getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
/**
 * 為寬度獲取一個建議最小值
 */
protected int getSuggestedMinimumWidth () {
    return (mBackground == null) ? mMinWidth : max(mMinWidth , mBackground.getMinimumWidth());
}
/**
 * 獲取默認的寬高值
 */
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;
}

從源碼我們了解到:

如果View的寬高模式為未指定,他的寬高將設置為android:minWidth/Height =”“值與背景寬高值中較大的一個;
如果View的寬高 模式為 EXACTLY (具體的size ),最終寬高就是這個size值;
如果View的寬高模式為EXACTLY (填充父控件 ),最終寬高將為填充父控件;
如果View的寬高模式為AT_MOST (包裹內容),最終寬高也是填充父控件。
也就是說如果我們的自定義控件在布局文件中,只需要設置指定的具體寬高,或者MATCH_PARENT 的情況,我們可以不用重寫onMeasure方法。

但如果自定義控件需要設置包裹內容WRAP_CONTENT ,我們需要重寫onMeasure方法,為控件設置需要的尺寸;默認情況下WRAP_CONTENT 的處理也將填充整個父控件。(上文中已經提到過WRAP_CONTENT的時候采用默認長度)

setMeasuredDimension

onMeasure方法最后需要調用setMeasuredDimension方法來保存測量的寬高值,如果不調用這個方法,可能會產生不可預測的問題。

總結

最后總結一下

測量控件大小是父控件發起的
父控件要測量子控件大小,需要重寫onMeasure方法,然后調用measureChildren或者measureChildWithMargins方法
View的onMeasure方法的參數是通過getChildMeasureSpec生成的
如果我們自定義控件需要使用wrap_content屬性,我們需要重寫onMeasure方法
測量控件的步驟:
父控件onMeasure->measureChildren/measureChildWithMargin->getChildMeasureSpec-> 子控件的measure->onMeasure->setMeasureDimension-> 父控件onMeasure結束調用setMeasureDimension
最后保存自己的大小

參考
自定義View目錄

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,748評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,165評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,595評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,633評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,435評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,943評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,035評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,175評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,713評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,599評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,788評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,303評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,034評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,412評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,664評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,408評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,747評論 2 370

推薦閱讀更多精彩內容