1. 程式人生 > >Android View框架的measure機制

Android View框架的measure機制

bili llc 過程 posit 完整 http vertical 模式 而且

概述

Android中View框架的工作機制中,主要有三個過程:

1、View樹的測量(measure)Android View框架的measure機制

2、View樹的布局(layout) Android View框架的layout機制

3、View樹的繪制(draw)Android View框架的draw機制

View框架的工作流程為:測量每一個View大小(measure)-->把每一個View放置到對應的位置(layout)-->繪制每一個View(draw)。

本文主要講述三大流程中的measure過程。

帶著問題來思考整個measure過程。

1、系統為什麽要有measure過程?

開發者在繪制UI的時候,基本都是通過XML布局文件的方式來配置UI,而每一個View必需要設置的兩個群屬性就是layout_width和layout_height,這兩個屬性代表著當前View的尺寸。

官方文檔截圖:

技術分享

所以這兩個屬性的值是必需要指定的,這兩個屬性的取值僅僅能為三種類型:

1、固定的大小。比方100dp。

2、剛好包裹當中的內容,wrap_content。

3、想要和父布局一樣大,match_parent / fill_parent。

因為Android希望提供一個更優雅的GUI框架,所以提供了自適應的尺寸,也就是 wrap_content 和 match_parent 。

試想一下,那假設這些屬性僅僅同意設置固定的大小,那麽每一個View的尺寸在繪制的時候就已經確定了。所以可能都不須要measure過程。可是因為須要滿足自適應尺寸的機制,所以須要一個measure過程。

2、measure過程都幹了點什麽事?

因為上面提到的自適應尺寸的機制。所以在用自適應尺寸來定義View大小的時候。View的真實尺寸還不能確定。可是View尺寸終於須要映射到屏幕上的像素大小,所以measure過程就是幹這件事。把各種尺寸值,經過計算。得到詳細的像素值。measure過程會遍歷整棵View樹,然後依次測量每一個View真實的尺寸。詳細是每一個ViewGroup會向它內部的每一個子View發送measure命令,然後由詳細子View的onMeasure()來測量自己的尺寸。

最後測量的結果保存在View的mMeasuredWidth和mMeasuredHeight中。保存的數據單位是像素。

3、對於自適應的尺寸機制,怎樣合理的測量一顆View樹?

系統在遍歷完布局文件後,針對布局文件,在內存中生成相應的View樹結構,這個時候,整棵View樹種的全部View對象,都還沒有詳細的尺寸,由於measure過程終於是要確定每一個View打的準確尺寸。也就是準確的像素值。

可是剛開始的時候。View中layout_width和layout_height兩個屬性的值,都僅僅是自適應的尺寸,也就是match_parent和wrap_content,這兩個值在系統中為負數。所以系統不會把它們當成詳細的尺寸值。所以當一個View須要把它內部的match_parent或者wrap_content轉換成詳細的像素值的時候。他須要知道兩個信息。

1、針對於match_parent,父布局當前詳細像素值是多少,由於match_parent就是子View想要和父布局一樣大。

2、針對wrap_content,子View須要依據當前自己內部的content,算出一個合理的能包裹全部內容的最小值。可是假設這個最小值比當前父布局還大,那不行,父布局會告訴你,我僅僅有這麽大,你也不應該超過這個尺寸。

因為樹這樣的數據結構的特殊性,我們在研究measure的過程時,能夠僅僅研究一個ViewGroup和2個View的簡單場景。大概示意圖例如以下:

技術分享

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

4、那麽ViewGroup是怎樣向子View傳遞限制信息的?

談到傳遞限制信息。那就是MeasureSpec類了。該類貫穿於整個measure過程。用來傳遞父布局對子View尺寸測量的約束信息。簡單來說,該類就保存兩類數據。

1、子View當前所在父布局的詳細尺寸。

2、父布局對子View的限制類型。

那麽限制類型又分為三種類型:

