measure 过程要分情况来看,如果只是一个原始的 View,那么通过 measure 方法就完成了测量过程,如果是一个 ViewGroup,除了完成自己的测量以外,还会遍历调用所有子元素的 measure 方法,各个子元素在递归去执行这个流程,下面针对两种情况分别分析。
1. View 的 measure 过程
View 的 measure 过程有由其 measure 方法来完成,measure 方法是一个 final 方法,所以子类是不能重写此方法的,在 View 的 measure 方法中会调用 View 的 onMeasure 方法,因此只需要看 onMeasure 方法即可,View 的 onMeasure 方法如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
上述代码简洁,但并不简单,setMeasuredDimension 方法设置了 View 的宽高的测量,来看下代码:
/**
* <p>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.</p>
*
* @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 方法需要通过getDefaultSize 方法来得到 View 的宽高,然后在进行测量,因此我们只需要看 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 方法的逻辑很简单,对于我们来说,我们只需要看 AT_MOST 和 EXACTLY这两种情况。简单的理解,其实 getDefaultSize 返回的大小就是 MeasureSpec 中的 specSize,而这个 specSize就是 View 测量后的宽高,这里多次提到测量后的大小,是因为 View 的最终大小是在 layout 阶段确定的,所以这里必须加以区分,但是几乎所有情况下 View 测量后的大小就是 View 的最终大小,需要注意的是不是所有情况,后续讲到 layout 过程的死后会说明,好了,我们继续往下看。
至于 UNSPECIFIEND 这种情况,一般用于系统内部的测量过程,在这种情况下,View 的大小为 getDefaultSize 的第一个参数 size,通过上边的源码我们可以看明白。而通过 onMeasure 的源码,我们可以看明白,这个 size 其实就是 getSuggestedMinimumWidth() 和 getSuggestedMinimumHeight() 这两个方法的返回值,我们来看下 getSuggestedMinimumWidth() 的源码,getSuggestedMinimumHeight() 和 getSuggestedMinimumWidth() 原理是一样的,所以只需要看其中一个就好:
/**
* Returns the suggested minimum height that the view should use. This
* returns the maximum of the view's minimum height
* and the background's minimum height
* ({@link android.graphics.drawable.Drawable#getMinimumHeight()}).
* <p>
* When being used in {@link #onMeasure(int, int)}, the caller should still
* ensure the returned height is within the requirements of the parent.
*
* @return The suggested minimum height of the view.
*/
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
从源码中,我们可以看出,如果 View 没有设置背景,那么我们得到的 View 的宽度就是 mMinWidth,而 mMinWidth 又对应于 android:minWidth 这个属性所指定的值,因此 View 的宽度即为 android:minWidth 所指定的值。如果 android:minWidth 不指定大小,那么它默认是0,也就是说,View 在 android:minWidth 不指定的情况下,宽高都是0,;如果 View 设置了背景,则 View 的宽度为 max(mMinWidth, mBackground.getMinimumWidth()),mMinWidth 的含义刚才我们已经说了,那么 mBackground.getMinimumWidth() 是什么呢?我们来看下 Drawable 的 getMinimumWidth() 方法(如果不明白为什么明明是 View 类,怎么又和 Drawable 类扯上了关系,请查看源码,这里就不解释了):
/**
* Returns the minimum width suggested by this Drawable. If a View uses this
* Drawable as a background, it is suggested that the View use at least this
* value for its width. (There will be some scenarios where this will not be
* possible.) This value should INCLUDE any padding.
*
* @return The minimum width suggested by this Drawable. If this Drawable
* doesn't have a suggested minimum width, 0 is returned.
*/
public int getMinimumWidth() {
final int intrinsicWidth = getIntrinsicWidth();
return intrinsicWidth > 0 ? intrinsicWidth : 0;
}
可以看出 getMinimumWidth() 返回的就是 Drawable 的原始宽度,前提是 Drawable 有原始宽度,否则返回0,那么 Drawable 在什么情况下有原始宽度呢?这里先举两个例子,以后会讲解具体的内容。
① ShapeDrawable 无原始的宽高;
② BitmapDrawable 欧原始的宽高。
从 getDefaultSize 方法的实现来看,View 的宽高由 specSize 决定,所以我们可以得出结论,直接继承 View 的自定义控件需要重写 onMeasure 方法并设置 warp_content 时的大小,否则在布局中使用 warp_content 就相当于使用 match_parent。为什么呢?这个原因结核上篇文章中的表格更好理解,如果 View 在布局中使用 warp_content,那么他的 specMode 是 AT_MOST,在这种模式下,他的宽高等于 specSize,查看表知道 View 的 specSize 是 parentSize,而 parentSize 是父容器中目前使用的大小,也就是当前父容器剩余空间的大小,很显然,View 的宽高就等于父容器当前剩余空间的大小,这种效果和在布局中使用 match_parent 完全一致,如何解决这个问题呢,方法如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
} else if(widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSpecSize);
} else if(heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, mHeight);
}
}
在上面的代码中,我们只需要给 View 指定一个默认的内部宽高(mWidth 和 mHeight),并在 warp_content 的时候设置此宽高即可。对于非 warp_content 情形,我们沿用系统的测量值即可,至于这个默认的内部宽高的大小如何指定,这个没有固定的依据,根据需要灵活指定即可,。如果查看 TextView、ImageView 等的源码就可以知道,针对 warp_content 情形,它们的 onMeasure 方法均做了特殊处,这里就不一一解读了,代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
BoringLayout.Metrics boring = UNKNOWN_BORING;
BoringLayout.Metrics hintBoring = UNKNOWN_BORING;
if (mTextDir == null) {
mTextDir = getTextDirectionHeuristic();
}
int des = -1;
boolean fromexisting = false;
if (widthMode == MeasureSpec.EXACTLY) {
// Parent has told us how big to be. So be it.
width = widthSize;
} else {
if (mLayout != null && mEllipsize == null) {
des = desired(mLayout);
}
if (des < 0) {
boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
if (boring != null) {
mBoring = boring;
}
} else {
fromexisting = true;
}
if (boring == null || boring == UNKNOWN_BORING) {
if (des < 0) {
des = (int) Math.ceil(Layout.getDesiredWidth(mTransformed, mTextPaint));
}
width = des;
} else {
width = boring.width;
}
final Drawables dr = mDrawables;
if (dr != null) {
width = Math.max(width, dr.mDrawableWidthTop);
width = Math.max(width, dr.mDrawableWidthBottom);
}
if (mHint != null) {
int hintDes = -1;
int hintWidth;
if (mHintLayout != null && mEllipsize == null) {
hintDes = desired(mHintLayout);
}
if (hintDes < 0) {
hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, mHintBoring);
if (hintBoring != null) {
mHintBoring = hintBoring;
}
}
if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
if (hintDes < 0) {
hintDes = (int) Math.ceil(Layout.getDesiredWidth(mHint, mTextPaint));
}
hintWidth = hintDes;
} else {
hintWidth = hintBoring.width;
}
if (hintWidth > width) {
width = hintWidth;
}
}
width += getCompoundPaddingLeft() + getCompoundPaddingRight();
if (mMaxWidthMode == EMS) {
width = Math.min(width, mMaxWidth * getLineHeight());
} else {
width = Math.min(width, mMaxWidth);
}
if (mMinWidthMode == EMS) {
width = Math.max(width, mMinWidth * getLineHeight());
} else {
width = Math.max(width, mMinWidth);
}
// Check against our minimum width
width = Math.max(width, getSuggestedMinimumWidth());
if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(widthSize, width);
}
}
int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
int unpaddedWidth = want;
if (mHorizontallyScrolling) want = VERY_WIDE;
int hintWant = want;
int hintWidth = (mHintLayout == null) ? hintWant : mHintLayout.getWidth();
if (mLayout == null) {
makeNewLayout(want, hintWant, boring, hintBoring,
width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
} else {
final boolean layoutChanged = (mLayout.getWidth() != want) ||
(hintWidth != hintWant) ||
(mLayout.getEllipsizedWidth() !=
width - getCompoundPaddingLeft() - getCompoundPaddingRight());
final boolean widthChanged = (mHint == null) &&
(mEllipsize == null) &&
(want > mLayout.getWidth()) &&
(mLayout instanceof BoringLayout || (fromexisting && des >= 0 && des <= want));
final boolean maximumChanged = (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum);
if (layoutChanged || maximumChanged) {
if (!maximumChanged && widthChanged) {
mLayout.increaseWidthTo(want);
} else {
makeNewLayout(want, hintWant, boring, hintBoring,
width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
}
} else {
// Nothing has changed
}
}
if (heightMode == MeasureSpec.EXACTLY) {
// Parent has told us how big to be. So be it.
height = heightSize;
mDesiredHeightAtMeasure = -1;
} else {
int desired = getDesiredHeight();
height = desired;
mDesiredHeightAtMeasure = desired;
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desired, heightSize);
}
}
int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();
if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));
}
/*
* We didn't let makeNewLayout() register to bring the cursor into view,
* so do it here if there is any possibility that it is needed.
*/
if (mMovement != null ||
mLayout.getWidth() > unpaddedWidth ||
mLayout.getHeight() > unpaddedHeight) {
registerForPreDraw();
} else {
scrollTo(0, 0);
}
setMeasuredDimension(width, height);
}
2. ViewGroup 的 measure 过程
对于 ViewGroup 来说,除了完成自己的 measure 过程以外,还会遍历去掉用所有子元素的 measure 方法,各个子元素在递归去执行这个过程。和 View 不同的是,ViewGroup 是一个抽象类,因此它没有重写 View 的 onMeasure 方法,但是它提供了一个 measureChildren 方法,代码如下:
/**
* Ask all of the children of this view to measure themselves, taking into
* account both the MeasureSpec requirements for this view and its padding.
* We skip children that are in the GONE state The heavy lifting is done in
* getChildMeasureSpec.
*
* @param widthMeasureSpec The width requirements for this view
* @param heightMeasureSpec The height requirements for this view
*/
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);
}
}
}
从上面代码来看,ViewGroup 在 measure 时,会对每一个子元素进行 measure,measureChild 这个方法实现也很好理解:
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding.
* The heavy lifting is done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param parentHeightMeasureSpec The height requirements for this view
*/
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);
}
很显然,measureChild 的思想就是取出子元素的 LayoutParams,然后再通过 getChildMeasureSpec 来创建子元素的 MeasureSpec,接着将 MeasureSpec 直接传递给 View 的 measure 方法来进行测量。
我们知道,ViewGroup 并没有定义其测量的具体过程,这是因为 ViewGroup 是一个抽象类,其测量过程的 onMeasure 方法需要各个子类具体实现,比如 LinearLayout、RelativeLayout 等,为什么 ViewGroup 不像 View 一样对其 onMeasure 方法做统一实现呢?那是因为不同的 ViewGroup 子类有不同的布局特性,这导致它们的测量细节各不相同,比如 LinearLayout 和 RelativeLayout 这两者的布局特性显然不同,因此,ViewGroup 不能做统一实现。下面通过 LinearLayout 的 onMeasure 方法来分析 ViewGroup 的 measure 过程,其他 Layout 类型读者可以自行分析。
首先来看 LinearLayout 的 onMeasure 方法,如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
上述代码很简单,我们来看下竖直布局的 LinearLayout 的测量过程,即 measureVertical 方法,measureVertical 的源码比较长,下面只描述大概意思,首先看一段代码:
// See how tall everyone is. Also remember max width.
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;
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
totalWeight += lp.weight;
final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
// Optimization: don't bother measuring children who are only
// laid out using excess space. These views will get measured
// later if we have space to distribute.
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
skippedMeasure = true;
} else {
if (useExcessSpace) {
// The heightMode is either UNSPECIFIED or AT_MOST, and
// this child is only laid out using excess space. Measure
// using WRAP_CONTENT so that we can find out the view's
// optimal height. We'll restore the original height of 0
// after measurement.
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).
final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
heightMeasureSpec, usedHeight);
final int childHeight = child.getMeasuredHeight();
if (useExcessSpace) {
// Restore the original height and record how much space
// we've allocated to excess-only children so that we can
// match the behavior of EXACTLY measurement.
lp.height = 0;
consumedExcessSpace += childHeight;
}
final int totalLength = mTotalLength;
mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
lp.bottomMargin + getNextLocationOffset(child));
if (useLargestChild) {
largestChildHeight = Math.max(childHeight, largestChildHeight);
}
}
从上面的代码可以看出,系统会遍历子元素并对每个子元素执行 measureChildBeforeLayout 方法,这个方法内部会调用子元素的 measure 方法,我们来看下:
void measureChildBeforeLayout(View child, int childIndex,
int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
int totalHeight) {
measureChildWithMargins(child, widthMeasureSpec, totalWidth,
heightMeasureSpec, totalHeight);
}
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);
}
通过调用这个方法,这样各个子元素开始依次进入 measure 过程,并且系统会通过 mTotalLength 这个变量来存储 LinearLayout 在竖直方向的初始高度,每次量一个子元素,mTotalLength 就会增加,增加的部分主要包括了子元素的高度以及子元素在竖直方向上的 margins、padding等,当子元素测量完毕后,LinearLayout 会测量自己的大小,代码如下:
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
...
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);
通过上述代码我们发现, 当子元素测量完毕之后,LinearLayout 会根据子元素的情况来测量自己的大小。针对竖直的 LinearLayout 而言,它在水平方向的测量过程遵循 View 的测量过程,在竖直方向的测量过程和 View 有所不同。具体来说是指,如果他的布局中高度采用的是 match_parent 或者具体的数值,那么他的测量过程和 View 一致,即高度为 specSize;如果它的布局中高度采用的是 warp_content,那么它的高度是所有子元素占用的高度的总和,但是仍然不能超过他的父容器的剩余空间,当然他的最终高度还需要考虑它在竖直方向的 padding,这个过程可以进一步看下源码:
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}
View 的 measure 过程是三大流程中最复杂的一个,measure 完成以后,通过 getMeasuredWidth/Height方法就可以正确获取到 View 的测量宽高,需要注意的是,在某些极端情况下,系统可能需要多次 measure 才能得到最终的宽高,在这种情形下,在 onMeasure 方法中拿到的测量宽高是不准确的,一个好的习惯是在 onLayout 方法中去获取 View 的测量宽高和最终宽高。
上面已经对 View 的 measure 过程进行了详细的分析,现在考虑一种情况。比如我们想在 Activity 已启动的时候就做一件任务,但是这一件任务需要获取某个 View 的宽高,读者可能会说,这很简单啊,在 onCreate 或者 onResume 里面获取这个 View 的宽高不就行了?读者可以自行试一下,实际上在 onCreate、onStart 和 onResume 中均无法正确获取到某个 View 的宽高信息,这是因为 Activity 的生命周期和 View 的 measure 过程不是同步执行的,因此无法保证 Activity 执行了 onCreate、onStart 和 onResume 时某个 View 已经测量完毕了,如果 View 还没有测量完毕,那获取到的宽高就是 0 了,那有没有办法解决这个问题呢?肯定是有的,下边来给出四种方法解决这个问题:
① Activity/View#onWindowFocusChanged
onWindowFocusChanged这个方法的含义是:View 已经初始化完毕了,宽高已经准备好了。这个时候去获取狂傲是没有问题的。需要注意的是,onWindowFocusChanged 会被调用多次,当 Activity 的窗口得到焦点和失去焦点的时候各调用一次,具体来说,Activity 继续执行和暂停执行的时候,onWindowFocusChanged 均会被调用,如果频繁的进行 onResume 和 onPause,那么 onWindowFocusChanged 会被频繁的调用,具体代码如下:
@Override
public void onWindowFocusChanged(boolean hasFocus) {
if(hasFocus) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
② view.post(runnable)
通过 post 可以将一个 runnable 投递到消息队列的尾部,然后等待 lopper 调用此 runnable 的时候,View 已经初始化好了,具体代码如下:
@Override
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
③ ViewTreeObserver
使用 ViewTreeObserver 的众多回调可以完成这个功能,比如使用 OnGlobalLayoutListener 接口,当 View 树发生改变 或者 View 树内部的 View 的可见性发生改变时,onGlobalLayout 方法会被回调,因此这是获取 View 宽高一个焊好的时机,需要注意的是,伴随着 View 树的动态发生改变时,onGlobalLayout方法会被调用多次,具体代码如下:
ViewTreeObserver viewTreeObserver = view.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
④ view.measure(int widthMeasureSpec, int heightMeasureSpec)
通过手动对 View 进行 measure 来得到 View 的宽高,这个方法比较复杂,这里要分情况处理,根据 View 的 LayoutParams来分:
a. match_parent
直接放弃,无法 measure 出具体的宽高,原因很简单,根据 View 的 measure 过程,构造此种 MeasureSpec 需要知道 parentSize,也就是父容器的剩余空间,而这个时候,我们无法知道 parentSize 的大小,所以理论上测不出 View 的大小。
b. 具体的数值(dp/px)
比如狂傲都是 100dp,如下measure:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
view.onMeasure(widthMeasureSpec, heightMeasureSpec);
c. warp_content
如下measure:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30) - 1, MeasureSpec.AT_MOST);
view.onMeasure(widthMeasureSpec, heightMeasureSpec);
注意 (1 << 30) - 1,通过分析 MeasureSpec 的实现我们知道,View 的尺寸使用 30 位的二进制表示,也就是说最大 30 个 1(即 2 ^30 - 1),也就是 (1 << 30) - 1,在最大化的模式下,我们用 View 理论上支持的最大值去构造 MeasureSpec 是合理的。关于 View 的 measure,网络上有两个错误的用法,为什么说是错误的,首先是违背了系统的内部实现规范(因为无法通过错误的 MeasureSpec 去得出合法的 SpecMode,从而导致 measure 过程出错),其次不能保证一定能 measure 出结果。
第一种错误用法,不过这种用法已经被Google干掉了,makeMeasureSpec 的第一个参数必须是大于 0 的:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec( -1, MeasureSpec.UNSPECIFIED);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec( -1, MeasureSpec.UNSPECIFIED);
view.onMeasure(widthMeasureSpec, heightMeasureSpec);
第二种错误用法:
view.onMeasure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
到此,View 的 measure 过程就完成了。篇幅太长,只能把 Layout 和 Draw 过程放在下一篇文章来讲解了。