【Android - 自定义View】之MeasureSpec简介
MeasureSpec是View测量过程中的一个重要的类,它被用来将View的尺寸规格(SpecSize)和尺寸模式(SpecMode)封装在一起,并提供打包和解包的方法。
MeasureSpec虽然是一个工具类,在View测量的时候会用这个类来解析View的规格和模式,因此很多人都用MeasureSpec兼代这个尺寸和模式的打包值。
MeasureSpec是一个 32 位的 int 值,其高2位代表SpecMode,即测量模式;低30位代表SpecSize,即在某种模式下的规格大小。MeasureSpec用这种方式将这两个值打包在一起,并提供了获取两个值得方法 getMode(int measureSpec) 和 getSize(int measureSpec) 。
简单地说,一个View的MeasureSpec是这个View的父布局传递给这个View的,代表父布局能够提供给这个View多大的空间(在一定程度上由其父布局的尺寸决定)。
MeasureSpec中的SpecMode有三个可选值,分别是 UNSPECIFIED 、 EXACTLY 、 AT_MOST 。通常,一个View在父容器中的尺寸声明(即这个View的LayoutParams的值)可能有三种:match_parent、wrap_content和固定值(如100dip)。
一个View的MeasureSpec是由这个View的LayoutParams的值和这个View所在父布局传递过来的MeasureSpec一起决定的,而大多数情况下,View的父容器都是ViewGroup。那么我们就先从ViewGroup开始,看它的 measureChildWithMargin() 方法,源码如下:
/** * 让这个ViewGroup中的某个View自行进行测量,将这个View的MeasureSpec的宽高需求和padding、margin都计算在内
* * @param child 要测量的子View * @param parentWidthMeasureSpec 对这个View的宽度需求 * @param widthUsed 由于其他子控件占用等原因,已经使用的宽度值 * @param parentHeightMeasureSpec 对这个View的高度需求 * @param heightUsed 由于其他子控件占用等原因,已经使用的高度值 */ 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); }
从源码中可以看到,在measureChildWidthMargins()方法中,调用了 getChildMeasureSpec() 方法来测量当前ViewGroup中某个View的最佳布局的MeasureSpec,然后将宽度的MeasureSpec和高度的MeasureSpec重新赋值给这个View。getChildMeasureSpec()方法的源码如下:
/** * 这个方法用来计算并返回当前ViewGroup中某个View的某个MeasureSpec(宽度或高度中的一个)。 * 这个方法的目的是结合当前ViewGroup的MeasureSpec和某个子View的LayoutParams, * 得到最适合这个子View的MeasureSpec并返回 * * @param spec 当前ViewGroup的MeasureSpec(宽度或高度中的一个) * @param padding 子View在某个尺寸(宽度或高度中的一个)上的padding和margin的和 * @param childDimension 子View希望得到的尺寸(宽度或高度中的一个)大小 * @return 返回计算得到的这个子View布局的最佳MeasureSpec */ public static int getChildMeasureSpec(int spec, int padding, int childDimension) { int specMode = View.MeasureSpec.getMode(spec); int specSize = View.MeasureSpec.getSize(spec); int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; switch (specMode) { // 父容器在这个尺寸方向(宽度或高度中的一个)上的尺寸是固定值或MATCH_PARENT case View.MeasureSpec.EXACTLY: if (childDimension >= 0) { // 如果childDimension不小于0,则表示子View的尺寸有具体的值,如100dip resultSize = childDimension; resultMode = View.MeasureSpec.EXACTLY; } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) { // ViewGroup.LayoutParams.MATCH_PARENT = -1 resultSize = size; resultMode = View.MeasureSpec.EXACTLY; } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) { // ViewGroup.LayoutParams.WRAP_CONTENT = -2 resultSize = size; resultMode = View.MeasureSpec.AT_MOST; } break; // 父容器在这个尺寸方向(宽度或高度中的一个)上的尺寸是WRAP_CONTENT case View.MeasureSpec.AT_MOST: if (childDimension >= 0) { resultSize = childDimension; resultMode = View.MeasureSpec.EXACTLY; } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) { resultSize = size; resultMode = View.MeasureSpec.AT_MOST; } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) { resultSize = size; resultMode = View.MeasureSpec.AT_MOST; } break; // 父容器没有指定在这个方向上的尺寸,子View想要多大就给多大 case View.MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { resultSize = childDimension; resultMode = View.MeasureSpec.EXACTLY; } else if (childDimension == ViewGroup.LayoutParams.MATCH_PARENT) { resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = View.MeasureSpec.UNSPECIFIED; } else if (childDimension == ViewGroup.LayoutParams.WRAP_CONTENT) { resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = View.MeasureSpec.UNSPECIFIED; } break; } // 重新包装这个View在当前尺寸方向上的尺寸规格和尺寸模式 return View.MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
可以看到,在这个方法中枚举了父容器的MeasureSpec和子View的LayoutParams的所有可能的情况,并一一给出了解决方案。针对不同的父容器和View本身不同的LayoutParams,View就可以有多种MeasureSpec,这里对所有情况进行了梳理,如下表所示:
对照上表举个例子:当父容器的MeasureSpec的SpecMode是EXACTLY,并且子View的LayoutParams是固定值的时候,通过上面的代码得到的子View的MeasureSpec的SpecMode是EXACTLY,SpecSize就是子View的宽度或高度值。以此类推。
在自定义View的时候,父控件的尺寸大多数都是MATCH_PARENT或具体的尺寸值,因此,我们可以粗略的将SpecMode与子View的LayoutParams的关系总结如下:
- UNSPECIFIED:父容器不对子View有任何限制,要多大给多大。这种情况不常见;
- EXACTLY:父容器已经检测出子View所需要的精确大小,这个时候子View的最终大小就是SpecSize的值。它对应于LayoutParams中的MATCH_PARENT和具体的数值这两种模式;
- AT_MOST:父容器指定了一个可用的大小(可能就是父容器的大小,也可能是父容器中出去已有子View之外可用的大小),子View的大小不能大于这个值,具体是多少要看子View的具体实现。它对应于LayoutParams中的WRAP_CONTENT。
另外,上面的代码中还提到了MeasureSpec类中的 makeMeasureSpec() 方法,这个方法就是将SpecSize和SpecMode打包成一个完整的MeasureSpec的方法。
最后补充一点:
我们上面说的都是针对普通View来说的,但是MeasureSpec不仅可以表示普通View的尺寸,也可以表示顶层View(DecorView)的尺寸。如果是针对DecorView,则其父容器就是当前Activity中窗口的大小,当SpecMode为EXACTLY的时候,其SpecSize就是窗口的尺寸大小。其他的和普通View的情况相同。
有了MeasureSpec,我们就可以在自定义View的时候,在 onMeasure() 方法中通过传入的宽度和高度的MeasureSpec的值,获得父布局分配给我们的自定义View的尺寸大小,然后进行相应的修改,最后通过 setMeasuredDimension() 方法重置View的尺寸,达到想要的效果。可以看出,MeasureSpec在自定义View中还是比较核心的知识点。