1、UNSPECIFIED,不限定。

意思就是,子View想要多大,我就能夠給你多大,你放心大膽的measure吧,不用管其它的。也不用管我傳遞給你的尺寸值。(事實上Android高版本號中推薦,僅僅要是這個模式。尺寸設置為0)

2、EXACTLY,精確的。意思就是。依據我當前的狀況,結合你指定的尺寸參數來考慮。你就應該是這個尺寸,詳細大小在MeasureSpec的尺寸屬性中。自己去查看吧,你也不要管你的content有多大了。就用這個尺寸吧。

3、AT_MOST,最多的。

意思就是,依據我當前的情況,結合你指定的尺寸參數來考慮,在不超過我給你限定的尺寸的前提下。你測量一個恰好能包裹你內容的尺寸就能夠了。

源碼分析

在View的源碼中,提取到了以下一些關於measure過程的信息。

我們知道。整棵View樹的根節點是DecorView,它是一個FrameLayout,所以它是一個ViewGroup。所以整棵View樹的測量是從一個ViewGroup對象的measure方法開始的。

View:

1、measure

/** 開始測量一個View有多大,parent會在參數中提供約束信息。實際的測量工作是在onMeasure()中進行的,該方法會調用onMeasure()方法。所以僅僅有onMeasure能被也必需要被override */
public final void measure(int widthMeasureSpec, int heightMeasureSpec);

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

2、onMeasure


/** 詳細測量過程,測量view和它的內容。來決定測量的寬高(mMeasuredWidth mMeasuredHeight )。

該方法中必需要調用setMeasuredDimension(int, int)來保存該view測量的寬高。 */
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec);

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

3、setMeasuredDimension


/** 保存測量結果 */
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight);

當View測量結束後,把測量結果保存起來,詳細保存在mMeasuredWidth和mMeasuredHeight中。


ViewGroup:

1、measureChildren

/** 讓全部子view測量自己的尺寸。須要考慮當前ViewGroup的MeasureSpec和Padding。

跳過狀態為gone的子view */
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec);-->getChildMeasureSpec()-->child.measure();

測量全部的子View尺寸,把measure過程交到子View內部。

2、measureChild

/** 測量單個View。須要考慮當前ViewGroup的MeasureSpec和Padding。 */
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec);-->getChildMeasureSpec()-->child.measure();

對每個詳細的子View進行測量。

3、measureChildWithMargins

/** 測量單個View,須要考慮當前ViewGroup的MeasureSpec和Padding、margins。 */
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed);-->getChildMeasureSpec()-->child.measure();

對每個詳細的子View進行測量。可是須要考慮到margin等信息。

4、getChildMeasureSpec


/** measureChildren過程中最困難的一部分,為child計算MeasureSpec。該方法為每一個child的每一個維度(寬、高)計算正確的MeasureSpec。目標就是把當前viewgroup的MeasureSpec和child的LayoutParams結合起來。生成最合理的結果。


比方,當前ViewGroup知道自己的準確大小。由於MeasureSpec的mode為EXACTLY,而child希望可以match_parent。這時就會為child生成一個mode為EXACTLY。大小為ViewGroup大小的MeasureSpec。


*/
public static int getChildMeasureSpec(int spec, int padding, int childDimension);

依據當前自身的狀況。以及特定子View的尺寸參數,為特定子View計算一個合理的限制信息。

源碼:

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        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;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } 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;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }


偽代碼:

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        獲取限制信息中的尺寸和模式。
        switch (限制信息中的模式) {
            case 當前容器的父容器。給當前容器設置了一個精確的尺寸:
                if (子View申請固定的尺寸) {
                    你就用你自己申請的尺寸值即可了;
                } 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尺寸的限制信息;
    }


當自己定義View的時候,也須要處理measure過程。主要有兩種情況。

1、繼承自View的子類。

