在一个ViewGroup里面放置多个ViewGroup本身就是有风险的,而常用的ListView、GridView、ScrollView就成了风险高发地。
ListView中嵌套ListView,高度失衡…
ListView中嵌套GridView,高度失衡…
GridView中嵌套GridView,高度失衡…
GridView中嵌套ListView,高度失衡…
ScrollView中嵌套ListView,高度失衡…
ScrollView中嵌套GridView,高度失衡…
…
不用挣扎了,统统翻车…
当前最简洁的解决方案,还是覆写onMeasure方法,重中之中是改变其MeasureSpecMode,使其传进正确的MeasureSpec.AT_MOST。
public class ListViewForScrollView extends ListView {
public ListViewForScrollView(Context context) {
super(context);
}
public ListViewForScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ListViewForScrollView(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//给一个较大值即可,不一定为Integer.MAX_VALUE >> 2
int heightMeasureSpecNew = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightMeasureSpecNew);
}
}
无论是ListView、GridView、ScrollView或是其他什么,覆写onMeasure始终是起作用的方法之一。
复习一下MeasureSpecMode:MeasureSpec的前两位,代表了测量模式。有三个常量值。
/**
* MODE_SHIFT = 30,未指定大小
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;//值为0
/**
* MODE_SHIFT = 30,指定大小
*/
public static final int EXACTLY = 1 << MODE_SHIFT;//值为1073741824
/**
* MODE_SHIFT = 30,指定最大值
*/
public static final int AT_MOST = 2 << MODE_SHIFT;//值为-2147483648
再复习一下LayoutParams:布局参数,封装了Layout的位置、高、宽等。也有三个常量值 。
@SuppressWarnings({"UnusedDeclaration"})
@Deprecated
public static final int FILL_PARENT = -1;
public static final int MATCH_PARENT = -1;
public static final int WRAP_CONTENT = -2;
那么现在,关门,放源码:
//ListView.java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
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);
/**
* 显示不全的原因所在,所有父ListView中的子ListView进行onMeasure时,
* 都会因为上层ListView传进来heightMode == MeasureSpec.UNSPECIFIED而进
* 入此判断中,导致高度只有一个item的高度
*/
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) {
// 当子ListView能进入此判断时,高度即可判断正确
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
setMeasuredDimension(widthSize, heightSize);
mWidthMeasureSpec = widthMeasureSpec;
}
当父ListView的高度设置为wrap_content时,父ListView的heightMode应该为MeasureSpec.AT_MOST,那么为什么经过measureHeightOfChildren方法传递到子ListView所在布局时,就发生了一些变化呢?
继续放源码:
//ListView.java
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;
}
int returnedHeight = mListPadding.top + mListPadding.bottom;
final int dividerHeight = ((mDividerHeight > 0) && mDivider != null) ? mDividerHeight : 0;
int prevHeightWithoutPartialChild = 0;
int i;
View child;
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);
//重点,测量每个item的大小
measureScrapChild(child, i, widthMeasureSpec, maxHeight);
if (i > 0) {
returnedHeight += dividerHeight;
}
//略过部分代码
returnedHeight += child.getMeasuredHeight();
}
//略过部分代码
return returnedHeight;
}
看来原因应该在measureScrapChild里面,继续:
//ListView.java
private void measureScrapChild(View child, int position, int widthMeasureSpec, int heightHint) {
LayoutParams p = (LayoutParams) child.getLayoutParams();
if (p == null) {
p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
child.setLayoutParams(p);
}
p.viewType = mAdapter.getItemViewType(position);
p.forceAdd = true;
final int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
mListPadding.left + mListPadding.right, p.width);
final int lpHeight = p.height;
final int childHeightSpec;
/**
* 重点,罪魁祸首出现了,还记得LayoutParams的常量值么?
* 在match_parent和wrap_content的情况下,lpHeight显然是小于0的,当前
* lp.height=-2,然后子View的heightMode变成了MeasureSpec.UNSPECIFIED,
* 只有当item即子ListView的高度设为某个准确的dp值时,才能避免UNSPECIFIED
*/
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeSafeMeasureSpec(heightHint, MeasureSpec.UNSPECIFIED);
}
//子View被传进了MeasureSpec.UNSPECIFIED
child.measure(childWidthSpec, childHeightSpec);
child.forceLayout();
}
问题就明显了,
当父ListView的height为wrap_content时,
经由measureHeightOfChildren–>
measureScrapChild–>
child.measure–>
向子ListView所在布局传进了MeasureSpec.UNSPECIFIED,所以子ListView进入onMeasure时,只测量了第一个item的高度:
即仅调用了measureScrapChild(child, 0, widthMeasureSpec, heightSize)方法,并将其作为整个ListView的高度。
借用上面发过的源码片段:
if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
|| heightMode == MeasureSpec.UNSPECIFIED)) {
final View child = obtainView(0, mIsScrap);
/**
* 显示不全的原因所在,所有父ListView中的子ListView进行onMeasure时,
* 都会因为上层ListView传进来heightMode == MeasureSpec.UNSPECIFIED而进
* 入此判断中,导致高度只有一个item的高度
*/
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);
}
}
(子ListView所在布局会经由类似measureChildWithMargins的方法,将子布局从父视图得到的widthMeasureSpec、heightMeasureSpec继续传递给其子控件,包括了子ListView,为了说法简便,后面不再称为子ListView所在布局,而直接称子ListView;例如父ListView的Item根布局为LinearLayout,LinearLayout会经由onMeasure–>measureChildBeforeLayout–>measureChildWithMargins将参数传递给其布局内的子控件)
那我不用wrap_content,我用match_parent或者200dp呢?
在onMeasure方法中确实找不出端倪了,得看另一段代码,具体流程可自己查看源码,这里只提一下,
ListView及GridView会覆写父类AbsListView的layoutChildren,AbsListView在执行onLayout方法时,会执行layoutChildren方法。
//AbsListView.java
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
mInLayout = true;
final int childCount = getChildCount();
if (changed) {
for (int i = 0; i < childCount; i++) {
getChildAt(i).forceLayout();
}
mRecycler.markChildrenDirty();
}
//重点,此处将操作其子控件
layoutChildren();
mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
if (mFastScroll != null) {
mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
}
mInLayout = false;
}
而ListView经由layoutChildren–>fillFromTop/fillUp/fillSpecific/…–>makeAndAddView–>setupChild,最终来到setupChild方法。
//ListView.java
private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft, boolean selected, boolean recycled) {
//略过部分代码
//重点,此处获取item的布局,下面的判断依然与之有关
AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
if (p == null) {
p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
}
p.viewType = mAdapter.getItemViewType(position);
/**
* 如果前面测量过,则为false,明显为wrap_content时进入了measureScrapChild方法测量
* 过,但为match_parent时,并未测量,为true
*/
if (needToMeasure) {
final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
mListPadding.left + mListPadding.right, p.width);
final int lpHeight = p.height;
final int childHeightSpec;
/**
* 重点,当item高度为wrap_content或match_parent时,其lpHeight是小于0的,
* 只有为精确值时,才能准确测量,所以当ListView等控件嵌套时,
* 只有指定父ListView的item根布局的高度,才有机会让子视图显示完全
*/
if (lpHeight > 0) {
childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
} else {
childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(),
MeasureSpec.UNSPECIFIED);
}
child.measure(childWidthSpec, childHeightSpec);
} else {
cleanupLayoutState(child);
}
//略过部分代码
}
可以看到,逃过了onMeasure,逃不过onLayout,onLayout时,item的高度如非精确值,都被强行MeasureSpec.UNSPECIFIED,于是子ListView顺理成章又只测量了第一个item的高度。
理一下,
当父ListView高度设为wrap_content时,依次经由
ListViewParent.onMeasure
ListViewParent.measureHeightOfChildren
ListViewParent.measureScrapChild
ListViewSon.onMeasure(MeasureSpec.UNSPECIFIED)
当父ListView高度设为match_parent或200dp(代指精确值)时,依次经由
ListViewParent.onMeasure
ListViewParent.setupChild
ListViewSon.onMeasure(MeasureSpec.UNSPECIFIED)
由此也可以看出来,改变父ListView的高度属性,是解决不了此问题的,只有干预ListViewSon的measure过程,才能解决此问题。
接下来看GridView,情景大概类似,不再赘述,直接放源码:
//GridView.java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Sets up mListPadding
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//...略过部分代码
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int childWidth = widthSize - mListPadding.left - mListPadding.right;
boolean didNotInitiallyFit = determineColumns(childWidth);
int childHeight = 0;
int childState = 0;
mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
final int count = mItemCount;
if (count > 0) {
final View child = obtainView(0, mIsScrap);
//...略过部分代码
/**
* 重点,此处为item封装了一个MeasureSpec.UNSPECIFIED高度,
* 可以看到GridView甚至都没有更强的判断条件,
* 直接向子控件传递了MeasureSpec.UNSPECIFIED
*/
int childHeightSpec = getChildMeasureSpec(
MeasureSpec.makeSafeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec),
MeasureSpec.UNSPECIFIED), 0, p.height);
int childWidthSpec = getChildMeasureSpec(
MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width);
//传进去了,如果child是ListView或者GridView,结果可想而知
child.measure(childWidthSpec, childHeightSpec);
//...略过部分代码
}
//...略过部分代码
setMeasuredDimension(widthSize, heightSize);
mWidthMeasureSpec = widthMeasureSpec;
}
最后来看ScrollView,先看看onMeasure:
//ScrollView.java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
/**
* 重点,此属性默认为false,
* 所以在不设置的情况下,onMeasure会到此为止,只会执行super.onMeasure;
* mFillViewport置为true后,子控件被允许铺满全屏,
* 但也仅此而已,嵌套ListView时,也只能显示一屏的Item且无法再滑动
*/
if (!mFillViewport) {
return;
}
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);
}
}
}
乍看之下,向child传入了MeasureSpec.EXACTLY,事实上根本没有运行到此处,只运行了super.onMeasure;
ScrollView继承自FrameLayout,FrameLayout继承自ViewGroup,我们知道ViewGroup中定义了measureChildren, measureChild, measureChildWithMargins来对子视图进行测量;
事实上,就是ScrollView覆写了measureChild改变了测绘结果:
//ScrollView.java
@Override
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
ViewGroup.LayoutParams lp = child.getLayoutParams();
int childWidthMeasureSpec;
int childHeightMeasureSpec;
childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft
+ mPaddingRight, lp.width);
//重点,此处向子控件传入了MeasureSpec.UNSPECIFIED
childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
MeasureSpec.getSize(parentHeightMeasureSpec), MeasureSpec.UNSPECIFIED);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
(不过也有一个相关的问题存在,ViewGroup的measureChildren调用了measureChild,那么又是哪里调用了measureChildren?)
以上。