面试之用源码的方式解释ScrollView 嵌套listview的解决方案

 虽然现在已经不使用这个东西,但是还是记录下来吧。去面试的时候 或会用到 !

从源码 的角度解释分析ScrollView 嵌套 ListView 时,为什么只会显示一个item。

解决方式有很多:通常用的解决方式是这样子的:

public class MyListView extends ListView {


    public MyListView(Context context) {
        super(context);
    }
    public MyListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public MyListView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
    @Override
    /**
     * 重写该方法,达到使ListView适应ScrollView的效果
     */
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
                MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }
}
百度很多正,都知道用这种方式就可以正常显示了,但是嵌套产生错的原因是什么呢?为什么这么解决呢?

接下来我们先看一下ScrollView的measureChildWithMargins()方法:


@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                    + widthUsed, lp.width);
    final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
            heightUsed;
    final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
            Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
            MeasureSpec.UNSPECIFIED);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
里面 明确定义了 childHeightMeasureSpec 的模式为/ MeasureSpec.UNSPECIFIED

关于MeasureSpec 一共四位字节,Android会用第一个高位字节存储mode,然后用剩余的三个字节存储size

       宽高都分为三种模式

         MeasureSpec.UNSPECIFIED:ViewGroup没有给View在尺寸上设置限制条件,这种情况下View可以忽略measureSpec中的size,View可以取自己想要的值作为量算的尺寸。通常系统才会使用,一般情况下不会使用

        MeasureSpec.EXACTLY:在布局中 指定了确切的值例如 30dp 等

        MeasureSpec.AT_MOST :在布局中指定了wrap_content

这就是我们我们 会出现问题的 所在地方了 我们再去看一下listView的 onMeasure方法,请注意代码里面的 中文注释!!!

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // Sets up mListPadding
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    //listView高度模式 由 heightMode决定,这个heightMeasureSpec 是ScrollView传递过来的,
    //他的具体高度是由heightSize决定 请看 本方法倒数 10多行的解释!!
    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 (widthMode == MeasureSpec.UNSPECIFIED) {
        widthSize = mListPadding.left + mListPadding.right + childWidth +
                getVerticalScrollbarWidth();
    } else {
        widthSize |= (childState & MEASURED_STATE_MASK);
    }

    if (heightMode == MeasureSpec.UNSPECIFIED) {
        //由于 ScrollView传递的是MeasureSpec.UNSPECIFIED,
        // 所以 listView的实际高度为一个item的高度,
        // 所以我们的解决方式是在改变传递的heightMode变为heightMode == MeasureSpec.AT_MOST
        // 就可以调用measureHeightOfChildren方法 
        heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                getVerticalFadingEdgeLength() * 2;
    }

    if (heightMode == MeasureSpec.AT_MOST) {
        //如果 重写,将 height
        // 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;
}
接下来 我们看一下measureHeightOfChildren方法

/**
 * Measures the height of the given range of children (inclusive) and
 * returns the height with this ListView's padding and divider heights
 * included. If maxHeight is provided, the measuring will stop when the
 * current height reaches maxHeight.
 *
 * @param widthMeasureSpec The width measure spec to be given to a child's
 *            {@link View#measure(int, int)}.
 * @param startPosition The position of the first child to be shown.
 * @param endPosition The (inclusive) position of the last child to be
 *            shown. Specify {@link #NO_POSITION} if the last child should be
 *            the last available child from the adapter.
 * @param maxHeight The maximum height that will be returned (if all the
 *            children don't fit in this value, this value will be
 *            returned).
 * @param disallowPartialChildPosition In general, whether the returned
 *            height should only contain entire children. This is more
 *            powerful--it is the first inclusive position at which partial
 *            children will not be allowed. Example: it looks nice to have
 *            at least 3 completely visible children, and in portrait this
 *            will most likely fit; but in landscape there could be times
 *            when even 2 children can not be completely shown, so a value
 *            of 2 (remember, inclusive) would be good (assuming
 *            startPosition is 0).
 * @return The height of this ListView with the given children.
 */