須要覆寫onMeasure來正確測量自己。最後都須要調用setMeasuredDimension來保存測量結果

一般來說。自己定義View的measure過程偽代碼為:

int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);

int viewSize = 0;

swith (mode) {
	case MeasureSpec.EXACTLY:
		viewSize = size; //當前View尺寸設置為父布局尺寸
		break;
	case MeasureSpec.AT_MOST:
		viewSize = Math.min(size, getContentSize()); //當前View尺寸為內容尺寸和父布局尺寸其中的最小值
		break;
	case MeasureSpec.UNSPECIFIED:
		viewSize = getContentSize(); //內容有多大。就設置尺寸為多大
		break;
	default:
		break;
}

setMeasuredDimension(viewSize);

2、繼承自ViewGroup的子類。

不但須要覆寫onMeasure來正確測量自己。可能還要覆寫一系列measureChild方法,來正確的測量子view。比方ScrollView。或者幹脆放棄父類實現的measureChild規則,自己又一次實現一套測量子view的規則,比方RelativeLayout。最後都須要調用setMeasuredDimension來保存測量結果。

一般來說,自己定義ViewGroup的measure過程的偽代碼為:

//ViewGroup開始測量自己的尺寸
viewGroup.onMeasure();
//ViewGroup為每一個child計算測量限制信息(MeasureSpec)
viewGroup.getChildMeasureSpec();
//把上一步生成的限制信息。傳遞給每一個子View,然後子View開始measure自己的尺寸
child.measure();
//子View測量完畢後,ViewGroup就能夠獲取每一個子View測量後的尺寸
child.getChildMeasuredSize();
//ViewGroup依據自己自身狀況,比方Padding等,計算自己的尺寸
viewGroup.calculateSelfSize();
//ViewGroup保存自己的尺寸
viewGroupsetMeasuredDimension();


案例分析

非常多開發者都遇到過這樣的需求,就是ScrollView內部嵌套ListView,而該ListView數據條數是不確定的。所以須要設置為包裹內容,然後就會發現ListView就會顯示第一行出來。然後就會百度到一條解決方式,繼承ListView。覆寫onMeasure方法。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }


問題是攻克了。可是非常多開發者並不知道為什麽。

以下會從ScrollView和ListView的measure過程來分析一下。

1、為什麽會出現上述問題?

備註:截取部分問題相關代碼,並非完整代碼。

看看ListView的onMeasure:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final View child = obtainView(0, mIsScrap);
        childHeight = child.getMeasuredHeight();
        if (heightMode == MeasureSpec.UNSPECIFIED) {
            heightSize = mListPadding.top + mListPadding.bottom + childHeight + getVerticalFadingEdgeLength() * 2;
            if (heightMode == MeasureSpec.AT_MOST) {
                // TODO: after first layout we should maybe start at the first visible position, not 0 
                heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
            }
            setMeasuredDimension(widthSize, heightSize);
            mWidthMeasureSpec = widthMeasureSpec;
        }
    }

當MeasureSpec mode為UNSPECIFIED的時候,僅僅測量第一個item打的高度,跟問題描寫敘述相符。所以我們推測可能是由於ScrollView傳遞了一個UNSPECIFIED限制給ListView。

再來看ScrollView的onMeasure代碼:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

調用了父類的onMeasure:

看看FrameLayout的onMeasure:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            }
        }
    }

調用了measureChildWithMargins。可是由於ScrollView覆寫了該方法,所以看看ScrollView的measureChildWithMargins方法:

    @Override
    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, 
                                           int parentHeightMeasureSpec, int heightUsed) {
        final int childHeightMeasureSpec = 
                MeasureSpec.makeSafeMeasureSpec(MeasureSpec.getSize(parentHeightMeasureSpec), 
                        MeasureSpec.UNSPECIFIED);
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }



果然,它向ListView的onMeasure傳遞了一個UNSPECIFIED的限制。

