自定義View控件之onMeasure方法詳解

前言

轉載請註明出處!
http://blog.csdn.net/u011692041/article/details/76093920

這類的文章很多很多,其實我也是不想寫的.但是說起來我雖然看了很多很多的文章,但是對於View控件的measure方法還是一知半解的.那麼今天我就來做一個總結,並且解決很多人問我的一些常見的問題.下面先把一些常見的問題羅列一遍
View控件中的measure方法被父容器調用,會引發測量的整個過程,也就有了onMeasure方法
父容器調用measure方法放在下一篇自定義ViewGroup中贅述

常見問題

  • 1.爲什麼我沒有重寫onMeasure方法,自定義控件能顯示
  • 2.爲什麼我的自定義控件不顯示
  • 3.我的自定義控件如何才能支持包裹(wrap_content)
  • 4.爲什麼我的自定義控件在xml中寫 wrap_content 和寫 match_parent 效果是一樣的
  • 5.我的自定義控件如何才能支持內邊距
  • 6.我的自定義控件在平常視圖使用是正常的,在列表中就不見啦?

帶着上述的問題,今天小金子來掰扯掰扯,下面分兩個方面來分析這些問題

onMeasure默認實現


我們知道自定義View(? extends View)的時候,我們需要自己繪製內容.和自定義ViewGroup不太一樣,自定義ViewGroup主要是如何排放每一個子View的位置.

我們先觀望一下系統的View默認的onMeasure方法是怎麼樣的

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;
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(
        getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
    );
}

因爲onMeasure方法涉及到其他的兩個方法,所以代碼都完整的貼出來了.在onMeasure方法中有一個setMeasuredDimension(int measuredWidth, int measuredHeight)方法,這個方法是很關鍵的.系統的註釋我就不貼出來了.這裏直接給出博主的解釋.
當我們的自定義View測量自己的寬和高的時候,一定要通過此方法保存測量好的寬和高.
沒有調用此方法保存測量好的寬和高或者調用了但是調用此方法的時候傳入的兩個值有一個是0的時候就是導致問題2出現的原因的其中兩個原因

getSuggestedMinimumWidth方法 和 getSuggestedMinimumHeight方法中的代碼很簡單,就是獲取自定義控件最小大小

getDefaultSize 方法很重要.從代碼中我們可以看到 getDefaultSize 方法中有兩個入參
第一個是最小的值,也就是getSuggestedMinimum*方法獲取到的值
第二個是父容器推薦的值和模式(MeasureSpec不懂的同學自行搜索下)
從swich中我們可以明顯的看出,這裏根據推薦的測量模式,進行不一樣的取值

最後onMeasure方法直接保存getDefaultSize方法防護的值到View內部,以供後續使用n

那麼我們可以總結出兩點:
1.當模式是 MeasureSpec.AT_MOST 或者 MeasureSpec.EXACTLY 的時候,兩者取得值是相同的,都是父容器推薦的值
2.當模式是MeasureSpec.UNSPECIFIED的時候,取的值是最小的值,這個模式一般只會出現在列表或者ScrollView中,因爲這類容器由於本身視圖是可以滾動的,拿ListView來說,縱向的高度可以認爲是無限大,那麼它就不可能有一個最大值可以推薦給你,所以這個模式就是用來描述父容器已經沒法給你一個推薦的值了,你得自己計算,如果不計算,視圖就看不見了
這也是問題6的根本原因

其實這裏就是問題4的根本原因

int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

這兩行代碼獲取到父容器推薦的值和計算模式

