Android View繪製原始碼分析 Measure

liuxuhui發表於2021-09-09

概述

在Android中所有佈局或者控制元件都直接或間接繼承至View,因此它們擁有相同的繪製機制。以前的學習經驗知道View在繪製過程中會經過measure、layout、draw三個流程,measure負責測量View的寬高,layout負責確定View在父容器中的位置,draw負責將View繪製到螢幕上。上篇文章分析到DecorView的繪製從ViewRootImpl類中的performTraversals開始,這篇主要分析View繪製流程中的measure部分。

1234567891011121314151617
原始碼路徑:SDK/sources/android-24/android/view/ViewRootImpl.javaprivate void performTraversals() {// cache mView since it is used so much below...final View host = mView;......WindowManager.LayoutParams lp = mWindowAttributes;......int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);// Ask host how big it wants to beperformMeasure(childWidthMeasureSpec, childHeightMeasureSpec);......performLayout(lp, mWidth, mHeight);......performDraw();......}

檢視這裡的getRootMeasureSpec方法

123456789101112131415161718192021222324252627282930313233
原始碼路徑:SDK/sources/android-24/android/view/ViewRootImpl .java/*** Figures out the measure spec for the root view in a window based on it's* layout params.** @param windowSize*            The available width or height of the window** @param rootDimension*            The layout params for one dimension (width or height) of the*            window.** @return The measure spec to use to measure the root view.*/private static int getRootMeasureSpec(int windowSize, int rootDimension) {int measureSpec;switch (rootDimension) {case ViewGroup.LayoutParams.MATCH_PARENT:// Window can't resize. Force root view to be windowSize.measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);break;case ViewGroup.LayoutParams.WRAP_CONTENT:// Window can resize. Set max size for root view.measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);break;default:// Window wants to be an exact size. Force root view to be that size.measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);break;}return measureSpec;}

根據註釋可知,該方法作用是根據根檢視的佈局引數推算出的其測量規格,這裡的第二個引數傳的是DecorView的LayoutParam對應的寬高,由之前的文章分析可知DecorView在生成佈局時指定的寬高均為MATCH_PARENT,滿足第一個分支,透過MeasureSpec的makeMeasureSpec方法生成測量規格並返回,檢視該方法:

123456789101112131415161718192021222324252627282930
原始碼路徑:SDK/sources/android-24/android/view/View.java/*** Creates a measure specification based on the supplied size and mode.** The mode must always be one of the following:* 
    *  
  • {@link android.view.View.MeasureSpec#UNSPECIFIED}
  • *  
  • {@link android.view.View.MeasureSpec#EXACTLY}
  • *  
  • {@link android.view.View.MeasureSpec#AT_MOST}
** 

Note: On API level 17 and lower, makeMeasureSpec's* implementation was such that the order of arguments did not matter* and overflow in either value could impact the resulting MeasureSpec.* {@link android.widget.RelativeLayout} was affected by this bug.* Apps targeting API levels greater than 17 will get the fixed, more strict* behavior.

** @param size the size of the measure specification* @param mode the mode of the measure specification* @return the measure specification based on size and mode*/public static int makeMeasureSpec(@IntRange(from = 0, to = (1 

根據註釋可知,該方法將傳入的大小和模式生成對應的測量規格,這裡多說一點MeasureSpec,它是一個由模式和大小組合的32位int整型值,高二位代表mode,低30位代表size,計算中使用了位運算來提高並最佳化效率,MeasureSpec有三種模式

123456789101112131415161718192021
/*** Measure specification mode: The parent has not imposed any constraint* on the child. It can be whatever size it wants.*///不確定模式:父佈局對子佈局沒有限制,子佈局想要多大就多大,0左移30位public static final int UNSPECIFIED = 0 

回到上面,在獲取到DecorView寬高的測量規格後,呼叫performMeasure()方法:

12345678
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");try {mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);} finally {Trace.traceEnd(Trace.TRACE_TAG_VIEW);}}

根據傳入的寬高測量規格呼叫mView的measure方法,這裡的mView就是DecorView,DecorView extends FrameLayout extends ViewGroup extends View,DecorView 、FrameLayout以及ViewGroup都沒有重寫measure方法,因此在View中檢視measure方法

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
原始碼路徑:SDK/sources/android-24/android/view/View.java/*** 

* This is called to find out how big a view should be. The parent* supplies constraint information in the width and height parameters.* 

** 

* The actual measurement work of a view is performed in* {@link #onMeasure(int, int)}, called by this method. Therefore, only* {@link #onMeasure(int, int)} can and must be overridden by subclasses.* 

*** @param widthMeasureSpec Horizontal space requirements as imposed by the*        parent* @param heightMeasureSpec Vertical space requirements as imposed by the*        parent** @see #onMeasure(int, int)*/public final void measure(int widthMeasureSpec, int heightMeasureSpec) {boolean optical = isLayoutModeOptical(this);if (optical != isLayoutModeOptical(mParent)) {Insets insets = getOpticalInsets();int oWidth  = insets.left + insets.right;int oHeight = insets.top  + insets.bottom;widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);}// Suppress sign extension for the low byteslong key = (long) widthMeasureSpec > 32), (int) value);mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;}// flag not set, setMeasuredDimension() was not invoked, we raise// an exception to warn the developerif ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {throw new IllegalStateException("View with id " + getId() + ": "+ getClass().getName() + "#onMeasure() did not set the"+ " measured dimension by calling"+ " setMeasuredDimension()");}mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;}//儲存本次View的測量規格用於下次判斷mOldWidthMeasureSpec = widthMeasureSpec;mOldHeightMeasureSpec = heightMeasureSpec;mMeasureCache.put(key, ((long) mMeasuredWidth) 

從註釋可知,該方法是查明一個View應該有多大,父佈局可以在寬高引數中提供約束資訊,實際的測量工作是在這個方法的onMeasure中完成的,因此,子類只能重寫onMeasure方法。檢視View的onMeasure方法:

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
/*** 

* 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.* 

** 

CONTRACT: When overriding this method, you* must call {@link #setMeasuredDimension(int, int)} to store the* measured width and height of this view. Failure to do so will trigger an* IllegalStateException, thrown by* {@link #measure(int, int)}. Calling the superclass'* {@link #onMeasure(int, int)} is a valid use.* 

** 

* The base class implementation of measure defaults to the background size,* unless a larger size is allowed by the MeasureSpec. Subclasses should* override {@link #onMeasure(int, int)} to provide better measurements of* their content.* 

** 

* If this method is overridden, it is the subclass's responsibility to make* sure the measured height and width are at least the view's minimum height* and width ({@link #getSuggestedMinimumHeight()} and* {@link #getSuggestedMinimumWidth()}).* 

** @param widthMeasureSpec horizontal space requirements as imposed by the parent.*                         The requirements are encoded with*                         {@link android.view.View.MeasureSpec}.* @param heightMeasureSpec vertical space requirements as imposed by the parent.*                         The requirements are encoded with*                         {@link android.view.View.MeasureSpec}.** @see #getMeasuredWidth()* @see #getMeasuredHeight()* @see #setMeasuredDimension(int, int)* @see #getSuggestedMinimumHeight()* @see #getSuggestedMinimumWidth()* @see android.view.View.MeasureSpec#getMode(int)* @see android.view.View.MeasureSpec#getSize(int)*/protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}

註釋裡有一條很重要的介紹,setMeasuredDimension方法必須在onMeasure方法中呼叫,不然會拋異常。可以看到View的onMeasure內部只是呼叫了setMeasuredDimension方法,引數一個套一個,先看下setMeasuredDimension

123456789101112131415161718192021222324
/*** 

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.

** @param measuredWidth The measured width of this view.  May be a complex* bit mask as defined by {@link #MEASURED_SIZE_MASK} and* {@link #MEASURED_STATE_TOO_SMALL}.* @param measuredHeight The measured height of this view.  May be a complex* bit mask as defined by {@link #MEASURED_SIZE_MASK} and* {@link #MEASURED_STATE_TOO_SMALL}.*/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);}

這裡的setMeasuredDimension將傳入的寬高值轉手又傳給了setMeasuredDimensionRaw

123456789101112131415161718
/*** Sets the measured dimension without extra processing for things like optical bounds.* Useful for reapplying consistent values that have already been cooked with adjustments* for optical bounds, etc. such as those from the measurement cache.** @param measuredWidth The measured width of this view.  May be a complex* bit mask as defined by {@link #MEASURED_SIZE_MASK} and* {@link #MEASURED_STATE_TOO_SMALL}.* @param measuredHeight The measured height of this view.  May be a complex* bit mask as defined by {@link #MEASURED_SIZE_MASK} and* {@link #MEASURED_STATE_TOO_SMALL}.*/private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {mMeasuredWidth = measuredWidth;mMeasuredHeight = measuredHeight;mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;}

在這個方法中只是將傳入的寬高值對View的系統變數賦值,測量流程就走完了。返回看下之前巢狀的裡的getDefaultSize方法:

12345678910111213141516171819202122232425
/*** 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;}

從註釋可知,該方法的作用是根據View佈局設定的寬高和父佈局傳遞的測量規格計算View的寬高,第二個引數是父佈局的測量規格,因此子View最終大小是由自身佈局大小和父佈局的測量規格共同決定的。從上面的程式碼中可以看到,預設情況下,如果父佈局指定的測量規格是MeasureSpec.AT_MOST或者MeasureSpec.EXACTLY,那麼返回的測量大小是一樣的,都是specSize;如果父佈局指定的測量規格是MeasureSpec.UNSPECIFIED,那麼返回的大小為size,即第一個引數值。specSize透過MeasureSpec.getSize獲得,大小由父佈局指定;size透過getSuggestedMinimumWidth方法獲得:

1234567891011121314
/*** Returns the suggested minimum width that the view should use. This* returns the maximum of the view's minimum width* and the background's minimum width*  ({@link android.graphics.drawable.Drawable#getMinimumWidth()}).* 

* When being used in {@link #onMeasure(int, int)}, the caller should still* ensure the returned width is within the requirements of the parent.** @return The suggested minimum width of the view.*/protected int getSuggestedMinimumWidth() {return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());}

從註釋可知,該方法返回View建議的最小寬高,也就是xml中設定的android:minWidth和 android:minHeight的值,建議的最小寬高是由View的Background尺寸與透過設定View的minWidth/minHeigh屬性共同決定的,如果該View設定了Background,就返回minXXX與Background最小寬高中最大的那個。

DecorView 、FrameLayout以及View中都有onMeasure方法,在View的measure方法中執行onMeasure時,會先執行DecorView 中onMeasure方法,具體如下:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
原始碼路徑:/SDK/sources/android-24/com/android/internal/policy/DecorView.java@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();//是否豎屏final boolean isPortrait =getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;//獲取寬高Mode,這裡獲取的widthMode和heightMode都是MeasureSpec.EXACTLYfinal int widthMode = getMode(widthMeasureSpec);final int heightMode = getMode(heightMeasureSpec);boolean fixedWidth = false;mApplyFloatingHorizontalInsets = false;//widthMode是MeasureSpec.EXACTLY所以不走這if (widthMode == AT_MOST) {final TypedValue tvw = isPortrait ? mWindow.mFixedWidthMinor : mWindow.mFixedWidthMajor;if (tvw != null && tvw.type != TypedValue.TYPE_NULL) {final int w;if (tvw.type == TypedValue.TYPE_DIMENSION) {w = (int) tvw.getDimension(metrics);} else if (tvw.type == TypedValue.TYPE_FRACTION) {w = (int) tvw.getFraction(metrics.widthPixels, metrics.widthPixels);} else {w = 0;}if (DEBUG_MEASURE) Log.d(mLogTag, "Fixed width: " + w);final int widthSize = MeasureSpec.getSize(widthMeasureSpec);if (w > 0) {widthMeasureSpec = MeasureSpec.makeMeasureSpec(Math.min(w, widthSize), EXACTLY);fixedWidth = true;} else {widthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize - mFloatingInsets.left - mFloatingInsets.right,AT_MOST);mApplyFloatingHorizontalInsets = true;}}}mApplyFloatingVerticalInsets = false;//heightMode是MeasureSpec.EXACTLY所以不走這if (heightMode == AT_MOST) {final TypedValue tvh = isPortrait ? mWindow.mFixedHeightMajor: mWindow.mFixedHeightMinor;if (tvh != null && tvh.type != TypedValue.TYPE_NULL) {final int h;if (tvh.type == TypedValue.TYPE_DIMENSION) {h = (int) tvh.getDimension(metrics);} else if (tvh.type == TypedValue.TYPE_FRACTION) {h = (int) tvh.getFraction(metrics.heightPixels, metrics.heightPixels);} else {h = 0;}if (DEBUG_MEASURE) Log.d(mLogTag, "Fixed height: " + h);final int heightSize = MeasureSpec.getSize(heightMeasureSpec);if (h > 0) {heightMeasureSpec = MeasureSpec.makeMeasureSpec(Math.min(h, heightSize), EXACTLY);} else if ((mWindow.getAttributes().flags & FLAG_LAYOUT_IN_SCREEN) == 0) {heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize - mFloatingInsets.top - mFloatingInsets.bottom, AT_MOST);mApplyFloatingVerticalInsets = true;}}}//獲取開端getOutsets(mOutsets);if (mOutsets.top > 0 || mOutsets.bottom > 0) {int mode = MeasureSpec.getMode(heightMeasureSpec);if (mode != MeasureSpec.UNSPECIFIED) {int height = MeasureSpec.getSize(heightMeasureSpec);//重新計算高度測量規格heightMeasureSpec = MeasureSpec.makeMeasureSpec(height + mOutsets.top + mOutsets.bottom, mode);}}if (mOutsets.left > 0 || mOutsets.right > 0) {int mode = MeasureSpec.getMode(widthMeasureSpec);if (mode != MeasureSpec.UNSPECIFIED) {int width = MeasureSpec.getSize(widthMeasureSpec);//重新計算寬度測量規格widthMeasureSpec = MeasureSpec.makeMeasureSpec(width + mOutsets.left + mOutsets.right, mode);}}//呼叫父類的onMeasure方法,也就是FrameLayout的onMeasuresuper.onMeasure(widthMeasureSpec, heightMeasureSpec);int width = getMeasuredWidth();boolean measure = false;widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, EXACTLY);//widthMode是MeasureSpec.EXACTLY所以不走這if (!fixedWidth && widthMode == AT_MOST) {final TypedValue tv = isPortrait ? mWindow.mMinWidthMinor : mWindow.mMinWidthMajor;if (tv.type != TypedValue.TYPE_NULL) {final int min;if (tv.type == TypedValue.TYPE_DIMENSION) {min = (int)tv.getDimension(metrics);} else if (tv.type == TypedValue.TYPE_FRACTION) {min = (int)tv.getFraction(mAvailableWidth, mAvailableWidth);} else {min = 0;}if (DEBUG_MEASURE) Log.d(mLogTag, "Adjust for min width: " + min + ", value::"+ tv.coerceToString() + ", mAvailableWidth=" + mAvailableWidth);if (width 

可以看到,由於DecorView的LayoutParams為MATCH_PARENT,所以在getRootMeasureSpec裡返回的測量規格是MeasureSpec.EXACTLY,所以這裡的onMeasure並沒有滿足AT_MOST判斷邏輯,看一下68行getOutsets方法,其實現是在View.java中

1234567891011121314151617181920
原始碼路徑:SDK/sources/android-24/android/view/View.java/*** Returns the outsets, which areas of the device that aren't a surface, but we would like to* treat them as such.* @hide*/public void getOutsets(Rect outOutsetRect) {if (mAttachInfo != null) {outOutsetRect.set(mAttachInfo.mOutsets);} else {outOutsetRect.setEmpty();}}原始碼路徑:SDK/sources/android-24/android/graphics/Rect.java/*** Set the rectangle to (0,0,0,0)*/public void setEmpty() {left = right = top = bottom = 0;}

因此DecorView的onMeasure中判斷如果left 、right 、top、bottom、大於0時,則加上這些值重新計算寬高測量規格,呼叫父類的onMeasure方法,檢視FrameLayout的onMeasure

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {//獲取子View個數int count = getChildCount();//widthMeasureSpec和heightMeasureSpec都是MeasureSpec.EXACTLY,所以這裡為falsefinal boolean measureMatchParentChildren =MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;mMatchParentChildren.clear();int maxHeight = 0;int maxWidth = 0;int childState = 0;for (int i = 0; i  1) {for (int i = 0; i 

該方法主要做了這幾件事

  • 遍歷子View,傳入自身的測量規格,呼叫measureChildWithMargins測量除了Visibility屬性設為GONE的子View的寬高

  • 獲取子View中最大的那個寬高,計算寬高值包括是否設定margin、padding、foreground,以及和建議的最小寬高比較

  • 呼叫setMeasuredDimension方法,將最大的子View的寬高值作為自己的寬高值
    看下這裡的measureChildWithMargins方法:

1234567891011121314151617
/** * 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) { //獲取子View的LayoutParams    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();    //結合父佈局的測量規格和padding,子View的margin和父佈局已使用寬度大小widthUsed(前面設定為0),以及子View自身寬度來獲取子View寬度測量規格final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin                    + widthUsed, lp.width);//結合父佈局的測量規格和padding,子View的margin和父佈局已使用高度大小heightUsed(前面設定為0),以及子View自身高度來獲取子View高度測量規格final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin                    + heightUsed, lp.height);//呼叫子View的measure方法,如果子View是ViewGroup則遞迴往下測量    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}

該方法對父檢視提供的measureSpec引數結合子View的LayoutParams引數,計算出子View的測量規格,再將測量規格傳遞給子View,最後由子View的measure方法完成測量,看下getChildMeasureSpec方法:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
/*** 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);//父佈局的大小-父佈局的Padding-子佈局的Margin,得到值才是子佈局的大小。int size = Math.max(0, specSize - padding);//初始化子佈局Mode和Size,最後根據這兩個值生成子佈局的測量規格int resultSize = 0;int resultMode = 0;switch (specMode) {// Parent has imposed an exact size on uscase 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 uscase MeasureSpec.AT_MOST:if (childDimension >= 0) {// Child wants a specific size... so be itresultSize = 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 becase MeasureSpec.UNSPECIFIED:if (childDimension >= 0) {// Child wants a specific size... let him have itresultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {// Child wants to be our size... find out how big it should// beresultSize = 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 beresultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;resultMode = MeasureSpec.UNSPECIFIED;}break;}//noinspection ResourceType//生成子佈局的測量規格return MeasureSpec.makeMeasureSpec(resultSize, resultMode);}

由前面分析DecorView的specMode為MeasureSpec.EXACTLY,因此走第一個分支,這裡分三種情況:

  • 如果子佈局的width或height是一個精確的值,則測量大小就是這個精確值,測量模式為EXACTLY;

  • 如果子佈局的width或height是MATCH_PARENT,則測量大小是父佈局-父佈局的Padding-子佈局的Margin,測量模式為EXACTLY

  • 如果子佈局的width或height是WRAP_CONTENT,則測量大小是父佈局-父佈局的Padding-子佈局的Margin,測量模式為AT_MOST
    最後再根據這裡的測量大小和測量模式生成子佈局的測量規格並返回,子佈局根據計算得來的測量規格呼叫measure方法進行測量,如果子佈局是ViewGroup,則遞迴往下測量,measureChildWithMargins方法執行完後,回到FrameLayout的onMeasure,在61行呼叫setMeasuredDimension方法,根據佈局中最大的子View設定自己的寬高,測量完成。

MainActivity.java

123456789101112131415161718
public class MainActivity extends Activity {private InitView initView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);initView = (InitView) findViewById(R.id.initView);Log.d("hcy", "onCreate: width is " + initView.getWidth());Log.d("hcy", "onCreate: measuredWidth is " + initView.getMeasuredWidth());}@Overrideprotected void onResume() {super.onResume();Log.d("hcy", "onResume: width is " + initView.getWidth());Log.d("hcy", "onResume: measuredWidth is " + initView.getMeasuredWidth());}}

自定義View

12345678910111213141516171819202122232425262728293031323334353637383940414243444546
public class InitView extends View {private Paint mPaint;public InitView(Context context){this(context, null);}public InitView(Context context, AttributeSet attrs) {this(context, attrs, 0);}public InitView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init();}private void init() {mPaint = new Paint();mPaint.setColor(Color.BLACK);mPaint.setAntiAlias(true);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {int specMode = MeasureSpec.getMode(widthMeasureSpec);switch (specMode) {case MeasureSpec.UNSPECIFIED:Log.d("hcy", "onMeasure: UNSPECIFIED");break;case MeasureSpec.EXACTLY:Log.d("hcy", "onMeasure: EXACTLY");break;case MeasureSpec.AT_MOST:Log.d("hcy", "onMeasure: AT_MOST");break;default:break;}super.onMeasure(widthMeasureSpec, heightMeasureSpec);}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);canvas.drawCircle(getWidth() / 2f, getHeight() / 2f, getWidth() / 2f, mPaint);}}

activity_main.xml

12345

  • 設定該View的佈局引數為wrap_content,執行效果:

    圖片描述

列印log如下:

12345
05-24 10:45:27.359  7026  7026 D hcy     : onCreate: width is 005-24 10:45:27.359  7026  7026 D hcy     : onCreate: measuredWidth is 005-24 10:45:27.362  7026  7026 D hcy     : onResume: width is 005-24 10:45:27.362  7026  7026 D hcy     : onResume: measuredWidth is 005-24 10:45:27.399  7026  7026 D hcy     : onMeasure: AT_MOST

  • 設定該View的佈局引數為match_parent,執行效果:

    圖片描述

    列印log如下:

    12345
    05-24 10:57:10.084  8526  8526 D hcy     : onCreate: width is 005-24 10:57:10.084  8526  8526 D hcy     : onCreate: measuredWidth is 005-24 10:57:10.101  8526  8526 D hcy     : onResume: width is 005-24 10:57:10.101  8526  8526 D hcy     : onResume: measuredWidth is 005-24 10:57:10.203  8526  8526 D hcy     : onMeasure: EXACTLY
  • 設定View的佈局引數為精確值(layout_width=”100dp”,layout_height=”100dp”),執行效果:

    圖片描述

12345
05-24 11:06:34.124 10903 10903 D hcy     : onCreate: width is 005-24 11:06:34.124 10903 10903 D hcy     : onCreate: measuredWidth is 005-24 11:06:34.126 10903 10903 D hcy     : onResume: width is 005-24 11:06:34.126 10903 10903 D hcy     : onResume: measuredWidth is 005-24 11:06:34.187 10903 10903 D hcy     : onMeasure: EXACTLY

現象:

  • 指定View的佈局引數為wrap_content,顯示卻充滿整個螢幕,是因為Activity的父佈局是id為content的FrameLayout,具體原因在上面getChildMeasureSpec方法中已經分析

  • 在onCreate和onResume方法中無法獲取到View的寬高,因為此時View的測量還沒開始。

圖片描述

  • View的measure方法由final修飾,子類無法重寫,可以透過重寫onMeasure方法來完成測量,重寫onMeasure後必須呼叫setMeasuredDimension,當然也可以不重寫,Android提供了一個預設測量檢視View大小的實現getDefaultSize。

  • 從流程圖來看,measure過程是從頂層父佈局向子佈局遞迴呼叫view.measure方法,實際的測量工作是由onMeasure方法完成,在測量中起關鍵作用的是測量規格MeasureSpec,最頂層的DecorView的測量規格是透過ViewRootImpl的getRootMeasureSpec方法獲得,LayoutParams的寬高均為MATCH_PARENT,specMode為EXACTLY,specSize為螢幕大小,子佈局的測量規格由父佈局的測量規格和自身的LayoutParams決定,待子佈局確定好大小後,父佈局再確定自身的大小

  • 因為ActivityThread的performResumeActivity開始之後才會陸續呼叫到performTraversals()方法開始測量,所以在Activity的onCreate和onResume方法中呼叫View.getWidth()和View.getMeasuredHeight()返回值為0, 測量還未開始。

原文連結:http://www.apkbus.com/blog-865196-78384.html

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/756/viewspace-2811268/,如需轉載,請註明出處,否則將追究法律責任。

相關文章