從源碼角度分析SrcollView嵌套ListView顯示不全的問題
問題描述
在之前開發(fā)的時(shí)候會(huì)碰到列表滑動(dòng)布局中ScrollView嵌套ListView的情況,當(dāng)嵌套了之后發(fā)現(xiàn)ListView只能顯示一行數(shù)據(jù)。碰到這種情況也是
于是乎開始Google,很快就找到解決方法,自定義view繼承自ListView,重寫onMeasure()方法,然后再加入一行代碼即可解決問題。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
But... why?
問題分析
首先應(yīng)該可以想到,ScrollView嵌套ListView,ListView相當(dāng)于ScrollView的子View了,我們應(yīng)該去看看父View如何去測(cè)量子View的,打開源碼先看ScrollView的onMeasure方法。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!mFillViewport) {
return;
}
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.UNSPECIFIED) {
return;
}
if (getChildCount() > 0) {
final View child = getChildAt(0);
final int widthPadding;
final int heightPadding;
final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
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 desiredHeight = getMeasuredHeight() - heightPadding;
if (child.getMeasuredHeight() < desiredHeight) {
final int childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec, widthPadding, lp.width);
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
desiredHeight, MeasureSpec.EXACTLY);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}
發(fā)現(xiàn)第一行就調(diào)用了父類的onMeasure方法,點(diǎn)進(jìn)去看看做了什么,ScrollView的父類是FrameLayout
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
}
截取了一部分代碼,看呀看,發(fā)現(xiàn)了一個(gè)很可疑的方法
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
看名稱是測(cè)量Child的,跟進(jìn),發(fā)現(xiàn)進(jìn)入了ViewGroup里面。
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 childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
一眼望去,感覺應(yīng)該在里面計(jì)算子View的寬高,并且把padding值和margin值都算進(jìn)去了。不管那么多了,ViewGroup是ScrollView的爺爺了吧,ScrollView里面應(yīng)該會(huì)重寫這個(gè)方法,因?yàn)镾crollView里面也能包裹其他的View,跟進(jìn)。
@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);
}
果然,這樣一來ListView被ScrollView包裹,它的寬高應(yīng)該是在這里被計(jì)算的,看看到底為啥只有一個(gè)條目的高度??吹竭@里
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
這里就是計(jì)算childHeight的地方,到這里我們就應(yīng)該去了解一下MeasureSpec
了.
MeasureSpec: 從字面意思翻譯像是測(cè)量規(guī)格,它在很大程度上決定了一個(gè)View的尺寸規(guī)則。實(shí)際上MeasureSpec
是一個(gè)32位的int值,高2位代表SpecMode(測(cè)量模式),低30位代表SpecSize(某種測(cè)量模式下的規(guī)格大?。?。重點(diǎn)就在于這個(gè)SpecMode
。
SpecMode: 測(cè)量模式有3種
- UNSPECFIED: 父容器不對(duì)View有任何限制,要多大給多大。這種情況一般用于系統(tǒng)內(nèi)部,表示一種測(cè)量狀態(tài)。
- EXACTLY: 父容器已經(jīng)檢測(cè)出View所需要的精確大小,這個(gè)時(shí)候View的最終大小就是SpecSize所指定的值。它對(duì)應(yīng)于LayoutParams中的match_parent和具體的數(shù)值這兩種模式。
- AT_MOST: 父容器指定了一個(gè)可用大小即SpecSize,View的大小不能大于這個(gè)值,具體是什么值要看不同View的具體實(shí)現(xiàn)。它對(duì)應(yīng)于LayoutParams中的wrap_content。
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
/** @hide */
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
在了解了測(cè)量模式之后,在看這段代碼
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);
在這里ScrollView對(duì)子View的高的測(cè)量模式都制定成了MeasureSpec.UNSPECIFIED
,也就是說,ListView要多高就給多高,這時(shí)候我們就可以去看看ListView到底要了多高。源碼轉(zhuǎn)入ListView,看看onMesaure()方法。
@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 (widthMode == MeasureSpec.UNSPECIFIED) {
widthSize = mListPadding.left + mListPadding.right + childWidth +
getVerticalScrollbarWidth();
} else {
widthSize |= (childState & MEASURED_STATE_MASK);
}
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;
}
看到這一段代碼
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
當(dāng)測(cè)量模式是MeasureSpec.UNSPECIFIED
的時(shí)候,我們只看到了 heightSize = ** + childHeight **..,oh my god, 難道ListView只向他爸爸要了一個(gè)孩子的高度,這就可以解釋清楚了為什么ScrollView嵌套ListView只顯示一行的高度了。
問題解決
解決問題的方法在開頭已經(jīng)給出了,但是為什么要這么做呢。
我們?cè)谟^察ListView的onMesaure方法的時(shí)候發(fā)現(xiàn),當(dāng)heightMode == MeasureSpec.UNSPECIFIED
時(shí),ListView只測(cè)量了一個(gè)child的高度,下面看看當(dāng)heightMode == MeasureSpec.AT_MOST
的時(shí)候會(huì)發(fā)生什么。
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);
}
看這個(gè)方法貌似又去測(cè)量每個(gè)孩子的高度,然后賦值給heightSize了,看看方法的具體實(shí)現(xiàn)。
/**
* 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.
*
* @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;
}
從注釋我們就可以看出,這個(gè)方法return
的是The height of this ListView with the given children.
也就是ListView包含孩子的高度??创a中
returnedHeight += child.getMeasuredHeight();
returnHeight也是在不斷的累加每個(gè)孩子的高度,所以最終會(huì)得到ListView的真實(shí)高度。
所以,現(xiàn)在的問題就是如何指定ListView的MeasureSpec.SpecMode == MeasureSpec.AT_MOST
再來看開頭的解決方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
makeMeasureSpec
方法就能重新指定測(cè)量規(guī)則,第一個(gè)參數(shù)提供一個(gè)30位的SpecSize,第二個(gè)參數(shù)提供一個(gè)2位的SpecMode,然后該方法將其合并成一個(gè)新的MeasureSpec.
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);
}
}
走完這個(gè)方法之后在調(diào)用父類的也就是ListView的onMeasure方法,重新獲取測(cè)量模式和測(cè)量尺寸,這次拿到的測(cè)量模式就是AT_MOST,就可以去測(cè)量每個(gè)孩子的高度累加了。
問題 為什么要使用Integer.MAX_VALUE
。
這是整型的最大值,很大很大有32位,我們?cè)谏厦?
measureHeightOfChildren
這個(gè)方法的時(shí)候,注意一段代碼
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;
}
return returnedHeight;
當(dāng)returnedHeight >= maxHeight的時(shí)候,它會(huì)返回一個(gè)我們并不想要的值,我們需要的是returnedHeight,所以我們不能讓這個(gè)判斷成立,就選擇了整型最大值。這個(gè)maxHeight追溯一下,就是我們?cè)O(shè)置的Integer.MAX_VALUE >> 2
, 右移兩位就剩下30位剛好和2位的模式配對(duì)。
結(jié)束語
此致 敬禮