View學習(二)-View的測量(measure)過程


上一篇文章中,我們介紹了DecorViewMeasureSpec, 下面的文章就開始討論View的三大流程。

View的三大流程都是通過ViewRoot來完成的。ViewRoot對應於ViewRootImpl類,它是連接WindowManagerDecorView的紐帶。在ActivityThread中,當Activity對象被創建完畢之后,會將DecorView添加到Window中,同時創建ViewRootImpl對象,並將ViewRootImpl對象和DecorView建立關聯。

View的繪制流程是從ViewRootImpl#performTraversals()開始的,它經過measure(測量),layout(布局),draw(繪制)三個過程才能最終將一個view顯示出來。

  • measure過程測量View的寬高尺寸。Measure完成以后,就可以通過getMeasuredWidth()getMeasuredHeight()來獲取View的寬高了。
  • layout過程確定View在父容器中的擺放位置。layout()完成之后,就可以通過getTop()getLeft(), getRight(), getBottom()來拿到View的左上角,右下角的坐標數據了。
  • draw過程負責將View繪制在屏幕上。draw()方法完成之后,View才能顯示在屏幕上。

ViewRootImpl#performTraversals()依次調用performMeasure, performLayout,performDraw三個方法。這三個方法依次完成View的 measure,layout,draw過程。

借用網上的一張圖,可以清晰的表達這個過程。

  • performMeasure調用 View#measure方法,而View#measure 則又調用 onMeasure方法,而對於View中的onMeasure方法,直接保存了測量得到的尺寸,而類似FrameLayout,RelativeLayout等各種容器ViewGroup,則在自己覆蓋重寫的的onMeasure方法中,對所有的子元素進行measure過程,此時measure流程就從父容器傳遞到子View中了,這就完成了一次measure過程,最終這個遞歸的過程完成了整個View樹的遍歷。
  • performLayout,performDraw的傳遞流程也是類似的。唯一不同的是performDraw的傳遞過程是通過draw方法中通過dispatchDraw來實現的,但是道理都是相同的。
  • traversals的意思就是遍歷。

其實到了這個時候,我們大概的流程就已經知道了,那么剩下的具體就看看View和ViewGroup當中的具體的測量過程了。這兩個還要分情況討論,因為View是屬於自己一人吃飽,全家不餓,把自己測量好就行了,可是ViewGroup不僅要測量自己,還要遍歷去調用所有子元素的measure過程,從而形成一個遞歸過程。

而measure的大流程則如下:

測量的時候父控件的onMeasure方法會遍歷他所有的子控件,挨個調用子控件的measure方法,measure方法會調用onMeasure,然后會調用setMeasureDimension方法保存測量的大小,一次遍歷下來,第一個子控件以及這個子控件中的所有孫控件都會完成測量工作;然后開始測量第二個子控件…;最后父控件所有的子控件都完成測量以后會調用setMeasureDimension方法保存自己的測量大小。值得注意的是,這個過程有可能重復執行多次,因為有的時候,一輪測量下來,父控件發現某一個子控件的尺寸不符合要求,就會重新測量一遍。

借用某個大神博客上的一張圖

View的measure過程

View的測量過程由measure()來完成,measure()方法是final的,子類無法重寫覆蓋。它會調用onMeasure方法。

根據measure的源碼,View其實也是比較喜歡偷懶的。意思是執行了measure方法並不一定將測量過程完整走一遍(就是調用onMeasure方法)。具體來說,如果View發現不是強制測量,且本次傳入的MeasureSpec與上次傳入的相同,那么View就沒必要重新測量一遍。如果真的需要測量,View也先查看之前是否緩存過相應的計算結果,如果有緩存,直接保存計算結果,就不用再調用onMeasure了。這樣也是最大限度的提高效率,避免重復工作。

onMeasure方法代碼如下:

//View#onMeasure
/**
 *  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 overridden by subclasses to provide accurate and efficient
 *  measurement of their contents.
 */
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

首先需要注意,該方法注釋的一句話,

