【Android】ScrollView嵌套ListView只顯示第一行問題原理分析

一般狀況下咱們在使用ScrollView嵌套ListView的時候,當出現問題的時候,相信絕大部分人都是在網上直接找別人的解決方案,都沒有關心爲何會出現這種問題,爲何這樣解決。bash

一般狀況下ScrollView嵌套ListView會出現只會顯示ListView的第一個item的狀況,而網上大部分的解決方式是:ide

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

那麼爲何會出現這樣的狀況呢?爲何這樣解決呢? 咱們來一塊兒分析分析:ui

問題緣由

出現問題的時候咱們會發現只顯示了ListView的第一個item,而其餘item都是可滑動出來的,猜想是因爲onMeasure的問題致使的。spa

因此咱們先來看ScrollView的源碼:code

@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的高度。get

@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會出現顯示不全的問題。string

如何解決

經過源碼咱們知道了問題所在的緣由,那麼如何解決呢?it

這個時候就須要重寫ListView,並覆蓋onMeasure方法了io

  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的真實高度,這個時候問題也就解決了。

相關文章
相關標籤/搜索