Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 從0開始自定義控件之 View 的 measure 過程(七)

Android 從0開始自定義控件之 View 的 measure 過程(七)

編輯:關於Android編程

前言

經過前面2篇的鋪墊,終於到正式學習 View 的三大流程:測量、布局、繪制流程了,這一篇就先從學習 measure 過程開始吧。

measure 過程要分兩種情況,第一種是 View,第二種是 ViewGroup。如果是 View 的話,那麼只通過 measure 方法就完成其測量過程,但是如果是 ViewGroup 的話,不僅需要完成自己的測量過程,還需要完成它所有子 View 的測量過程。如果子 View 又是一個 ViewGroup,那麼繼續遞歸這個流程。下面先從 View 開始,詳細了解下 View 的 measure 過程。

View 的 measure 過程

View 的測量過程是由 View 的 measure 方法來完成的,但是該方法是一個 finall 方法,所以不能被重寫。在 measure 方法中會去調用 onMeasure() 方法,因此我們只需在 View 中重寫 onMeasure() 方法來完成 View 的測量即可。那麼 View 默認的 measure 實現是怎樣的呢? 來看下 View 的 onMeasure() 方法:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

可以看到,該方法的實現很簡單,直接調用了 setMeasuredDimension() 方法來設置測量的尺寸。關鍵就在於 getDefaultSize() 方法上, 繼續跟進,看看 getDefaultSize() 方法的實現::

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

從上述代碼上可以看到,關於我們關心的 AT_MOST 和 EXACTLY 測量模式,其實 getDefaultSize() 方法返回的就是 MeasureSpec 的 specSize。
而這個 MeasureSpec 如果閱讀過上篇文章後,就應該知道是 ViewGroup 傳遞而來的。如果不太了解,建議返回去看下上篇文章,這裡就不重復介紹了。

到這裡也就理解了,為什麼當我們在布局中寫 wrap_content,如果不重寫 onMeasure() 方法,則默認大小是父控件的可用大小了。
當我們在布局中寫 wrap_content 時,那麼測量模式就是: AT_MOST,在該模式下,它的寬高等於 specSize。而 specSize 由 ViewGroup 傳遞過來時就是 parentSize,也就是父控件的可用大小。
當我們在布局中寫 match_parent 時,那麼不用多說,寬高當然也是 parentSize。這時候,我們只需對 AT_MOST 測量模式進行處理:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    int width = 0;
    int height = 0;

    if(widthMode == MeasureSpec.AT_MOST){
        width = ...
    }

    if(heightMode == MeasureSpec.AT_MOST){
        height = ...
    }

    setMeasuredDimension(widthMode != MeasureSpec.AT_MOST ? widthSize : width,
            heightMode != MeasureSpec.AT_MOST? heightSize : height);
}

上述代碼,判斷當測量模式是最大模式時,自己計算 View 的寬高。其他情況,直接使用 specSize。

至於 UNSPECIFIED 這種情況,則是使用的第一個參數的值,也就是:getSuggestedMinimumWidth()和getSuggestedMinimumHeight()方法,一般用於系統內部的測量過程。
這兩個方法的源碼如下:

protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

}
protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

大概意思就是,判斷 View 有沒有背景,沒有背景的話,那麼值就是 View 最小的寬度或高度,也就是對應 xml 中的:android:minWidth、android:minHeight 屬性,如果屬性沒有指定的話,默認為0。
有背景的話,那麼值就是 View 最小的寬度或高度 和 背景的最小寬度或高度,取兩者中最大的一個值。這個值就是當測量模式是 UNSPECIFIED 時 View 的測量寬/高。

到這裡就完成了整個 View 的 measure 過程,完成之後我們就可以通過 getMeasureWidth() 和 getMeasureHeight() 方法獲取 View 正確的測量寬/高了。但是需要注意的時,在某些極端情況下,系統可能需要再多次 measure 過程後才能確定最終的測量寬/高,在這種情況下,直接在 onMeasure() 方法中獲取的測量寬/高可能是不准確的,保險的做法是在 onLayout() 方法中去獲取。

ViewGroup 的 measure 過程

ViewGroup 的 measure 過程 和 View 不同,不僅需要完成自身的 measure 過程,還需要去遍歷所有子 View 的 measure 方法,各個子元素之間再遞歸這個流程。
ViewGroup 提供了一個叫 measureChildren() 的方法:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

該方法遍歷了所有的子 View,判斷如果子 View 沒有 GONE 掉的時候,就繼續執行 measureChild() 方法:

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

該方法獲取了子 View 的 LayoutParams,然後通過 getChildMeasureSpec() 方法創建了子 View 的 MeasureSpec,至於是怎麼生成的,上一篇關於 MeasureSpec 的文章有寫。
創建好子 View 的 MeasureSpec 後,然後將 MeasureSpec 傳給了子 VIew 進行 View 的 measure 過程。

通過上面的代碼我們可以發現,ViewGroup 並沒有定義其具體的測量過程,這是因為 ViewGroup 是一個抽象類,它測量過程的 onMeasure 方法需要它的子類去實現,比如說像 LinearLayout、RelativeLayout等。
它並不像 View 一樣,對 onMeasure 方法做了統一實現,這是因為它的子類都有不同的布局特性,就像 LinearLayout 和 RelativeLayout 一樣,兩者的布局特性截然不同,沒有辦法做統一實現。

注意事項

由於 View 的 measure 過程和 Activity 的生命周期不是同步的,那麼如果直接在 Activity 的生命周期方法,如:onCreate() 、onStart()、onResumt() 中直接獲取 View 的寬/高是無法正確獲取到的。
因為沒辦法保證當走這些生命周期回調方法前,View 的 measure 過程已經走完。如果沒有走完就直接獲取的話,那麼得到的只會是 0。下面給出幾種解決方法:

方案1:
重寫 onWindowFocusChanged() 方法,在該方法中獲取寬/高:
@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if(hasFocus){
        int measuredWidth = view.getMeasuredWidth();
        int measuredHeight = view.getMeasuredHeight();
    }
}

該方法會在當前 Activity 的 Window 獲得或失去焦點的時候回調,當回調該方法時,表示 Activtiy 是完全對用戶可見的,這時候 View 已經初始化完畢、寬/高都已經測量好了,這時就能獲取到寬/高了。

方案2:
view.post(new Runnable() {
    @Override
    public void run() {
        int measuredWidth = view.getMeasuredWidth();
        int measuredHeight = view.getMeasuredHeight();
    }
});

該方案,通過 post 方法將一個 runnable 投遞到消息隊列的底部,然後等待 Looper 調用該 runnable 時,View 也已經初始化好了,這時就能獲取到寬/高了。

方案3:
ViewTreeObserver treeObserver = view.getViewTreeObserver();
treeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
        int measuredWidth = view.getMeasuredWidth();
        int measuredHeight = view.getMeasuredHeight();
    }
});

該方案,通過監聽 View 樹的狀態發生改變或者 View 樹內部的 View 可見性發生改變時,在 onGlobalLayout 回調中獲取 View 的寬/高。需要注意的時,該回調會被調用多次,所以這裡在第一次回調中,就移除了監聽,避免多次獲取。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved