以下是我個人對at_most、unspecified、exactly模的個人理解,是首先給出這幾個字段的簡短描述,這幾個字段是測量View的時候用到的,這些字段用來描述父view傳給子view的高度或者寬度的意義
就是告訴子view這些高度和寬度你該如何參考,首先,得知道這個模式,是父view向子view傳遞的。
at_most: 這個子view 最大不能超過這個值。
unspecified:大小不確定,還得你自己用尺子給自己量一下,父view盡量給你所需的長度。
exactly:這個 子view 你就是這么大了。
正常人都知道一個基本view的繪制基本需要3個過程:measure -> layout -> draw
參考父view參考的長寬來具體測量(或者確認)自己的長寬以及子view長寬 -> 確定自己在父view上的坐標位置以及子view在自己區域的坐標位置 -> 繪制到屏幕上
我們來看view的測量階段,單純看一下view的onMeasure 方法,它有兩個參數:widthMeasureSpec、heightMeasureSpec,這兩個參數是父view給子View參考用的。
這個值肯定不是亂給的,這個包含了長度(高寬)與模式(at_most、unspecified、exactly)
當前 view 測量完成之后,需要調用 setMeasuredDimension(width,height) 設置,相信大家都是了解這個過程的。我注意看了一下 View.java 中對於這個方法調用是這樣的:
setMeasuredDimsion(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec))
於是我查看了 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; }
從getDefaultSize 的源碼中我們可以得出這些個結論,默認View的測量實現中,除了unspecified模式之外,其他模式都是用了父 view 傳遞過來的長度,換句話說,系統或者源碼編寫者至少是這么用的。
我這有個問題,父view為啥要給子view傳遞一個參考長度呢? 我自己測量自己不行嗎?對於這個問題,我舉個例子:
同志們都知道,我們平時用的控件,比如TextView,在xml的布局中必然有一個父布局,必須有個布局容器來裝它。這里有兩個屬性android:width,android:height 是必須寫的,否則就會出錯。這里設定的值
就是父view穿給TextView的參考值。現在想一下代碼中動態創建一個TextView的過程的代碼
LinearLayout ll = (LinearLayout)findViewById(R.id.ll);
TextView text_view = new TextView(this);
ll.addView(text_view);
text_view = new TextView(this);
text_view.setText("計划好了再娶吧");
text_view.setBackgroundColor(Color.RED);
ll.addView(text_view);
如果你實驗過了,我為什么要說動態添加view,因為我想說一下這個LayoutParams 參數,我們可以主動為我們的text_view 設置,也可以使用默認的,所謂默認就是你在向父布局添加的過程中(addView過程)
父布局發現你沒有設置LayoutParams,會主動為你設置一個layoutParams。對LinearLayout來講如果是VERTICAL模式,那么該布局默認為子view生成的LayoutParams 的寬度就是match_parent
不管是默認還是自己創建設置,最終父布局會拿到LayoutParams里面的各種參數,其中就包括了對子view的寬高的參考值。
這里我們拿LinearLayout的測量,在LinearLayout 也會參考它的父布局所給的參考。因為本身是個布局容器,它會依次測量它的每一個孩子,大概會調用這個方法
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed)
方法的源碼如下:
/** * Ask one of the children of this view to measure itself, taking into * account both the MeasureSpec requirements for this view and its padding * and margins. The child must have MarginLayoutParams The heavy lifting is * done in getChildMeasureSpec. * * @param child The child to measure * @param parentWidthMeasureSpec The width requirements for this view * @param widthUsed Extra space that has been used up by the parent * horizontally (possibly by other children of the parent) * @param parentHeightMeasureSpec The height requirements for this view * @param heightUsed Extra space that has been used up by the parent * vertically (possibly by other children of the parent) */
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 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); }
通過源碼我們可以看出這個方法所干的事情:
1、得到子view的 MarginLayoutParams
2、確定子view的長寬的參考值getChildMeasureSpec(...)
3、調用子view的measure 並傳入參考值,進行遞歸測量child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
看了一下getChildMeasureSpec(..)的源碼,稍微有點長,但是我們必須知道子view的參考值是怎么來的,雖然知道是android:width 或 android:height 中設置,但怎么轉換的我們需要看一下
/** * Does the hard part of measureChildren: figuring out the MeasureSpec to * pass to a particular child. This method figures out the right MeasureSpec * for one dimension (height or width) of one child view. * * The goal is to combine information from our MeasureSpec with the * LayoutParams of the child to get the best possible results. For example, * if the this view knows its size (because its MeasureSpec has a mode of * EXACTLY), and the child has indicated in its LayoutParams that it wants * to be the same size as the parent, the parent should ask the child to * layout given an exact size. * * @param spec The requirements for this view * @param padding The padding of this view for the current dimension and * margins, if applicable * @param childDimension How big the child wants to be in the current * dimension * @return a MeasureSpec integer for the child */
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; } //noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
注釋寫的很明白:這是測量子view的關鍵,目的就是為了得到要給這個子view傳遞MeasurepSpec的大小(測量尺寸 + 規格), 是通過我們的MeasureSpec 和 LayoutParams參數來得到的。
注意:這里的this view指的是不是測量的子view 而是當前view
方法簽名:public static int getChildMeasureSpec(int spec, int padding, int childDimension)
spec: 父容器提供當前View的長度(尺寸+規格)。
padding: 該view 的padding 和 被其他子view占據的部分等。
childDimension: 我們通過LayoutParams 設定的值(match_parent, wrap_content, 固定值) 這里僅僅是尺寸。
這樣我們就有了兩個參考點:我的MeasureSpec,子view的LayoutParams。
計算剩余空間: int size = Math.max(0, specSize - padding)
大概就是通過父控件的specMode(父控件的父控件傳過來的)和LayoutParams 里面的參數,來確定具體給這個子view如何傳參考值,測量完之后就得到了(mode + size)。在測量完成每一個子view的測量高度之后,
父容器根據自身的padding和相應的子view的寬高之和得到自身的測量結果。
先寫這么多,也不知道理解的對不對,請多指教。
有一個問題,為甚對於一個TextView,我們調用TextView.measure(0, 0) 會知道TextView長寬。
當調用measure就表示自身測量(TextView的自身測量很復雜),但是一定會參考一下父控件傳過來的值,並且只有mode是exactly 或者 at_most的時候回參考。否則就是自己的測量結果,因為傳遞了0那么,得到的mode就是 unspecified的,因此就是測量自身的大小。也就是最小
寬度和高度。