实名反对《阿里巴巴Android开发手册》中NestedScrollView嵌套RecyclerView的用法

相信大部分人都知道Android大体的绘制流程(把大象装冰箱,总共分几步?):

  1. measure
  2. layout
  3. draw

映射到我们平常自定义View中的方法就是onMeasureonLayoutonDraw三个方法,对于继承自ViewGroup的视图,除了要确定自身的大小外,还要帮助子View测量,确定他们的大小,对此,ViewGroup提供了一个静态方法getChildMeasureSpec:

/**

  • Does the hard part of measureChildren: figuring out the MeasureSpec to
  • pass to a particular child. This method figures out the right MeasureSpec
  • for one dimension (height or width) of one child view.
  • The goal is to combine information from our MeasureSpec with the
  • LayoutParams of the child to get the best possible results. For example,
  • if the this view knows its size (because its MeasureSpec has a mode of
  • EXACTLY), and the child has indicated in its LayoutParams that it wants
  • to be the same size as the parent, the parent should ask the child to
  • layout given an exact size.
  • @param spec The requirements for this view
  • @param padding The padding of this view for the current dimension and
  •    margins, if applicable
    
  • @param childDimension How big the child wants to be in the current
  •    dimension
    
  • @return a MeasureSpec integer for the child
    */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

int size = Math.max(0, specSize - padding);

int resultSize = 0;
int resultMode = 0;

switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can’t be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size… so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can’t be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size… let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size… find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size… find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

该方法返回一个MeasureSpec,关于MeasureSpce,翻译成中文即为测量规格,它是一个32位的int类型,高2位代表测量模式,低30位代表测量大小。网上关于它的介绍有很多,这里就不展开讲了。我们这里只要知道,测量模式有3种:

  • UNSPECIFIED 未指明模式 父布局不限制子布局的大小,对其不做任何限制。
  • EXACTLY 精确模式 父布局可以确定子布局的最终大小。
  • AT_MOST 至多模式 父布局确定不了子布局的最终大小,但是子布局的大小不能超过父布局给出的大小。

总结成表格就是这样的(借用任玉刚大佬的图):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

那这个方法返回的MeasureSpec参数子View又是在哪里用到的呢? 答案就是ViewGroup在测量子View的时候,会调用measureChildgetChildMeasureSpec传递给子View的measure方法,measure方法会继续调用我们自定义View时常用到的onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法,这里的widthMeasureSpecheightMeasureSpec 参数就是父布局传递过来的。我们来看下View类中的onMeasure方法:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

我们可以认为,调用setMeasuredDimension方法就标志着子View完成了测量,其高度和宽度也就随之确定了下来。通过不断的递归循环这个流程就能完成最终的测量。

RecyclerView的测量出现问题了么?

回到我们这个问题,通过以上View测量流程的回顾,我们可以确定:RecyclerView的高度是由NestedScrollView中传递给RecyclerView中的MeasureSpec参数和RecyclerView中的onMeasure两处决定的 我们先来看看NestedScrollView中传递给RecyclerView中的MeasureSpec参数,在NestedScrollView的measureChild方法中是这么写的:

protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
ViewGroup.LayoutParams lp = child.getLayoutParams();

int childWidthMeasureSpec;
int childHeightMeasureSpec;

childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft()

  • getPaddingRight(), lp.width);

childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

我们可以看到,传递给RecyclerView关于高度的测量模式是UNSPECIFIED。 接下来看看RecyclerView中的onMeasure():

protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
if (mLayout.mAutoMeasure) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
&& heightMode == MeasureSpec.EXACTLY;
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
if (skipMeasure || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
// set dimensions in 2nd step. Pre-layout should happen with old dimensions for
// consistency
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// now we can get the width and height from the children.
// 这行代码是重点
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);

// if RecyclerView has non-exact width and height and if there is at least one child
// which also has non-exact width & height, we have to re-measure.
if (mLayout.shouldMeasureTwice()) {
mLayout.setMeasureSpecs(
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
} else {
if (mHasFixedSize) {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
return;
}
// custom onMeasure
if (mAdapterUpdateDuringMeasure) {
eatRequestLayout();
onEnterLayoutOrScroll();
processAdapterUpdatesAndSetAnimationFlags();
onExitLayoutOrScroll();

if (mState.mRunPredictiveAnimations) {
mState.mInPreLayout = true;
} else {
// consume remaining updates to provide a consistent state with the layout pass.
mAdapterHelper.consumeUpdatesInOnePass();
mState.mInPreLayout = false;
}
mAdapterUpdateDuringMeasure = false;
resumeRequestLayout(false);
}

if (mAdapter != null) {
mState.mItemCount = mAdapter.getItemCount();
} else {
mState.mItemCount = 0;
}
eatRequestLayout();
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
resumeRequestLayout(false);
mState.mInPreLayout = false; // clear
}
}

这块代码的逻辑还是很清晰的,在mAutoMeasure属性为true时,除了RecyclerView没有精确的宽度和高度 + 至少有一个孩子也有不精确的宽度和高度的时候需要测量两次的时候,高度的测量模式为EXACTLY,其余都是调用mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec)来确定RecyclerView的大小。

关于mAutoMeasure属性什么时候为true,源码里的注释是这么说的:

/**

  • Defines whether the layout should be measured by the RecyclerView or the LayoutManager
  • wants to handle the layout measurements itself.
  • This method is usually called by the LayoutManager with value {@code true} if it wants
  • to support WRAP_CONTENT. If you are using a public LayoutManager but want to customize
  • the measurement logic, you can call this method with {@code false} and override
  • {@link LayoutManager#onMeasure(int, int)} to implement your custom measurement logic.
  • AutoMeasure is a convenience mechanism for LayoutManagers to easily wrap their content or
  • handle various specs provided by the RecyclerView’s parent.
  • It works by calling {@link LayoutManager#onLayoutChildren(Recycler, State)} during an
  • {@link RecyclerView#onMeasure(int, int)} call, then calculating desired dimensions based
  • on children’s positions. It does this while supporting all existing animation
  • capabilities of the RecyclerView.

知道你们的英语和我也是半斤八两,所以这里用大白话翻译一下,中心意思就是:**如果搭配RecyclerView的LayoutManager支持WRAP_CONTENT的属性时,这个值就应该为true
看到这里我相信你们又该有疑问了:
都有哪些LayoutManager支持WRAP_CONTENT属性呢?源码注释是这么说的:

/**

  • AutoMeasure works as follows:
    1. LayoutManager should call {@code setAutoMeasureEnabled(true)} to enable it. All of
    2. the framework LayoutManagers use {@code auto-measure}.

    3. */

    意思就是所用Android提供的原生的LayoutManager的mAutoMeasure属性都为true

    我们再来看下setMeasuredDimensionFromChildren方法。

    void setMeasuredDimensionFromChildren(int widthSpec, int heightSpec) {
    final int count = getChildCount();
    if (count == 0) {
    mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
    return;
    }
    int minX = Integer.MAX_VALUE;
    int minY = Integer.MAX_VALUE;
    int maxX = Integer.MIN_VALUE;
    int maxY = Integer.MIN_VALUE;

    for (int i = 0; i < count; i++) {
    View child = getChildAt(i);
    final Rect bounds = mRecyclerView.mTempRect;
    getDecoratedBoundsWithMargins(child, bounds);
    if (bounds.left < minX) {
    minX = bounds.left;
    }
    if (bounds.right > maxX) {
    maxX = bounds.right;
    }
    if (bounds.top < minY) {
    minY = bounds.top;
    }
    if (bounds.bottom > maxY) {
    maxY = bounds.bottom;
    }
    }

    自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

    深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

    因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
    img
    img
    img
    img
    img
    img
    img

    既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

    由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

    如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
    img

    最后为了帮助大家深刻理解Android相关知识点的原理以及面试相关知识,这里放上我搜集整理的2019-2021BATJ 面试真题解析,我把大厂面试中常被问到的技术点整理成了PDF,包知识脉络 + 诸多细节。

    节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

    《960全网最全Android开发笔记》

    《379页Android开发面试宝典》

    历时半年,我们整理了这份市面上最全面的安卓面试题解析大全
    包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

    如何使用它?

    1.可以通过目录索引直接翻看需要的知识点,查漏补缺。
    2.五角星数表示面试问到的频率,代表重要推荐指数

    《507页Android开发相关源码解析》

    只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

    真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

    腾讯、字节跳动、阿里、百度等BAT大厂 2019-2021面试真题解析

    资料太多,全部展示会影响篇幅,暂时就先列举这些部分截图

    -1711864447341)]

    《507页Android开发相关源码解析》

    只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

    真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

    [外链图片转存中…(img-9CpG5uLU-1711864447342)]

    腾讯、字节跳动、阿里、百度等BAT大厂 2019-2021面试真题解析

    [外链图片转存中…(img-MjevKuic-1711864447342)]

    资料太多,全部展示会影响篇幅,暂时就先列举这些部分截图

    本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值