ListView嵌套显示不全的原因

问题所在

在一个ViewGroup里面放置多个ViewGroup本身就是有风险的,而常用的ListViewGridViewScrollView就成了风险高发地。
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始终是起作用的方法之一。

原因

复习一下MeasureSpecModeMeasureSpec的前两位,代表了测量模式。有三个常量值。

	    /**
         * 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时,父ListViewheightMode应该为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方法中确实找不出端倪了,得看另一段代码,具体流程可自己查看源码,这里只提一下,
ListViewGridView会覆写父类AbsListViewlayoutChildrenAbsListView在执行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,逃不过onLayoutonLayout时,item的高度如非精确值,都被强行MeasureSpec.UNSPECIFIED,于是子ListView顺理成章又只测量了第一个item的高度。

理一下,
当父ListView高度设为wrap_content时,依次经由
ListViewParent.onMeasure
ListViewParent.measureHeightOfChildren
ListViewParent.measureScrapChild
ListViewSon.onMeasure(MeasureSpec.UNSPECIFIED)

Created with Raphaël 2.2.0 START ListView.onMeasure UNSPECIFIED? measureScrapChild(child, 0) END AT_MOST? measureHeightOfChildren measureScrapChild lp.height>0? heightMode=UNSPECIFIED yes no yes no

当父ListView高度设为match_parent或200dp(代指精确值)时,依次经由
ListViewParent.onMeasure
ListViewParent.setupChild
ListViewSon.onMeasure(MeasureSpec.UNSPECIFIED)

Created with Raphaël 2.2.0 START ListView.onMeasure UNSPECIFIED? measureScrapChild(child, 0) END AT_MOST? setupChildren lp.height>0? heightMode=UNSPECIFIED yes no no no

由此也可以看出来,改变父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?

以上。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值