為什麽呢,想想,由於ScrollView。本來就是能夠在豎直方向滾動的布局,所以。它對它全部的子View的高度就是UNSPECIFIED,意思就是,不限制子View有多高,由於我本來就是須要豎直滑動的,它的本意就是如此,所以它對子View高度不做不論什麽限制。


2、為什麽這樣的解決方法能夠解決問題?

看看ListView的onMeasure:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final View child = obtainView(0, mIsScrap);
        childHeight = child.getMeasuredHeight();
        if (heightMode == MeasureSpec.UNSPECIFIED) {
            heightSize = mListPadding.top + mListPadding.bottom + childHeight + getVerticalFadingEdgeLength() * 2;
            if (heightMode == MeasureSpec.AT_MOST) {
                // TODO: after first layout we should maybe start at the first visible position, not 0 
                heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
            }
            setMeasuredDimension(widthSize, heightSize);
            mWidthMeasureSpec = widthMeasureSpec;
        }
    }

僅僅要讓heightMode == MeasureSpec.AT_MOST,它就會測量它的完整高度,所以第一個數據,限制mode的值就確定下來了。

第二個數據就是尺寸上限,假設給個200。那麽當ListView數據過多的時候。該ListView最大高度就是200了,還是不能全然顯示內容,怎麽辦?那麽就給個最大值吧,最大值是多少呢,Integer.MAX_VALUE?

先看一下MeasureSpec的代碼說明:

        private static final int MODE_SHIFT = 30;
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
        public static final int EXACTLY = 1 << MODE_SHIFT;
        public static final int AT_MOST = 2 << MODE_SHIFT;

他用最高兩位存儲mode,用其它剩余未存儲size。

所以Integer.MAX_VALUE >> 2,就是限制信息所能攜帶的最大尺寸數據。所以最後就須要用這兩個值做成一個限制信息。傳遞給ListView的height維度。

也就是例如以下代碼:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }



自己動手

以下我們自己寫一個自己定義的ViewGroup,讓它內部的每個子View都垂直排布。而且讓每個子View的左邊界都距離上一個子View的左邊界一定的距離。

而且支持wrap_content。大概看起來例如以下圖所看到的:

技術分享


實際執行效果例如以下圖所看到的:
技術分享

代碼例如以下:

public class VerticalOffsetLayout extends ViewGroup {

    private static final int OFFSET = 100;

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

    public VerticalOffsetLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public VerticalOffsetLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @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 height = 0;

        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            ViewGroup.LayoutParams lp = child.getLayoutParams();
            int childWidthSpec = getChildMeasureSpec(widthMeasureSpec, 0, lp.width);
            int childHeightSpec = getChildMeasureSpec(heightMeasureSpec, 0, lp.height);
            child.measure(childWidthSpec, childHeightSpec);
        }

        switch (widthMode) {
            case MeasureSpec.EXACTLY:
                width = widthSize;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.UNSPECIFIED:
                for (int i = 0; i < childCount; i++) {
                    View child = getChildAt(i);
                    int widthAddOffset = i * OFFSET + child.getMeasuredWidth();
                    width = Math.max(width, widthAddOffset);
                }
                break;
            default:
                break;

        }

        switch (heightMode) {
            case MeasureSpec.EXACTLY:
                height = heightSize;
                break;
            case MeasureSpec.AT_MOST:
            case MeasureSpec.UNSPECIFIED:
                for (int i = 0; i < childCount; i++) {
                    View child = getChildAt(i);
                    height = height + child.getMeasuredHeight();
                }
                break;
            default:
                break;

        }

        setMeasuredDimension(width, height);
    }

    @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 child = getChildAt(i);
            left = i * OFFSET;
            right = left + child.getMeasuredWidth();
            bottom = top + child.getMeasuredHeight();

            child.layout(left, top, right, bottom);

            top += child.getMeasuredHeight();
        }
    }
}


Android View框架的measure機制