View的Measure流程

首先,Measure流程 是爲了測量,並計算view的大小.寬mMeasuredWidth,高mMeasuredHeight,然後將寬高保存.爲後續layout 和draw 提供數據支撐.

在閱讀本文之前, 請參照view和ViewGroup源碼.

數值保存MeasureSpec

父容器的layoutParams會確認MeasureSpec,即view的測量模式和大小
MeasureSpec包含一個32位的int值,高2位代表SpaceMode,低30位代表SpecSize.
MeasureSpec有三類.view會有兩個MeasureSpec變量,分別爲widthMeasureSpec,heightMeasureSpec.

以下結論會在getChildMeasureSpec中得到驗證
1. EXACTLY :父容器已經測量出所需要的精確大小,這也是childview的最終大小——match_parent,精確值.

  1. ATMOST : child view最終的大小不能超過父容器的給的——wrap_content .

  2. UNSPECIFIED: 不確定,源碼內部使用——-一般在ScrollView,ListView .

MeasureSpace大多數情況下是由父佈局的MeasureSpace和自己的Layoutparams確定(當然,還有margin,padding).詳情見viewgroup.getChildMeasureSpec()

View

View的關鍵方法

2. measure 父佈局會在自己的onMeasure方法中,調用child.measure ,這就把measure過程轉移到了子View中。

3. onMeasure 子View會在該方法中,根據父佈局給出的限制信息,和自己的content大小,來合理的測量自己的尺寸。

4. setMeasuredDimension當View測量結束後,把測量結果保存起來,具體保存在mMeasuredWidth和mMeasuredHeight中。

View的測量過程

measure()–>onMeasure()–>setMeasuredDimension()

viewGroup

viewGroup的關鍵方法

1. getChildMeasureSpec(父容器space,padding,一般是父容器的layoutparam.width或heigh)爲child計算MeasureSpec。該方法爲每個child的每個維度(寬、高)計算正確的MeasureSpec。目標就是把當前viewgroup的MeasureSpec和child的LayoutParams結合起來,生成最合理的結果。

    //主代碼
      case MeasureSpec.EXACTLY:
     if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }

一段通俗易懂的getChildMeasureSpec僞代碼

public static int getChildMeasureSpec(int 限制信息中的模式, int padding, int layoutparam.width或heigh) {
        獲取限制信息中的尺寸和模式。
        switch (限制信息中的模式) {
            case 當前容器的父容器,給當前容器設置了一個精確的尺寸:
                if (子View申請固定的尺寸LayoutParams) {
                    你就用你自己申請的尺寸值就行了;
                } else if (子View希望和父容器一樣大) {
                    你就用父容器的尺寸值就行了;
                } else if (子View希望包裹內容) {
                    你最大尺寸值爲父容器的尺寸值,但是你還是要儘可能小的測量自己的尺寸,包裹你的內容就足夠了;
                } 
                    break;
            case 當前容器的父容器,給當前容器設置了一個最大尺寸:
                if (子View申請固定的尺寸) {
                    你就用你自己申請的尺寸值就行了;
                } else if (子View希望和父容器一樣大) {
                    你最大尺寸值爲父容器的尺寸值,但是你還是要儘可能小的測量自己的尺寸,包裹你的內容就足夠了;
                } else if (子View希望包裹內容) {
                    你最大尺寸值爲父容器的尺寸值,但是你還是要儘可能小的測量自己的尺寸,包裹你的內容就足夠了;
                } 
                    break;
            case 當前容器的父容器,對當前容器的尺寸不限制:
                if (子View申請固定的尺寸) {
                    你就用你自己申請的尺寸值就行了;
                } else if (子View希望和父容器一樣大) {
                    父容器對子View尺寸不做限制。
                } else if (子View希望包裹內容) {
                    父容器對子View尺寸不做限制。
                }
                    break;
        } return 對子View尺寸的限制信息;
    }

這個就是對應的結論圖,前三項是方法參數,後兩個爲計算得到的值.

這裏寫圖片描述

還有這個結論

  • EXACTLY :父容器已經測量出所需要的精確大小,這也是childview的最終大小——match_parent,確定值(不管你是0還是多少).

  • ATMOST : child view最終的大小不能超過父容器的給的——wrap_content .

  • UNSPECIFIED: 不確定,源碼內部使用——-一般在ScrollView,ListView (我們一般用match_parent, wrap_content,還有確定值,這個不用).

2. measureChildren讓所有子view測量自己的尺寸,需要考慮當前ViewGroup的MeasureSpec和Padding。跳過狀態爲gone的子view.

3. measureChild 測量單個view的尺寸.需要考慮當前ViewGroup的MeasureSpec和Padding.

4. measureChildWithMargins 測量單個View,需要考慮當前ViewGroup的MeasureSpec和Padding、margins.

viewGroup的測量流程

measureChildren()–> getChildMeasureSpec()–>child.measure()

    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
  }

綜合流程