This method is invoked by {@link #measure(int, int)} and should be overridden by subclasses to provide accurate and efficient measurement of their contents.

這就意味着,我們在進行自定義View時,是應該重寫覆蓋這個方法的。

先看一下onMeasure方法中調用的getDefaultSize方法。

//View#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;
    }

UNSPECIFIED模式一般用於系統內部的測量過程,在這種情況下,View的大小為getDefaultSize的第一個參數size,即寬度為getSuggestedMinimumWidth()返回的值,而getSuggestedMinimumWidth()可以用一句偽代碼來表示。

    mMinWidth = android:minWidth;
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());

對於測量過程而言,width,height過程都是一樣的流程。所以為了行文簡單,所以我們就簡單的只說width。

再回過頭來看 getDefaultSize方法的源碼。可以看到,不管我們的View的LayoutParams設置的是 match_parent或者 wrap_content,它的最終尺寸都是 相同的。再結合上一篇文章末尾我們根據getChildMeasureSpec方法而整理出來的那張表。

當View為wrap_content時,它的size就是parentSize-padding,這和match_parent時,size是一樣的。(雖然mode可能不一樣)。

結論就是: 對於View來講,使用wrap_content和使用match_parent效果是一樣的,它倆的默認size是相同的。

所以各個子View,(當然也包括我們extends View,自定義的View),在測量階段,針對wrap_content都要做相應的處理,否則使用 wrap_content就和使用 match_parent效果都是一樣的, 那就是默認填充滿父View剩下的空間。

而我們在自定義View時,針對wrap_content情況一般處理方式是,在onMeasure中,增加對MeasureSpec.AT_MOST的判斷語句,結合具體業務場景或情況,設定一個默認值,或計算出一個值。

wrap_content的意思就是 包裹內容,但是仔細思考一下,內容又是什么呢,具體到不同的子View場景,肯定有不同的意義,所以從這個角度來思考,作為繼承結構上最頂級,同時也是最抽象的View而言,wrap_cotentmatch_parent默認尺寸一樣,也就有道理了。

其實普通View的onMeasure邏輯大同小異,基本都是測量自身內容和背景,然后根據父View傳遞過來的MeasureSpec進行最終的大小判定,例如TextView會根據文字的長度,大小,行高,行寬,顯示方式,背景圖片,以及父View傳遞過來的模式和大小最終確定自身的大小.

在測量結束時,調用了setMeasuredDimension來存儲測量得到的寬高值,該方法源碼當中,注釋是這樣的。

This method must be called by {@link #onMeasure(int, int)} to store the measured width and measured height. Failing to do so will trigger an exception at measurement time.

如果我們自定義View時,重寫了onMeasure方法,那么就需要調用setMeasuredDimension方法來保存結果,否則就會拋出異常。

ViewGroup的measure過程

ViewGroup比View復雜,因為它要遍歷去調用所有子元素的measure過程,各個子元素再遞歸去執行這個過程。正所謂領導都要能者多勞之,領導要干的工作也比較多。

ViewGroup是一個抽象類,它沒有重寫View的onMeasure方法,這是合理的,因為各個不同的子容器(比如LinearLayout, FrameLayout,RelativeLayout等)它們有它們特定的具體布局方式(比如如何擺放子View),所以ViewGroup沒辦法具體統一,onMeasure的實現邏輯,都是在各個具體容器類中實現的。

但是ViewGroup當中,提供了三個測量子控件的方法。

/**
  *遍歷ViewGroup中所有的子控件,調用measuireChild測量寬高
  */
 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);
        }
    }
}

/**
* 測量某一個child的寬高
*/
//ViewGroup中方法。
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);
}

/**
* 測量某一個child的寬高,考慮margin值
*/
protected void measureChildWithMargins (View child,
       int parentWidthMeasureSpec, int widthUsed,
       int parentHeightMeasureSpec, int heightUsed) {
   final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
   //獲取子控件的寬高約束規則,相比於 measureChild方法,這里考慮了 lp.margin值
   final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
           mPaddingLeft + mPaddingRight + lp. leftMargin + lp.rightMargin
                   + widthUsed, lp. width);
   final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
           mPaddingTop + mPaddingBottom + lp. topMargin + lp.bottomMargin
                   + heightUsed, lp. height);
   //測量子控件
   child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

