做为一名 Android 开发,正常状况下对 View 的绘制机制基本仍是耳熟能详的,尤为对于常常须要自定义 View 实现一些特殊效果的同窗。java
网上也出现了大量的 Blog 讲 View 的 onMeasure()
、onLayout()
、onDraw()
等,虽然这是一个每一个 Android 开发都应该知晓的东西,但这一系列实在是太多了,彻底不符合我们短平快的这个系列初衷。android
那么,今天咱们就来简单谈谈 measure()
过程当中很是重要的 MeasureSpec
。ide
对于绝大多数人来讲,都是知道 MeasureSpec
是一个 32 位的 int 类型。而且取了最前面的两位表明 Mode,后 30 位表明大小 Size。布局
相比也很是清楚 MeasureSpec
有 3 种模式,它们分别是 EXACTLY
、AT_MOST
和 UNSPECIFIED
。测试
- 精确模式(MeasureSpec.EXACTLY):在这种模式下,尺寸的值是多少,那么这个组件的长或宽就是多少,对应
MATCH_PARENT
和肯定的值。- 最大模式(MeasureSpec.AT_MOST):这个也就是父组件,可以给出的最大的空间,当前组件的长或宽最大只能为这么大,固然也能够比这个小。对应
WRAP_CONETNT
。- 未指定模式(MeasureSpec.UNSPECIFIED):这个就是说,当前组件,能够随便用空间,不受限制。
一般来讲,咱们在自定义 View 的时候会常常地接触到 AT_MOST
和 EXACTLY
,咱们一般会根据两种模式去定义本身的 View 大小,在 wrap_content
的时候使用本身计算或者设置的一个默认值。而更多的时候咱们都会认为 UNSPECIFIED
这个模式被应用在系统源码中。具体就体如今 NestedScrollView
和 ScrollView
中。this
咱们看这样一个 XML 文件:日志
<?xml version="1.0" encoding="utf-8"?> <android.support.v4.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/scrollView" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorAccent" android:text="Hello World" android:textColor="#fff"> </TextView> </android.support.v4.widget.NestedScrollView>
在 NestedScrollView
里面写了一个充满屏幕高度的 TextView
,为了更方便看效果,咱们设置了一个背景颜色。但咱们从 XML 预览中却会惊讶的发现不同的状况。
code
咱们所指望的是填充满屏幕的 TextView
,但实际效果却和 TextView
设置高度为 wrap_content
一模一样。xml
很明显,这必定是高度测量出现的问题,若是咱们的父布局是 LinearLayout
,很明显没有任何问题。因此问题必定出在了 NestedScrollView
的 onMeasure()
中。blog
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (this.mFillViewport) { int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (heightMode != 0) { if (this.getChildCount() > 0) { View child = this.getChildAt(0); LayoutParams lp = (LayoutParams)child.getLayoutParams(); int childSize = child.getMeasuredHeight(); int parentSpace = this.getMeasuredHeight() - this.getPaddingTop() - this.getPaddingBottom() - lp.topMargin - lp.bottomMargin; if (childSize < parentSpace) { int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, this.getPaddingLeft() + this.getPaddingRight() + lp.leftMargin + lp.rightMargin, lp.width); int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(parentSpace, 1073741824); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } } } }
因为咱们并无在外面设置 mFillViewport
这个属性,因此并不会进入到 if 条件中,咱们来看看 NestedScrollView
的 super FrameLayout
的 onMeasure()
作了什么。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int count = getChildCount(); final 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 < count; i++) { final View child = getChildAt(i); if (mMeasureAllChildren || child.getVisibility() != GONE) { measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); childState = combineMeasuredStates(childState, child.getMeasuredState()); if (measureMatchParentChildren) { if (lp.width == LayoutParams.MATCH_PARENT || lp.height == LayoutParams.MATCH_PARENT) { mMatchParentChildren.add(child); } } } } // ignore something... }
注意其中的关键方法 measureChildWithMargins()
,这个方法在 NestedScrollView
中获得了彻底重写。
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { MarginLayoutParams lp = (MarginLayoutParams)child.getLayoutParams(); int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, this.getPaddingLeft() + this.getPaddingRight() + lp.leftMargin + lp.rightMargin + widthUsed, lp.width); int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, 0); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
咱们看到其中有句很是关键的代码:
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, 0);
NestedScrollView
直接无视了用户设置的 MODE,直接采用了 UNSPECIFIED
作处理。通过测试发现,当咱们重写 NestedScrollView
的这句代码,而且把 MODE 设置为 EXACTLY
的时候,咱们获得了咱们想要的效果,我已经查看 Google 的源码提交日志,并无找到缘由。
实际上,绝大多数开发以前遇到的嵌套
ListView
或者RecylerView
只展现一行也是因为这个问题,解决方案就是重写NestedScrollView
的measureChildWithMargins()
或者重写ListView
或者RecylerView
的onMeasure()
方法让其展现正确的高度。
我起初猜测是只有 UNSPECIFIED
才能实现滚动效果,但很遗憾并非这样的。因此在这里抛出这个问题,但愿有知情人士能一块儿讨论。