每日一问:谈谈对 MeasureSpec 的了解

作者 : 开心源码 本文共4406个字,预计阅读时间需要12分钟 发布时间: 2022-05-12 共183人阅读

作为一名 Android 开发,正常情况下对 View 的绘制机制基本还是耳熟能详的,尤其对于经常需要自己设置 View 实现少量特殊效果的同学。

网上也出现了大量的 Blog 讲 View 的 onMeasure()onLayout()onDraw() 等,尽管这是一个每个 Android 开发都应该知晓的东西,但这一系列实在是太多了,完全不符合咱们短平快的这个系列初衷。

那么,今天我们就来简单谈谈 measure() 过程中非常重要的 MeasureSpec

对于绝大多数人来说,都是知道 MeasureSpec 是一个 32 位的 int 类型。并且取了最前面的两位代表 Mode,后 30 位代表大小 Size。

相比也非常清楚 MeasureSpec 有 3 种模式,它们分别是 EXACTLYAT_MOSTUNSPECIFIED

  • 准确模式(MeasureSpec.EXACTLY):在这种模式下,尺寸的值是多少,那么这个组件的长或者宽就是多少,对应 MATCH_PARENT 和确定的值。
  • 最大模式(MeasureSpec.AT_MOST):这个也就是父组件,能够给出的最大的空间,当前组件的长或者宽最大只能为这么大,当然也可以比这个小。对应 WRAP_CONETNT
  • 未指定模式(MeasureSpec.UNSPECIFIED):这个就是说,当前组件,可以随意用空间,不受限制。

通常来说,我们在自己设置 View 的时候会经常地接触到 AT_MOSTEXACTLY,我们通常会根据两种模式去定义自己的 View 大小,在 wrap_content 的时候使用自己计算或者者设置的一个默认值。而更多的时候我们都会认为 UNSPECIFIED 这个模式被应用在系统源码中。具体就表现在 NestedScrollViewScrollView 中。

我们看这样一个 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,很显著没有任何疑问。所以问题肯定出在了 NestedScrollViewonMeasure() 中。

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 FrameLayoutonMeasure() 做了什么。

@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 只展现一行也是因为这个问题,处理方案就是重写 NestedScrollViewmeasureChildWithMargins() 或者者重写 ListView 或者者 RecylerViewonMeasure() 方法让其展现正确的高度。

我起初猜想是只有 UNSPECIFIED 才能实现滚动效果,但很遗憾并不是这样的。所以在这里抛出这个问题,希望有知情人士能一起探讨。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 每日一问:谈谈对 MeasureSpec 的了解

发表回复