每日一问:谈谈对 MeasureSpec 的了解
作为一名 Android 开发,正常情况下对 View 的绘制机制基本还是耳熟能详的,尤其对于经常需要自己设置 View 实现少量特殊效果的同学。
网上也出现了大量的 Blog 讲 View 的 onMeasure()、onLayout()、onDraw() 等,尽管这是一个每个 Android 开发都应该知晓的东西,但这一系列实在是太多了,完全不符合咱们短平快的这个系列初衷。
那么,今天我们就来简单谈谈 measure() 过程中非常重要的 MeasureSpec。
对于绝大多数人来说,都是知道 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 中。
我们看这样一个 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 预览中却会惊讶的发现不一样的情况。

我们所期望的是填充满屏幕的 TextView,但实际效果却和 TextView 设置高度为 wrap_content 如出一辙。
很显著,这肯定是高度测量出现的问题,假如我们的父布局是 LinearLayout,很显著没有任何疑问。所以问题肯定出在了 NestedScrollView 的 onMeasure() 中。
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() 做了什么。
@Overrideprotected 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 才能实现滚动效果,但很遗憾并不是这样的。所以在这里抛出这个问题,希望有知情人士能一起探讨。
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 每日一问:谈谈对 MeasureSpec 的了解