上一篇文章,介紹了MeasureSpec類的基礎知識,包括三種模式及子view的measureSpec的生成過程,搞懂了這個,那麼我們下面就可以進入到onMeasure流程中了,同時,也會在上一篇的基礎上做一下關于自定義view wrap_content和match_parent的補充。
onMeasure源碼分析
onMeasure
/**
* <p>
* Measure the view and its content to determine the measured width and the measured height. This method is invoked by {@link #measure(int, int)} and should be overriden by subclasses to provide accurate and efficient measurement of their contents.
* </p>
* @param widthMeasureSpec horizontal space requirements as imposed by the parent.The requirements are encoded with {@link android.view.View.MeasureSpec}.
* @param heightMeasureSpec vertical space requirements as imposed by the parent. The requirements are encoded with {@link android.view.View.MeasureSpec}.
*
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
通過源碼可以看出,onMeasure流程為:
- 通過setMeasureDimension設定measure階段view的寬和高
- 通過getDefaultSize方法擷取預設的寬度和高度
- 通過getSuggestedMinimumHeight/width方法擷取建議的最小寬度或高度值
下面,一次進入對應的方法,看一下内部實作。
getSuggestedMinimumWidth(height)
/**
* Returns the suggested minimum height that the view should use. This
* returns the maximum of the view's minimum height
* and the background's minimum height
* ({@link android.graphics.drawable.Drawable#getMinimumHeight()}).
* <p>
* When being used in {@link #onMeasure(int, int)}, the caller should still
* ensure the returned height is within the requirements of the parent.
*
* @return The suggested minimum height of the view.
*/
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
- 其中mBackground為view的背景,如果有背景的話,可以擷取到其最小高 寬度值
- mMinHeight 此值可以在xml中通過minHeight設定,也可以通過view的setMinimumHeight()方法設定
getDefaultSize
/**
* Utility to return a default size. Uses the supplied size if the
* MeasureSpec imposed no constraints. Will get larger if allowed
* by the MeasureSpec.
*
* @param size Default size for this view
* @param measureSpec Constraints imposed by the parent
* @return The size this view should be.
*/
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的寬度或高度值,通過switch,可以看出有兩種傳回,分别如下:
- measureSpec的mode為unspecified時,傳回的就是view的最小寬度或高度值,這種情況一般很少見
- measureSpec的mode為exactly或at_most時,傳回的是view measureSpec中的specSize
由此,也得知,在measure階段中,view的大小寬高,由其measureSpec中的specSize決定的。
解決自定義view之wrap_content問題
問題及MeasureSpec規則回顧
首先回顧一下上篇文章中,繪制的measureSpec形成圖,如下
由圖可以看到,當子view的寬高為warp_content時,不管父容器的specMode為exactly還是at_most,其占據的空間都為parentLeftSize,顯然,這不是我們所期望的,那麼,我們就應該對wrap_content的情況在onMeasure階段進行特殊處理。
解決方案
- 如果在xml中,寬高均為wrap_content,需要設定view的寬高為mWidth mHeight
- 如果在xml中,寬高有一個被設定為wrap_content,那麼就将該值設定為預設值,另一個采用系統測量的specSize即可,代碼示例如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//擷取寬高的size mode
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSise = MeasureSpec.getSize(heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//寬高都為wrap_content
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if (widthMode == MeasureSpec.AT_MOST) {
//寬度為wrap_content
setMeasuredDimension(mWidth, heightSise);
} else if (heightMode == MeasureSpec.AT_MOST) {
//高度為wrap_content
setMeasuredDimension(widthSize, mHeight);
}
}
好了,現在就解決了自定義view寬高為wrap_content的問題了,不過。。。。
引發的另一個“血案”
上面成功解決了wrap_content問題,不過,不知道注意到沒有,在子view的layoutParams為match_parent,父容器的specMode為at_most時,子view的specMode也為at_most,其size也為parentLeftSize,那這種情況下,子view match_parent的case也會走到剛才解決wrap_content的代碼中,那麼本來寬高想要填充父容器,現在卻被設定了預設值,這樣也不合理的,有木有,有木有!!!
不過呢,仔細推理一下,就會發現,原來。。。搜噶。
問:什麼情況下,父容器的specMode為at_most呢
答:兩種情況:
a. 當父容器的layoutParams為wrap_content,系統給父容器的specMode為at_most
b. 父容器的layoutParams為match_parent,系統給父容器的specMode為at_most
下面,來分别分析一下這兩種情況。
A. 父容器為wrap_content,子view為match_parent,子view為包裹内容,想和父容器一樣大,而父容器又不知道自己知道多大,那麼兩者就陷入死循環了,誰也決定不了誰,這種case理論上可能出現,但是實際中一般是不可取的。
B. 既然父容器是match_parent,那麼爺(父容器的父容器)應該為什麼呢?下面,排除法,列舉一下
- 爺容器為wrap_content,此case同A,不可取,不正确的
- 精确的值,比如200dp。試想一下,如果爺容器為精确的值,爺mode為exactly,父為match_parent,那麼父的mode就不可能為at_most,而應該是exactly,是以這種也是不可能的
- 爺容器為match_parent,爺容器的mode可能為exactly或at_most,那麼,分别分析一下
- 爺容器mode為exactly,這種情況下,父容器的mode應該為exactly,而不是at_most,是以這種case是不可能的。
- 爺容器mode為at_most,大小為match_parent,那麼父容器的mode為at_most,這是唯一可能存在的情況。仔細分析一下,這樣就會陷入一個死循環,子view大小是match_parent,mode為at_most,父容器大小是match_parent,mode為at_most,爺容器大小也是match_parent,mode為at_most,如此下去,直到rootview,如果根view的大小為match_parent,那麼其對應的mode應該為exactly,是以這種case也是不可能的。
由上面推測發現,如果子view的大小為match_parent,并且父容器的mode為at_most,那麼此時子view的mode也為at_most其大小為parentLeftSize,這種情況是不合理的,不可取的。
ViewGroup的measure
viewgroup是一個抽象類,他提供了測量child的measureChildren方法,在該方法裡,又會調用measureChild方法,在measureChild方法裡,會執行child.measure()方法,就回到我們前面分析的流程中了。
執行步驟:measureChildren()—>measureChild()—>child.measure()
ViewGroup是一個抽象類,沒有實作onMeasure()方法,那麼其子類就應該根據自身的特點去測量子view的,測量好了子view的大小,那麼其自身的大小也就明确了。比如linearLayout根據布局方向完成測量,relativeLayout根據子view的相對位置完成測量,等等。
結束語
至此,關于自定義view寬高為wrap_content的情況解決了,onMeasure()的調用過程也簡單的分析了一下,歡迎指出不足和問題,不定時再更新自己的了解和補充。