目录
3.widthMeasureSpec和heightMeasureSpec
1.引言
刚入职不满一年的Android新人,如有错漏请轻拍。在自定义View的过程中,通常不需要重写onMeasure(),但到了ViewGroup中,常常需要自定义子View的排列,遇到LinearLayout或RelativeLayout很难甚至没办法实现的需求时,就必须要重写onMeasure。我也曾查阅过网上许多的文章,但总感觉不够系统和完善,于是决定自己摸索一番。
2.谁来调用onMeasure()?
稍微看一下源码应该都能看到,onMeasure是被View#measure()调用的,那么我们为什么还要去探索这个问题呢。因为我很想知道onMeasure(int widthMeasureSpec, int heightMeasureSpec),两个参数是怎么来的。我们在XML放置自定义的ViewGroup时候,往往会放置为顶级布局。那么这两个参数,在文档上说是父布局对自定义ViewGroup的约束,是哪里传递过来的呢。
XML文件
<?xml version="1.0" encoding="utf-8"?>
<com.example.customsizeview.ViewGroupTouch.ViewPagerCustomSize
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.example.customsizeview.ViewGroupTouch.CustomView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/darker_gray" />
</com.example.customsizeview.ViewGroupTouch.ViewPagerCustomSize>
布局文件仅仅是自定义ViewGroup嵌套一个自定义View,这个自定义View只重写了onMeasure()用于输出。
其实通过AndroidStudio的Layout Inspector可以看出来即使在XML中是顶级布局,在实际的布局中也是子布局。
从onMeasure()的定义看,我们需要先找出自定义ViewGroup的父ViewGroup也就是ContentFrameLayout,然后一层层往上追溯,直到DecorView怎么生成widthMeasureSpec、heightMeasureSpec两个参数才行。最后再一直处理和传递到自定义View Group才是,但这样未免过于复杂,而且看DecorView的源码,我并没用完全看懂。但至少我们现在清楚了两点,一个是XML的顶部布局拿到的widthMeasureSpec和heightMeasureSpec也是从上层传入的,一个是DecorView其本身代表的是手机界面的大小,比如我的小米6就是1920*1080,是个确切的数值。
3.widthMeasureSpec和heightMeasureSpec
widthMeasureSpec和heightMeasureSpec一个是宽一个是高,只要研究一个另一个是一样的。我们就来分析widthMeasureSpec。既然没办法很容易的搞清楚widthMeasureSpec怎么产生的,那么我们看看widthMeasureSpec到底是什么吧。widthMeasureSpec其实是包含了父ViewGroup对子View 宽度模式和宽度尺寸两个要求。
可以通过MeasureSpec.getMode(widthMeasureSpec)和MeasureSpec.getSize(widthMeasureSpec)获取到 mode和size。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
然后我们将mode和size打印出来(我通过debug打断点来打印的)。。。。。
发现size还很好理解,mode是个啥?别急,我们看看官方控件中是怎么处理这个widthMeasureSpec的吧。measureChildren()就是ViewGroup自带的处理子View布局的方法。让我们来看看measureChildren()是怎么处理widthMeasureSpec的。
首先进入ViewGroup#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会遍历自己的子View,对每个子view都设置一次measureChild(),并将widthMeasureSpec作为参数之一传入。
再进入ViewGroup#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);
}
这里会获取到刚才传入的child,通过child.getLayoutParam获取布局属性,最后通过lp.width获取child的layout_width作为getChildMeasureSpec()的参数之一。
再进入viewGroup#getChildMeasureSpec()
/**
* 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);
}
spec就是measureChildren()传入的widthMeasureSpec,然后获取widthMeasureSpec的mode和size,先判断mode,再判断childDimension,而childDimension就是child的layout_width。然后会给resultSize和resultMode赋值,你会发现,这两个值似乎也很熟悉,似乎和widthMeasureSpec的size和mode是一样的。的确是的,MeasureSpec.makeMeasureSpec(resultSize, resultMode)会将resultSize和resultMode组装成一个MeasureSpec(其实就是两个int通过位运算放进要给int里面,算是节省了空间),也放一下代码吧。
View#makeMeasureSpec
/**
* Creates a measure specification based on the supplied size and mode.
*
* The mode must always be one of the following:
* <ul>
* <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>
* <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>
* <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>
* </ul>
*
* <p><strong>Note:</strong> 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.</p>
*
* @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.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
现在我们了解了ViewGroup测量子View的操作后,会发现其实MeasureSpec的mode只会有三个值:
/**
* 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;
通过打印出来会发现对应的是
UNSPECIFIED = 0
EXACTLY = 1073741824
AT_MOST = -2147483648
回到最开始,自定义ViewGroup的widthMeasureSpec的mode 其实就是EXACTLY,通过注释会发现,这是父ViewGroup对子View尺寸的精确要求。子View的大小完全按照父ViewGroup的要求来设置。(这里是自定义ViewGroup的父ViewGroup对自定义ViewGroup的要求)。
4.wrap_content之谜
尝试过在xml文件中,子View的layout属性设置为wrap而父ViewGroup设置为match时,你往往会发现,有时候wrap_content代表的是不占空间,有时候则和设置为match_parent一样,这使得我这种看不怪表里不一的人很是头大,你咋能一天一个样呢?那么我们来看看wrap_content是如何影响测量的。
我们将XML中CustomView的layout属性设置为wrap_content
<com.example.customsizeview.ViewGroupTouch.CustomView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/darker_gray" />
从之前的分析,我们看到现在我们自定义的ViewGroup收到的widthMeasureSpec mode = EXACTLY,而其调用measureChildren()时,将收到的widthMeasureSpec直接传入,最后getChildMeasureSpec()判断widthMeasureSpec的mode = EXACTLY,而子View的lp.width = wrap_content ,于是resultSize = 1080 ,resultMode = AT_MOST,最后会通过 child.measure(childWidthMeasureSpec, childHeightMeasureSpec);将子View的测量结果保存到子View的全局变量中。
这里child.measure()就是View#measure(),最后会执行View#onMeasure()
/**
* <p>
* 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.
* </p>
*
* <p>
* <strong>CONTRACT:</strong> When overriding this method, you
* <em>must</em> call {@link #setMeasuredDimension(int, int)} to store the
* measured width and height of this view. Failure to do so will trigger an
* <code>IllegalStateException</code>, thrown by
* {@link #measure(int, int)}. Calling the superclass'
* {@link #onMeasure(int, int)} is a valid use.
* </p>
*
* <p>
* 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.
* </p>
*
* <p>
* 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()}).
* </p>
*
* @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));
}
别看这么多其实就一行代码,就做了一件事情,将getDefaultSize()的结果作为setMeasuredDimension()的参数。(这里就不细看setMeasuredDimension()了,很简单,就是将其做一些处理后保存为view的全局变量mMeasuredWidth,getSuggestedMinimumWidth()则是判断若有背景,则返回背景图的最小值),我们看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;
}
判断子View#onMeasure()的widthMeasureSpec,其mode = AT_MOST和EXACTLY的时候,result都 = specSize,specSize就是widthMeasureSpec的size。最后返回result。那么最后setMeasuredDimension()的其实就是widthMeasureSpec的size,也就是铺满父容器。那么如何才能让wrap_content变成宽度为0呢,当然可以自己仿照getDefaultSize()来重写onMeasure(),也可以看看RelativeLayout,它用了resolveSize()来设置wrap_content的宽度。
5.总结
最后总结一下,自定义ViewGroup重写onMeasure()的时候,onMeasure()的两个参数也是由父ViewGroup的child.measure()传过来的,而XML中自定义ViewGroup的layout属性,在父ViewGroup执行getChildMeasureSpec()的时候会和父ViewGroup给出的widthMeasureSpec一起影响,最终生成自定义ViewGroup#onMeasure()的两个参数。概括下就是View#onMeasure()的两个参数,受父ViewGruop的MeasureSpec和View在xml中设置的layout属性一起影响。这里还有一个问题,就是顶级的布局,其父ViewGroup给顶级布局的MeasureSpec的mode到底是什么?
表中 父ViewGoup代表的是顶级布局(自定义布局)。通过这样测试,我发现其实MeasureSpec的mode就是EXACTLY(结合getChildMeasureSpec反推出来的)。
onMeasure的重写其实比较有限,主要就是要完善wrap_content的功能才去重写。重写的步骤很简单:
- ViewGroup要给每个子View发布测量命令并配置他们的MeasureSpec(用ViewGroup已经给出的方法也是可以的)。
- 根据getChildCount()和getChildAt()来遍历子View,然后通过view#getMeasuredWidth()来获取子View测量好的宽高。
- 设置自定义ViewGroup的宽高,setMeasuredDimension()
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); measureChildren(widthMeasureSpec, heightMeasureSpec); View view = getChildAt(0); int viewWidth = view.getMeasuredWidth(); int viewHeight = view.getMeasuredHeight(); Log.d("debug","viewWidth---:"+viewWidth+"---viewHeight---:"+viewHeight); super.onMeasure(widthMeasureSpec, heightMeasureSpec); }