Android View體系(七)從源碼解析View的measure流程
前言
在上一篇我們了解了Activity的構成后,開始了解一下View的工作流程,就是measure、layout和draw。measure用來測量View的寬高,layout用來確定View的位置,draw則用來繪制View。這一講我們來看看measure流程,measure流程分為View的measure流程和ViewGroup的measure流程,只不過ViewGroup的measure流程除了要完成自己的測量還要遍歷去調用子元素的measure()方法。
1.View的measure流程
先來看看onMeasure()方法(View.java):
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); }
看看setMeasuredDimension()方法:
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { boolean optical = isLayoutModeOptical(this); if (optical != isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets(); int opticalWidth = insets.left + insets.right; int opticalHeight = insets.top + insets.bottom; measuredWidth += optical ? opticalWidth : -opticalWidth; measuredHeight += optical ? opticalHeight : -opticalHeight; } setMeasuredDimensionRaw(measuredWidth, measuredHeight); }
很顯然是用來設置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; }
specMode是View的測量模式,而specSize是View的測量大小,看到這里我們有必要先看看MeasureSpec類:
public static class MeasureSpec { private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT; /** * Measure specification mode: The parent has not imposed any constraint * on the child. It can be whatever size it wants. */ public static final int UNSPECIFIED = 0 << MODE_SHIFT; /** * Measure specification mode: The parent has determined an exact size * for the child. The child is going to be given those bounds regardless * of how big it wants to be. */ public static final int EXACTLY = 1 << MODE_SHIFT; /** * Measure specification mode: The child can be as large as it wants up * to the specified size. */ public static final int AT_MOST = 2 << MODE_SHIFT; ... public static int getMode(int measureSpec) { return (measureSpec & MODE_MASK); } public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } ... }
MeasureSpec類幫助我們來測量View,它是一個32位的int值,高兩位為specMode (測量的模式),低30位為specSize (測量的大小),測量模式分為三種:
- UNSPECIFIED:未指定模式,View想多大就多大,父容器不做限制,一般用于系統內部的測量。
- AT_MOST:最大模式,對應于wrap_comtent屬性,只要尺寸不超過父控件允許的最大尺寸就行。
- EXACTLY:精確模式,對應于match_parent屬性和具體的數值,父容器測量出View所需要的大小,也就是specSize的值。
讓我們回頭看看getDefaultSize()方法,很顯然在AT_MOST和EXACTLY模式下,都返回specSize這個值,也就是View測量后的大小,而在UNSPECIFIED模式返回的是getDefaultSize()方法的第一次個參數的值,這第一個參數從onMeasure()方法來看是getSuggestedMinimumWidth()方法和getSuggestedMinimumHeight()得到的,那我們來看看getSuggestedMinimumWidth()方法做了什么,我們只需要弄懂getSuggestedMinimumWidth()方法,因為這兩個方法原理是一樣的:
protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }
如果View沒有設置背景則取值為mMinWidth,mMinWidth是可以設置的,它對應于android:minWidth這個屬性設置的值或者View的setMinimumWidth的值,如果不指定的話則默認為0:
public void setMinimumWidth(int minWidth) { mMinWidth = minWidth; requestLayout(); }
如果View設置了背景在取值為max(mMinWidth, mBackground.getMinimumWidth()),取值mMinWidth和mBackground.getMinimumWidth()的最大值,上面我們說過了mMinWidth,那來看看mBackground.getMinimumWidth(),這個mBackground是Drawable類型的,看一下Drawable類的getMinimumWidth()方法(Drawable.java):
public int getMinimumWidth() { final int intrinsicWidth = getIntrinsicWidth(); return intrinsicWidth > 0 ? intrinsicWidth : 0; }
intrinsicWidth得到的是這個Drawable的固有的寬度,如果固有寬度大于0則返回固有寬度,否則返回0。
總結一下getSuggestedMinimumWidth()方法就是:如果View沒有設置背景則返回mMinWidth ,如果設置了背景就返回mMinWidth 和Drawable最小寬度兩個值的最大值。
2.ViewGroup的measure流程
ViewGroup的measure原理
講完了View的measure流程,接下來看看ViewGroup的measure流程,對于ViewGroup,它不只要measure自己本身,還要遍歷的調用子元素的measure()方法,ViewGroup中沒有定義onMeasure()方法,但他定義了measureChildren()方法(ViewGroup.java):
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); } } }
就是遍歷子元素并調用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); }
調用child.getLayoutParams()方法來獲得子元素的LayoutParams屬性,并獲取到子元素的MeasureSpec并調用子元素的measure()方法進行測量。getChildMeasureSpec()方法里寫了什么呢?
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 = 0; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = 0; resultMode = MeasureSpec.UNSPECIFIED; } break; } return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
很顯然這是根據父容器的MeasureSpec的模式再結合子元素的LayoutParams屬性來得出子元素的MeasureSpec屬性,有一點需要注意的是如果父容器的MeasureSpec屬性為AT_MOST,子元素的LayoutParams屬性為WRAP_CONTENT,那根據代碼我們會發現子元素的MeasureSpec屬性也為AT_MOST,它的specSize值為父容器的specSize減去padding的值,也就是說跟這個子元素設置LayoutParams屬性為MATCH_PARENT效果是一樣的,為了解決這個問題需要在LayoutParams屬性為WRAP_CONTENT時指定一下默認的寬和高。
LinearLayout的measure流程
ViewGroup并沒有提供onMeasure()方法,而是讓其子類來各自實現測量的方法,究其原因就是ViewGroup有不同的布局的需要很難統一,接下來我們來簡單分析一下ViewGroup的子類LinearLayout的measure流程,先來看看它的onMeasure()方法(LinearLayout.java):
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mOrientation == VERTICAL) { measureVertical(widthMeasureSpec, heightMeasureSpec); } else { measureHorizontal(widthMeasureSpec, heightMeasureSpec); } }
來看看垂直measureVertical()方法的部分源碼:
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { mTotalLength = 0; mTotalLength = 0; ... for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i); if (child == null) { mTotalLength += measureNullChild(i); continue; } if (child.getVisibility() == View.GONE) { i += getChildrenSkipCount(child, i); continue; } if (hasDividerBeforeChildAt(i)) { mTotalLength += mDividerHeight; } LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); totalWeight += lp.weight; if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) { // Optimization: don't bother measuring children who are going to use // leftover space. These views will get measured again down below if // there is any leftover space. final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin); skippedMeasure = true; } else { int oldHeight = Integer.MIN_VALUE; if (lp.height == 0 && lp.weight > 0) { // heightMode is either UNSPECIFIED or AT_MOST, and this // child wanted to stretch to fill available space. // Translate that to WRAP_CONTENT so that it does not end up // with a height of 0 oldHeight = 0; lp.height = LayoutParams.WRAP_CONTENT; } // 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). measureChildBeforeLayout( child, i, widthMeasureSpec, 0, heightMeasureSpec, totalWeight == 0 ? mTotalLength : 0); if (oldHeight != Integer.MIN_VALUE) { lp.height = oldHeight; } final int childHeight = child.getMeasuredHeight(); final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child)); ... if (useLargestChild && (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) { mTotalLength = 0; for (int i = 0; i < count; ++i) { final View child = getVirtualChildAt(i); if (child == null) { mTotalLength += measureNullChild(i); continue; } if (child.getVisibility() == GONE) { i += getChildrenSkipCount(child, i); continue; } final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); // Account for negative margins final int totalLength = mTotalLength; mTotalLength = Math.max(totalLength, totalLength + largestChildHeight + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child)); } } // Add in our padding mTotalLength += mPaddingTop + mPaddingBottom; int heightSize = mTotalLength; // Check against our minimum height
定義了mTotalLength用來存儲LinearLayout在垂直方向的高度,然后遍歷子元素,根據子元素的MeasureSpec模式分別計算每個子元素的高度,如果是wrap_content則將每個子元素的高度和margin垂直高度等值相加并賦值給mTotalLength得出整個LinearLayout的高度。如果布局高度設置為match_parent者具體數值則和View的測量方法一樣。
浙公網安備 33010602011771號