final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
        int maxHeight, int disallowPartialChildPosition) {
    final ListAdapter adapter = mAdapter;
    if (adapter == null) {
        return mListPadding.top + mListPadding.bottom;
    }

    // Include the padding of the list
    int returnedHeight = mListPadding.top + mListPadding.bottom;
    final int dividerHeight = mDividerHeight;
    // The previous height value that was less than maxHeight and contained
    // no partial children
    int prevHeightWithoutPartialChild = 0;
    int i;
    View child;

    // mItemCount - 1 since endPosition parameter is inclusive
    endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
    final AbsListView.RecycleBin recycleBin = mRecycler;
    final boolean recyle = recycleOnMeasure();
    final boolean[] isScrap = mIsScrap;

    for (i = startPosition; i <= endPosition; ++i) {
        child = obtainView(i, isScrap);

        measureScrapChild(child, i, widthMeasureSpec, maxHeight);

        if (i > 0) {
            // Count the divider for all but one child
            returnedHeight += dividerHeight;
        }

        // Recycle the view before we possibly return from the method
        if (recyle && recycleBin.shouldRecycleViewType(
                ((LayoutParams) child.getLayoutParams()).viewType)) {
            recycleBin.addScrapView(child, -1);
        }

        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;
        }

        if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
            prevHeightWithoutPartialChild = returnedHeight;
        }
    }

    // At this point, we went through the range of children, and they each
    // completely fit, so return the returnedHeight
    return returnedHeight;
}
这个代码 很简单了 ,就是用一个for循环将各个子View的高度测量出来,然后相加得到了最后的ListView的高度。。这下 是不是有点 清楚了。

我们 再看我们解决的方式:

 @Override
    /**
     * 重写该方法,达到使ListView适应ScrollView的效果
     */
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
                MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }

这个方法 很简单 重写了 listview的onMeasure方法,只增加了一句话,现在 只有一个 东西 没有说清楚了,就是MeasureSpec。

MeasureSpec.makeMeasureSpec 是构建一个 MeasureSpec。需要两个参数 一个size, 一个mode

/**
 * Creates a measure specification based on the supplied size and mode.
 *
 * The mode must always be one of the following:
 * <ul>
 *  <li>{@link android.view.View.MeasureSpec#UNSPECIFIED}</li>
 *  <li>{@link android.view.View.MeasureSpec#EXACTLY}</li>
 *  <li>{@link android.view.View.MeasureSpec#AT_MOST}</li>
 * </ul>
 *
 * <p><strong>Note:</strong> On API level 17 and lower, makeMeasureSpec's
 * implementation was such that the order of arguments did not matter
 * and overflow in either value could impact the resulting MeasureSpec.
 * {@link android.widget.RelativeLayout} was affected by this bug.
 * Apps targeting API levels greater than 17 will get the fixed, more strict
 * behavior.</p>
 *
 * @param size the size of the measure specification
 * @param mode the mode of the measure specification
 * @return the measure specification based on size and mode
 */
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                  @MeasureSpecMode int mode) {
    if (sUseBrokenMakeMeasureSpec) {
        return size + mode;
    } else {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}

为什么要传最大值并且需要 右移两位?这个问题是因为 如果我们要 走 heightMode == MeasureSpec.AT_MOST,会进入到

  if (heightMode == MeasureSpec.UNSPECIFIED) {
        //由于 ScrollView传递的是MeasureSpec.UNSPECIFIED,
        // 所以 listView的实际高度为一个item的高度,
        // 所以我们的解决方式是在改变传递的heightMode变为heightMode == MeasureSpec.AT_MOST
        // 就可以调用measureHeightOfChildren方法 
        heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                getVerticalFadingEdgeLength() * 2;
    }

    if (heightMode == MeasureSpec.AT_MOST) {
        //如果 重写,将 height
        // TODO: after first layout we should maybe start at the first visible position, not 0
        heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
    }
其中 有一个参数为heightSize ,是由listview的onMeasure方法中MeasureSpec.getSize(heightMeasureSpec);得到的 ,

int heightSize = MeasureSpec.getSize(heightMeasureSpec);
heightSize 的作用是 measureHeightOfChildren方法中 充当 maxHeight,

final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
        int maxHeight, int disallowPartialChildPosition) {

其中 在measureHeightOfChildren有这么一段 ,请看中文注释!!!!

for (i = startPosition; i <= endPosition; ++i) {
    child = obtainView(i, isScrap);

    measureScrapChild(child, i, widthMeasureSpec, maxHeight);

    if (i > 0) {
        // Count the divider for all but one child
        returnedHeight += dividerHeight;
    }

    // Recycle the view before we possibly return from the method
    if (recyle && recycleBin.shouldRecycleViewType(
            ((LayoutParams) child.getLayoutParams()).viewType)) {
        recycleBin.addScrapView(child, -1);
    }
    //由于在for 循环里这里的returnedHeight会累加,如果不传最大的值,
    // 有可能会出现returnedHeight >= maxHeight为true的情况,如果出现了会直接return ,
    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;
    }

    if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
        prevHeightWithoutPartialChild = returnedHeight;
    }
}

// At this point, we went through the range of children, and they each
// completely fit, so return the returnedHeight

return returnedHeight;


public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

还有最后一点,为什么要 右移两位?因为 MeasureSpec 是用 32位数字表示 ,前两位表述模式,后30位表示大小,所以 MODE_SHIFT 是用30位表示的,所以 Interger.max是32位表述的,需要右移两位,已得到一个30位最大的数字。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值