一個app啓動之後,view樹的measure過程從根節點DecordView開始,也就是從ViewGroup開始.

一般而言

  1. 首先從viewgroup. 測量自身measure(),然後measureChildren()開始,遵循viewgroup的測量流程,measure()–>measureChild()測量子view–> getChildMeasureSpec()爲child計算測量模式–>child.measure()子view開始測量.

  2. 子view如果是viewgroup,則重複1,如果是view,則遵循view的測量流程child.measure()–>measure()–>onMeasure()–>setMeasuredDimension()保存尺寸.

  3. 遍歷到最後一層,最後一個view.

  4. 把子view的尺寸告訴父佈局,讓父佈局重新測量大小.

在measure過程中,ViewGroup會根據自己當前的狀況,結合子View的尺寸數據,進行一個綜合評定,然後把相關信息告訴子View,然後子View在onMeasure自己的時候,一邊需要考慮到自己的content大小,一邊還要考慮的父佈局的限制信息,然後綜合評定,測量出一個最優的結果。

這裏寫圖片描述

measure實踐

需求

我們做這樣一個view,view需要適配所有layoutParams類型.

思路

1.確定viewgroup的大小,
在onMeasure中,根據MeasuredSpace的不同,分別進行測量.

 switch (widthMode) {

            case MeasureSpec.EXACTLY://本容器爲match_parent或者有精確大小時,容器width大小是測量的大小
                width = widthSize;
                break;
            case MeasureSpec.AT_MOST://本容器爲wrap_content,容器width大小是  最大子view的width大小+pading+margin
                width = getWidth(widthSize, childCount);
                break;
        }

2.確定子view的大小
在layout中,用for讓每一個子view,都向右平移一些像素.

 for (int i = 0; i < childCount; i++) { //依次 定位 每個 子view
            View v = getChildAt(i);
            left = i * OFFSET;
            right = left + v.getMeasuredWidth();
            bottom = top + v.getMeasuredHeight();
            v.layout(left, top, right, bottom);
            top += v.getMeasuredHeight();
        }

代碼

/**
 * Created by chenchangjun on 17/7/18.
 */

public class MyMeasureView extends ViewGroup {


    private static final int OFFSET = 50;

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, 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 heigh = 0;
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility()==GONE){
                continue;
            }
            ViewGroup.LayoutParams layoutParams = child.getLayoutParams();//獲取子view的layoutParams
            int childWithSpace = getChildMeasureSpec(widthMeasureSpec, 0, layoutParams.width); //獲取子view的測量模式(本容器的MeasureSpec,padding,子view的layoutParams中的width)
            int childHeighSpace = getChildMeasureSpec(heightMeasureSpec, 0, layoutParams.height);
            child.measure(childWithSpace, childHeighSpace); //子view進行測量

        }

        switch (widthMode) {

            case MeasureSpec.EXACTLY://本容器爲match_parent或者有精確大小時,容器width大小是測量的大小
                width = widthSize;
                break;
            case MeasureSpec.AT_MOST://本容器爲wrap_content,容器width大小是  最大子view的width大小+pading+margin
                width = getWidth(widthSize, childCount);
                break;
        }
        switch (heightMode) {

            case MeasureSpec.EXACTLY:
                heigh = heightSize;
                break;
            case MeasureSpec.AT_MOST:
                heigh = getHeigh(heightSize, childCount);
                break;
        }
        setMeasuredDimension(width, heigh);

    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int left = 0;
        int right = 0;
        int top = 0;
        int bottom = 0;

        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) { //依次 定位 每個 子view
            View v = getChildAt(i);
            if (v.getVisibility()==GONE){
                continue;
            }
            left = i * OFFSET;
            right = left + v.getMeasuredWidth();
            bottom = top + v.getMeasuredHeight();
            v.layout(left, top, right, bottom);
            top += v.getMeasuredHeight();
        }

    }

    private int getHeigh(int heightSize, int childCount) {
        int heigh;
        heigh = heightSize;
        for (int i = 0; i < childCount; i++) {
            View view = getChildAt(i);
            heigh = heigh + view.getMeasuredHeight();
        }
        return heigh;
    }

    private int getWidth(int widthSize, int childCount) {
        int width;
        width = widthSize;
        for (int i = 0; i < childCount; i++) {
            View view = getChildAt(i);
            if (view.getVisibility()==GONE){
                continue;
            }
            int widthOffset = i * OFFSET + view.getMeasuredHeight();
            width = Math.max(width, widthOffset);  //此處wrap_Content取子view的最大width
        }
        return width;
    }



    public MyMeasureView(Context context) {
        super(context);
    }

    public MyMeasureView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public MyMeasureView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public MyMeasureView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }


}

結果

image.png

參考

http://www.cnblogs.com/nanxiaojue/p/3536381.html

http://www.cnblogs.com/xyhuangjinfu/p/5435201.html

發佈了85 篇原創文章 · 獲贊 87 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章