望名知意,根據方法的命名我們就能知道每個方法的作用。measureChild,或measureChildWithMargin,它們的作用就是取出child的layoutParams,然后再通過getChildMeasureSpec來創建child的MeasureSpec,然后再將MeasureSpec傳遞給child進行measure。這樣就完成了一輪遞歸。

所以我們在上一篇博客中,着重介紹了getChildMeasureSpec方法,指出這個方法是很重要的。

  • measureChildWithMarginmeasureChild的區別就是父控件是否支持margin屬性。

因為各個不同的容器(比如LinearLayout,FrameLayout,RelativeLayout等),都有各自的measure方式,所以我們就挑選LinearLayout來看一下它的measure流程。

    //LinearLayout
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mOrientation == VERTICAL) {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        }
    }

我們就選擇豎直方向的measure過程來分析。
源碼比較長,我們只看主流程。

//LinearLayout#measureVertical
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
    final View child = getVirtualChildAt(i);
        
        // ....

        // Determine how big this child would like to be. If this or
        // previous children have given a weight, then we allow it to
        // use all available space (and we will shrink things later
        // if needed).
        final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
        measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
                heightMeasureSpec, usedHeight);

        final int childHeight = child.getMeasuredHeight();
        
        final int totalLength = mTotalLength;
        mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
               lp.bottomMargin + getNextLocationOffset(child));

}

系統會遍歷子元素,並對每個子元素執行measureChildBeforeLayout方法,該方法內部會調用子元素的measure方法,這樣各個子元素就依次進入measure過程。系統通過mTotalLength這個變量動態存儲LinearLayout在豎直方向上的高度,並且伴隨着每測量一個子元素,mTotalLength則會逐步增加。增加的部分包括了子元素的高度以及子元素在豎直方向上的margin等。

而當所有子元素都測量完畢之后,LinearLayout則會測量自己的大小。

 //LinearLayout#measureVertical
 // Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;

int heightSize = mTotalLength;

// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());

// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;



setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
        heightSizeAndState);

針對豎直方向的LinearLayout而言,它在水平方向上的測量過程遵循View的測量過程,而豎直方向則和View有所不同,具體來說,如果它的高度參數是具體數值或match_parent,則測量過程和View一致,即高度為SpecSize;而如果高度參數采用的是wrap_content,那么它的高度就是所有子元素所占用的高度總和,但是仍然不能超過它父容器的剩余空間。並且最終高度還要考慮豎直方向的padding。

具體可以參考以下源碼:

  //LinearLayout
  public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        final int specMode = MeasureSpec.getMode(measureSpec);
        final int specSize = MeasureSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
            case MeasureSpec.AT_MOST:
                if (specSize < size) {
                  //當specMode為AT_MOST,並且父控件指定的尺寸specSize小於View自己想要的尺寸時,
                //我們就會用掩碼MEASURED_STATE_TOO_SMALL向測量結果加入尺寸太小的標記
                //這樣其父ViewGroup就可以通過該標記知道其給子View的尺寸太小了,
                //然后可能分配更大一點的尺寸給子View
                    result = specSize | MEASURED_STATE_TOO_SMALL;
                } else {
                    result = size;
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
            case MeasureSpec.UNSPECIFIED:
            default:
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }

resolveSizeAndState方法和getDefaultSize方法類似,其內部實現邏輯是一樣的,不過區別在於,resolveSizeAndState方法除了返回尺寸信息,還會返回測量的status標志位信息。

View的測量過程是三大流程中比較復雜的,只有測量完畢之后,我們才有可能得到正確的寬高值。

而View的measure過程和Activity的生命周期方法是不同步的。所以我們不能簡單的通過onStart,onResume方法來獲取View的尺寸值。

參考內容:


作者: www.yaoxiaowen.com

github: https://github.com/yaowen369

歡迎對於本人的博客內容批評指點,如果問題,可評論或郵件(yaowen369@gmail.com)聯系

<p >
		 歡迎轉載,轉載請注明出處.謝謝
</p>


<script type="text/javascript">
 function    Curgo()   
 {   
     window.open(window.location.href);
 }   
</script>


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM