【Android】ScrollView嵌套ListView只显示第一行问题原理分析

通常情况下我们在使用ScrollView嵌套ListView的时候,当出现问题的时候,相信绝大部分人都是在网上直接找别人的解决方案,都没有关心为什么会出现这种问题,为什么这样解决。

通常情况下ScrollView嵌套ListView会出现只会显示ListView的第一个item的情况,而网上大部分的解决方式是:

@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
   super.onMeasure(widthMeasureSpec, expandSpec);
}

那么为什么会出现这样的情况呢?为什么这样解决呢?
我们来一起分析分析:

问题原因

出现问题的时候我们会发现只显示了ListView的第一个item,而其他item都是可滑动出来的,猜测是由于onMeasure的问题导致的。

所以我们先来看ScrollView的源码:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   super.onMeasure(widthMeasureSpec, heightMeasureSpec);

   final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
   if (heightMode == MeasureSpec.UNSPECIFIED) {
       return;
   }

   if (getChildCount() > 0) {
       final View child = getChildAt(0);
       final int height = getMeasuredHeight();
       if (child.getMeasuredHeight() < height) {
           final int widthPadding;
           final int heightPadding;
           final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
           final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
           if (targetSdkVersion >= VERSION_CODES.M) {
               widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
               heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
           } else {
               widthPadding = mPaddingLeft + mPaddingRight;
               heightPadding = mPaddingTop + mPaddingBottom;
           }

           final int childWidthMeasureSpec = getChildMeasureSpec(
                   widthMeasureSpec, widthPadding, lp.width);
           final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                   height - heightPadding, MeasureSpec.EXACTLY);
           child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
       }
   }
}

通过源码可以看到ScrollView在测量的时候只拿了子View中的第一个,也就是ListView的高度。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   // Sets up mListPadding
   super.onMeasure(widthMeasureSpec, heightMeasureSpec);

   final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
   final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
   int widthSize = MeasureSpec.getSize(widthMeasureSpec);
   int heightSize = MeasureSpec.getSize(heightMeasureSpec);

   int childWidth = 0;
   int childHeight = 0;
   int childState = 0;

   mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
   if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
           || heightMode == MeasureSpec.UNSPECIFIED)) {
       final View child = obtainView(0, mIsScrap);

       // Lay out child directly against the parent measure spec so that
       // we can obtain exected minimum width and height.
       measureScrapChild(child, 0, widthMeasureSpec, heightSize);

       childWidth = child.getMeasuredWidth();
       childHeight = child.getMeasuredHeight();
       childState = combineMeasuredStates(childState, child.getMeasuredState());

       if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
               ((LayoutParams) child.getLayoutParams()).viewType)) {
           mRecycler.addScrapView(child, 0);
       }
   }
   // 代码省略

   if (heightMode == MeasureSpec.UNSPECIFIED) {
       heightSize = mListPadding.top + mListPadding.bottom + childHeight +
               getVerticalFadingEdgeLength() * 2;
   }

   if (heightMode == MeasureSpec.AT_MOST) {
       // TODO: after first layout we should maybe start at the first visible position, not 0
       heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
   }

   setMeasuredDimension(widthSize, heightSize);

   mWidthMeasureSpec = widthMeasureSpec;
}

通过ListView的onMeasure方法,我们看到:

  1. 首先判断itemCount是否大于0,如果大于0,计算第一个item的高度
  2. 由于heightMode是MeasureSpec.UNSPECIFIED,所以ListView的高度就是第一个item的高度加上相关的参数

所以这就得出了为什么ScrollView嵌套ListView会出现显示不全的问题。

如何解决

通过源码我们知道了问题所在的原因,那么如何解决呢?

这个时候就需要重写ListView,并覆盖onMeasure方法了

  1. 首先我们知道系统的mode是MeasureSpec.UNSPECIFIED,而我们又发现:

    if (heightMode == MeasureSpec.AT_MOST) {
    // TODO: after first layout we should maybe start at the first visible position, not 0
    heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
    

    ListView当mode是MeasureSpec.AT_MOST的时候会依次累计计算每个item的高度,所以我们需要设置mode为MeasureSpec.AT_MOST

  2. 那么为什么要设置高度为Integer.MAX_VALUE >> 2

    final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
       int maxHeight, int disallowPartialChildPosition) {
        
        returnedHeight += child.getMeasuredHeight();
    
       if (returnedHeight >= maxHeight) {
           // We went over, figure out which height to return.  If returnedHeight > maxHeight,
           // then the i'th position did not fit completely.
           return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
                       && (i > disallowPartialChildPosition) // We've past the min pos
                       && (prevHeightWithoutPartialChild > 0) // We have a prev height
                       && (returnedHeight != maxHeight) // i'th child did not fit completely
                   ? prevHeightWithoutPartialChild
                   : maxHeight;
       }
    }
    

    在计算高度的时候ListView会累加每个item的高度,并最终和maxHeight比较,也就是Integer.MAX_VALUE >> 2,当小于maxHeight的时候,就直接返回ListView的真实高度,这个时候问题也就解决了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容