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 的 MeasureSpec | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
具體尺寸 | childDimension(EXACTLY) | childDimension(EXACTLY) | childDimension(EXACTLY) |
match_parent | size(EXACTLY) | size(AT_MOST) | size(UNSPECIFIED) |
wrap_content | size(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 實現一個日期選擇器這篇文章中的相關代碼,具體可以點擊查看,