天天看點

Android自定義View基礎之onMeasure詳解

上一篇文章,介紹了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形成圖,如下

Android自定義View基礎之onMeasure詳解

由圖可以看到,當子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()的調用過程也簡單的分析了一下,歡迎指出不足和問題,不定時再更新自己的了解和補充。