banner
jzman

jzman

Coding、思考、自觉。
github

自訂View中為何match_parent和wrap_content效果一樣

PS:如果不能預測未來是什麼樣的,那麼就好好做好當下。

今天來分享一個我在自定義 View 中遇到的問題,如果分析有誤,還望各位指出,在自定義 View 的過程中一定會遇到一個問題,自定義 View 沒有問題,唯獨在自定義的 View 中 match_parent 和 wrap_content 效果一致,onMeasure () 方法如下:

/**
 *
 * 測量View的寬度和高度,這個方法由 measure方法調用,一般由子類重寫該方法以提供更加精確和高效的測量
 *
 * 規定:當重寫該方法的時候。你必須調用setMeasuredDimension(int, int)方法來存放View的測量寬度和高度,
 * 測量失敗拋出 IllegalStateException
 *
 * 測量的基類實現默認為背景大小,除非允許更大尺寸的測量規範,子類應該重寫該方法以便提供更好地測量
 *
 * 如果該方法被重寫,則子類的責任就是確保測量的高度和寬度至少是View的最小寬度和高度
 *
 * @param widthMeasureSpec 父容器施加的水平方向測量規範.
 * @param heightMeasureSpec 父容器施加的垂直方向測量規範.
 */
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //存儲測量的寬高
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

看一下 getDefaultSize 方法的具體實現:

//根據指定的測量模式獲得對應的大小
public static int getDefaultSize(int size, int measureSpec) {
    //默認大小,其大小與是否設置背景相關
    int result = size;
    //從View的測量規範中獲得對應的測量模式和測量大小
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
    //根據View的測量模式設置當前View的大小
    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    //返回View的尺寸大小
    return result;
}

顯然,默認情況下 View 指定的寬高為 match_parent 和 wrap_content 時,也就是測量模式為 AT_MOST 和 EXACTLY 的時候,最終返回的都是從指定 MeasureSpec 中獲得的尺寸大小,所以默認情況下設置 View 的寬高為 match_parent 和 wrap_content 時效果是一樣的。

onMeasure 中的寬度和高度的 MeasureSpec 是由父 View MeasureSpec 及自身的佈局參數 LayoutParamas 來確定,具體體現在 ViewGroup 中的 getChildMeasureSpec () 方法中,源碼如下:

/**
 * 測量子View的難點:找出MeasureSpec傳遞給指定的子View,該方法找出了正確的MeasureSpec用於子View寬度或高度的測量
 *
 * 該方法的目標是結合MeasureSpec和子View的LayoutParams獲得最準確結果
 * 如父View的MeasureSpec中指定的測量模式為EXACTLY,子View制定了確切的尺寸大小,則父View會給子View一個確切的大小
 *
 * @param spec 父View指定的寬或高的MeasureSpec
 * @param padding 如果當前View設置了外邊距和內邊距,表示當前View的內邊距和外邊距,
 *                如 mPaddingLeft + mPaddingRight + mLeftMargin + mRightMargin
 * @param childDimension 當前View的寬或高
 * @return 子View測量後的對應尺寸大小
 */
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    //獲得當前測量維度的父View測量模式和測量大小
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);
    //獲得子View真正可以可以使用的尺寸大小
    int size = Math.max(0, specSize - padding);
    //子View的測量模式和尺寸大小,還需測量完成
    int resultSize = 0;
    int resultMode = 0;
    //根據父View指定的不同的測量模式对子View進行測量
    switch (specMode) {
        //EXACTLY:父View指定確切的尺寸,如match_parent、100dp
        case MeasureSpec.EXACTLY:
            //如果子View指定了確切的尺寸大小
            if (childDimension >= 0) {
                //指定尺寸大小為子View自己設置的尺寸大小
                resultSize = childDimension;
                //指定子View測量模式為EXACTLY

            //如果子View設置了尺寸大小是:MATCH_PARENT
            } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
                //指定子View的尺寸大小為子View可以是使用的最大的尺寸
                resultSize = size;
                //指定子View測量模式為EXACTLY

            //如果子View設置了尺寸大小是:WRAP_CONTENT
            } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
                // 子View的尺寸大小由自己包裹的內容決定,但是不能超過父View指定的尺寸大小
                resultSize = size;
                //指定子View測量模式為EXACTLY
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        //AT_MOST:父View允許子View一個能夠達到的最大的尺寸大小,如wrap_content
        case MeasureSpec.AT_MOST:
            //如果子View指定了確切的尺寸大小
            if (childDimension >= 0) {
                // 指定子View的測量模式和測量大小,此時可以確定子View的測量模式的尺寸大小
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;

            } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) {
                //子View想要獲得父View指定的最大尺寸,但是父View的尺寸大小不能確定,子View的大小只能由子View自己包裹的內容決定
                //故測量模式指定為 MeasureSpec.AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;

            } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // UNSPECIFIED:父View不对子View約束,子View可以設置它想要的大小
        //如果父View指定的測量模式是UNSPECIFIED,其子View自身的尺寸大小如果不確切指定,則
        // 子View尺寸大小有兩種取值範圍:0或子View所被允許的最大尺寸
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;

            } else if (childDimension == ViewGroup.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 == ViewGroup.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;
    }
    //返回子View對應的MeasureSpec
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

上面只是對 getChildMeasureSpec () 源碼的分析,回到問題本身,為什麼在自定義 View 中默認情況下為何寬高設置為 match_parent 和 wrap_content 效果一樣,下面表格是根據上述代碼分析總結如下:

父 View 的 MeasureSpecEXACTLYAT_MOSTUNSPECIFIED
具體尺寸childDimension(EXACTLY)childDimension(EXACTLY)childDimension(EXACTLY)
match_parentsize(EXACTLY)size(AT_MOST)size(UNSPECIFIED)
wrap_contentsize(AT_MOST)size(AT_MOST)size 或 0 (UNSPECIFIED)

上面表格內容都是基於 getChildMeasureSpec () 方法的內部實現,默認狀態下當子 View 的寬高設置為 match_parent 或 wrap_content 時,其子 View 的實際大小是 size,size 表示為子 View 可以真正可以使用的大小,及除去內邊距和外邊距之外子 View 可以使用的尺寸大小,當我們自定義 View 時計算坐標的時候使用到的就是尺寸大小 size,雖然最終子 View 的 MeasureSpec 可能不同,但是從某個具體的 MeasureSpec 中解析出來的 size 是一樣,所以在自定義 View 中默認情況下寬高設置為 match_parent 和 wrap_content 效果一樣,到此這個問題就算找到原因了。

那麼如何解決這個問題呢,當然就要在獲取自定義 View 的寬高之前重新保存合適的寬高的尺寸大小,這個尺寸大小可以由具體的需求設置一個默認的大小,即當設置自定義 View 的 LayoutParams 設置為 wrap_content 時要設置默認的尺寸,比如下面這樣處理:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //wrap_content默認寬高
    Rect mRect = new Rect();
    mLetterPaint.getTextBounds("A", 0, 1, mRect);
    int mDefaultWidth = mRect.width() + dpToPx(mContext, 12);
    int mDefaultHeight = mRect.height() + dpToPx(mContext, 5);

    //重新設置wrap_content時的默認寬高
    if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT &&
            getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
        //重新保存合適的寬高
        setMeasuredDimension(mDefaultWidth, mHeight);
    } else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
        setMeasuredDimension(mDefaultWidth, heightSize);
    } else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
        setMeasuredDimension(widthSize, mDefaultHeight);
    }
    //如果當前View的LayoutParams為wrap_content則獲取的寬高就是對應的默認寬高
    int mWidth = getMeasuredWidth();
    int mHeight = getMeasuredHeight();
}

上面是自定義 View 實現一個日期選擇器這篇文章中的相關代碼,具體可以點擊查看,

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。