而這裏所謂的父容器推薦的模式,其實其中的兩個 MeasureSpec.AT_MOSTMeasureSpec.EXACTLY 是和我們在xml中寫的 wrap_contentmatch_parent 息息相關的。但是隻是相關,並不能決定,爲什麼呢?.

    /**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * The heavy lifting is done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param parentHeightMeasureSpec The height requirements for this 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 中的一段代碼,用於向子View推薦模式和值,那麼就簡單的從代碼中可以看出,子View中的被推薦的值和模式和 子View本身的LayoutParams(也就是你在xml中寫的寬和高)、measureChild方法中傳入的 parentWidthMeasureSpec, parentHeightMeasureSpec都息息相關
所以這裏只是先告訴你影響子View的推薦的模式和值的因素有哪些,在下一篇中會具體的講述
自定義ViewGroup測量方法的使用

影響測量推薦的值和模式的因素總結

以下是默認的情況下,系統的一些佈局RelativeLayout之類的可能對測量流程做了更改,所以發現不符合的情況,應該是系統的對默認的做了更改

我們就根據上面的默認實現的方法做一個小總結先,先有一個先入爲主的認識,下一篇還會中點講述

下面的 父View推薦模式 指的是上面的measureChild 方法傳入的parentWidthMeasureSpec 或者 parentHeightMeasureSpec中獲取出來的推薦模式,這樣子獲取

int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

1.
子View: wrap_content
父View推薦模式: MeasureSpec.AT_MOST
推薦結果: 模式:MeasureSpec.AT_MOST ,值:父View所能給的最大值
(推薦值用不用是你的事情)

2.
子View: wrap_content
父View推薦模式: MeasureSpec.EXACTLY
推薦結果: 模式:MeasureSpec.AT_MOST ,值:父View所能給的最大值
(推薦值用不用是你的事情)

3.
子View: wrap_content
父View推薦模式: MeasureSpec.UNSPECIFIED
推薦結果: 模式:MeasureSpec.UNSPECIFIED ,值:0或者父容器推薦的值 // 這種情況就是引起一個自定義控件在列表或者ScrollView中不可見的原因

4.
子View: match_parent
父View推薦模式: MeasureSpec.AT_MOST
推薦結果: 模式:MeasureSpec.AT_MOST ,值:父View所能給的最大值
(推薦值用不用是你的事情)
和第一種運行結果是一樣的,不一樣的是父視圖的大小,不在討論範圍內,下一篇介紹

5.
子View: match_parent
父View推薦模式: MeasureSpec.EXACTLY
推薦結果: 模式:MeasureSpec.EXACTLY ,值:父View所能給的最大值
(推薦值用不用是你的事情)

6.
子View: match_parent
父View推薦模式: MeasureSpec.UNSPECIFIED
推薦結果: 模式:MeasureSpec.UNSPECIFIED ,值:0或者父容器推薦的值 // 這種情況就是引起一個自定義控件在列表或者ScrollView中不可見的原因

7.
子View: 40dp // 舉例這種情況
父View推薦模式: MeasureSpec.AT_MOST
推薦結果: 模式:MeasureSpec.EXACTLY ,值:40dp

8.
子View: 40dp // 舉例這種情況
父View推薦模式: MeasureSpec.EXACTLY
推薦結果: 模式:MeasureSpec.EXACTLY ,值:40dp

9.
子View: 40dp // 舉例這種情況
父View推薦模式: MeasureSpec.UNSPECIFIED
推薦結果: 模式:MeasureSpec.UNSPECIFIED ,值:40dp

以上結論並不是我個人得出的,而是有依據的,代碼在ViewGroup類中的一個靜態的方法中

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

從這裏我們清晰的看到所有情況的判斷,這也是我總結的出處

下一篇自定義ViewGroup下一篇中我們會詳細的再說明的,暫時看不懂的小夥伴不用擔心.先往下看剩下的吧

上面也就解釋了爲什麼一個自定義控件,默認的時候,在xml寫 wrap_content 和 寫
match_parent 是一樣的效果了.
也解釋了爲什麼一個自定義View控件在沒有重寫onMeasure方法的時候,能顯示,就是因爲有一個默認的實現.過程不再贅述

onMeasure 自定義實現

上面講了整個onMeasure 方法的默認實現,我們有一點必須要很重視

那就是無論父視圖是如何推薦的,我們都應該根據推薦的模式和值對自身View的大小有一個精準的測量

那麼我們如何來自定義測量的代碼呢?下面切看我花樣作死~~~~



首先我們新建一個MyView類繼承View,然後重寫onMeasure方法,先從父容器推薦的傳入的兩個參數中獲取出計算模式和相應的值,這都是套路,學着點~~~

public class MyView extends View {

    public MyView(Context context) {
            this(context, null);
    }

    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

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

        // get calculate mode of width and height
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

        // get recommend width and height
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);


    }

}

然後我們上面說過必須調用* setMeasuredDimension(int measuredWidth, int measuredHeight)*方法來保存計算好的寬和高

第一浪

一直讓我們的自定義View顯示100px * 100px的寬和高

答案:

setMeasuredDimension(100,100); // 直接設置測量的寬和高是100px,不管什麼情況下

寬和高寫 wrap_content 的效果:

這裏寫圖片描述

寬和高寫上500dp的效果

這裏寫圖片描述

寬和高寫 match_parent 的效果

這裏寫圖片描述

我們可以看到,我們的大小根本不隨着xml的寬和高而有任何的改變,這就是因爲我們在onMeasure 方法中直接寫死了一個寬和高導致的!

第二浪

假如我們需要繪製一個100px * 100px的區域,實現xml中寫wrap_content的時候是包裹100px * 100px的區域的,其他情況大小由xml寫的爲準,也就是40dp就顯示40dp大小,match_parent就是最大的大小

答案:

public class MyView extends View {

    public MyView(Context context) {
            this(context, null);
    }

    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        p.setColor(Color.GREEN);
        p.setStyle(Paint.Style.FILL);
    }

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

        // get calculate mode of width and height
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

        // get recommend width and height
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);

        if (modeWidth == MeasureSpec.AT_MOST) { // wrap_content
            sizeWidth = Math.min(100,sizeWidth);
            modeWidth = MeasureSpec.EXACTLY;
        }

        if (modeHeight == MeasureSpec.AT_MOST) { // wrap_content
            sizeHeight = Math.min(100,sizeHeight);
            modeHeight = MeasureSpec.EXACTLY;
        }

        widthMeasureSpec = MeasureSpec.makeMeasureSpec(sizeWidth, modeWidth);
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(sizeHeight, modeHeight);

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    }

    private Paint p = new Paint();

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // draw a green rect(繪製一個100*100的正方形)
        canvas.drawRect(0,0,100,100,p);

    }
}

效果:

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

重點關注我們的onMeasure方法.由於我們上面分析得知,我們的自定義控件默認的實現支持xml中寫40dp這種固定的大小,也支持match_parent 的效果,就是不支持wrap_content,當wrap_content的時候應該是包裹着要繪製的視圖內容的,而我們上面的需求就是有個100px*100px的繪製區域,我們在裏面繪製了一個綠色的矩形.
所以我們在onMeasure只需要在默認的實現前面做一個小動作即可.判斷推薦的模式是MeasureSpec.AT_MOST 的時候把推薦的值和100做一個取小的操作即可
然後把計算模式改爲MeasureSpec.EXACTLY就行啦
爲什麼要100和推薦的值取小的操作,因爲有可能父容器推薦給你的值是小於100的,那麼你肯定不能讓自身的大小超過給你的值,當然了實際運行中,不進行如此細緻的判斷也是可以的,直接採用100,但是作爲程序員還是得思考的嚴謹些

onMeasure 支持內邊距

其實這個很簡單,因爲我們在xml中爲View寫的內邊距都會在創建這個View的時候轉化爲View控件的屬性

我們在測量的時候直接拿過來計算就行

其實我們只需要在包裹的情況下添加內邊距的值就行了,試想一下,如果一個View的繪製區域是100*100,而上下左右都有一個20的邊距,那麼這個View的大小理所應當應該是140 * 140
其他情況就不需要考慮內邊距啦

然後我們的繪製代碼也要改變了,因爲有內邊距的存在我們需要一個繪製偏移量,這也就是爲什麼下面的繪製代碼那麼寫的原因

總結

  • 1.onMeasure 方法就是讓你測量自身的大小並且存儲測量的大小的一個地方
  • 2.一定要注意,計算測量的大小後,一定要保存測量的值。不管是調用父類的onMeasure方法還是自己調用setMeasuredDimension(int measuredWidth, int measuredHeight)
  • 3.系統默認的實現中,包裹(MeasureSpec.AT_MOST)的情況是不支持的,因爲系統的View根本就不知道要繪製的內容的大小,所以包裹內容無從談起,而你自定義了View,那你就有這個責任支持包裹(MeasureSpec.AT_MOST)的情況,因爲你自己定義的View你自己知道繪製的大小
  • 4.保存的測量大小僅供父容器在排列子View的時候使用,或者壓根就不使用,所以切記在onDraw(Canvas canvas)方法中使用自己測量的 measuredWidthmeasuredHeight,
    因爲這個時候你已經知道了自身的實際大小,這個大小和父容器有關(後續文章跟進),所以你應該在onDraw(Canvas canvas)方法中直接使用getWidth()getHeight() 的值!
  • 5.當模式是MeasureSpec.UNSPECIFIED的時候,拿ListView來說,縱向的高度可以認爲是無限大,那麼它就不可能有一個最大值可以推薦給你,所以這個模式就是用來描述父容器已經沒法給你一個推薦的值了,你得自己計算,如果不計算,視圖就看不見了,這也就是爲什麼有些自定義控件在普通情況下是可見的,好使的,但是一放進列表中就雞雞了,原因就在於你在onMeasure中沒有判斷模式是MeasureSpec.UNSPECIFIED的情況,如果使用默認的,那就是上述中講的使用View的getSuggestedMinimumWidth方法 和 getSuggestedMinimumHeight方法中返回的值作爲測量的高度.其實作爲一個自定義控件,理應在MeasureSpec.UNSPECIFIED能呈現一個包裹視圖的狀態!所以這種情況和MeasureSpec.AT_MOST 類似處理即可

上述寫的100是因爲我的控件繪製區域就是100,你們可一定要按照你們自己的繪製區域來計算哈,別傻敷敷的瞎寫,哈哈

暫時就寫這麼多吧,如有不足後續再跟進吧,看到這裏的童鞋,如果喜歡小生,評論一下撒,反正我也不會回覆你,哈哈哈

貼出全部代碼(以供複製,ps:我多貼心!!)

public class MyView extends View {

    public MyView(Context context) {
            this(context, null);
    }

    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        p.setColor(Color.GREEN);
        p.setStyle(Paint.Style.FILL);
    }

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

        // get calculate mode of width and height
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

        // get recommend width and height
        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);

        if (modeWidth == MeasureSpec.UNSPECIFIED) { // this view used in scrollView or listview or recyclerView
            int wrap_width = 100 + getPaddingLeft() + getPaddingRight();
            sizeWidth = wrap_width;
            modeWidth = MeasureSpec.EXACTLY;
        }

        if (modeHeight == MeasureSpec.UNSPECIFIED) { // this view used in scrollView or listview or recyclerView
            int wrap_height = 100 + getPaddingTop() + getPaddingBottom();
            sizeHeight = wrap_height;
            modeHeight = MeasureSpec.EXACTLY;
        }

        if (modeWidth == MeasureSpec.AT_MOST) { // wrap_content
            int wrap_width = 100 + getPaddingLeft() + getPaddingRight();
            sizeWidth = Math.min(wrap_width,sizeWidth);
            modeWidth = MeasureSpec.EXACTLY;
        }

        if (modeHeight == MeasureSpec.AT_MOST) { // wrap_content
            int wrap_height = 100 + getPaddingTop() + getPaddingBottom();
            sizeHeight = Math.min(wrap_height,sizeHeight);
            modeHeight = MeasureSpec.EXACTLY;
        }

        widthMeasureSpec = MeasureSpec.makeMeasureSpec(sizeWidth, modeWidth);
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(sizeHeight, modeHeight);

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    }

    private Paint p = new Paint();

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // draw a green rect(繪製一個100*100的正方形)
        canvas.drawRect(getPaddingLeft(),getPaddingTop(),getPaddingLeft() + 100,getPaddingTop() + 100,p);

    }
}

這裏寫圖片描述

發佈了119 篇原創文章 · 獲贊 275 · 訪問量 37萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章