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

  • 时间:2019-06-11 06:34 作者:nanchen2251 来源:nanchen2251 阅读:54
  • 扫一扫,手机访问
摘要:作为一名 Android 开发,正常情况下对 View 的绘制机制基本还是耳熟能详的,尤其对于经常需要自己设置 View 实现少量特殊效果的同学。网上也出现了大量的 Blog 讲 View 的 onMeasure()、onLayout()、onDraw() 等,尽管这是一个每个 Android 开发都

作为一名 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 才能实现滚动效果,但很遗憾并不是这样的。所以在这里抛出这个问题,希望有知情人士能一起探讨。

  • 全部评论(0)
最新发布的资讯信息
【网页前端|HTML】最全面的前端开发指南(2019-07-15 12:57)
【系统环境|数据库】零基础如何快速学好大数据?(2019-06-29 12:27)
【系统环境|Linux】零基础如何学好大数据?必备需要学习知识(2019-06-18 11:54)
【系统环境|】Hadoop环境中管理大数据存储八大技巧(2019-06-15 11:01)
【系统环境|服务器应用】现在国内IT行业是不是程序员过多了?(2019-06-11 06:34)
【系统环境|服务器应用】新贵 Flutter(2) 自己设置 Widget(2019-06-11 06:34)
【系统环境|服务器应用】Android完整知识体系路线(菜鸟-资深-大牛必进之路)(2019-06-11 06:34)
【系统环境|服务器应用】Java程序员小伙经历三个月备战,终获阿里offer(2019-06-11 06:34)
【系统环境|服务器应用】每日一问:谈谈对 MeasureSpec 的了解(2019-06-11 06:34)
【系统环境|服务器应用】【科普】晶体管-1(2019-06-11 06:34)
手机二维码手机访问领取大礼包
返回顶部