虽然现在已经不使用这个东西,但是还是记录下来吧。去面试的时候 或会用到 !
从源码 的角度解释分析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位最大的数字。