从源码角度分析ScrollView嵌套ListView显示不全问题

  1. 简介
    在我们开发项目过程中,我们在座绝大部分的人一定都遇到过ScrollVIew嵌套ListView显示不全,只显示一个item高度的问题,当然,你如果说你没有遇到过也是没有问题的。这种现象是很常见的,网上一搜基本解决方法一般有这样几种,
    1.1 动态设置listview,去测量每个item高度,通过for循环叠加计算listview的总高度
    1.2 使用LinearLayout代替ListView
    1.3 自定义MyListView直接集成系统的ListView,重写onMeasure()方法
    等等...

  2. 实现
    接下来带大家具体分析下这几种方式的具体使用,前两种使我们这节课的一个小插曲,待会看下其实现方式即可,今天我们重点带大家看下第三种实现方式,它为什么要这样去写

    方式一: 动态设置listview,去测量每个item高度,通过for循环叠加计算listview的总高度
    我们大家都知道,在布局文件中如果直接将item高度写死,是可以解决这个问题的,但是我们也都知道,listview高度随着数据是可变化,实际高度还需要实际去测量,那么既然这样,我们就可以手动的去计算ListView的高度了,代码如下,直接作为工具类拷贝到项目中即可使用

/**
     * Describe: 动态设置listview,去测量每个item高度,通过for循环叠加计算listview的总高度
     * 

* Author: Jack-Chen *

* Time 16/9/27 下午4:28 */ public class ListViewUtil { public static void adaptiveHight(Context context,ListView listView,float dividerHeight) { try { ListAdapter listAdapter = listView.getAdapter(); if (listAdapter == null) { return; } int totalHeight = 0; for (int i = 0; i < listAdapter.getCount(); i++) { View listItem = listAdapter.getView(i, null, listView); listItem.measure(0, 0); totalHeight += listItem.getMeasuredHeight(); } ViewGroup.LayoutParams params = listView.getLayoutParams(); if (dividerHeight != -1) { totalHeight += UIHelper.dip2px(context, dividerHeight) * (listAdapter.getCount() - 1); } params.height = totalHeight; listView.setLayoutParams(params); }catch (Exception ex){ ex.printStackTrace(); } } public static int getItemsHight(ListView listView) { ListAdapter listAdapter = listView.getAdapter(); if (listAdapter == null) { return 0; } int totalHeight = 0; for (int i = 0; i < listAdapter.getCount(); i++) { View listItem = listAdapter.getView(i, null, listView); listItem.measure(0, 0); totalHeight += listItem.getMeasuredHeight(); } return totalHeight; } public static int getItemHight(ListView listView) { ListAdapter listAdapter = listView.getAdapter(); if (listAdapter == null) { return 0; } int itemHeight = 0; if(listAdapter.getCount()>0) { View listItem = listAdapter.getView(0, null, listView); listItem.measure(0, 0); itemHeight= listItem.getMeasuredHeight(); } return itemHeight; } }

方式二: 使用LinearLayout代替ListView
既然listview不能适应ScrollView,那么我们完全可以找一个可以适应ScrollView的控件来代替ListView,此时LinearLayout是最好的选择,但如果我们还想继续使用已经定义好的adapter,那么我们只需要定义一个类去继承LinearLayout,最后为其适配BaseAdapter即可
具体代码如下:
2.2.1: 自定义LinearLayoutForListView 继承LinearLayout

/**
     * Describe: 使用LinearLayout代替ListView
     * 

* Author: Jack-Chen *

* Time 16/5/27 下午3:45 */ public class LinearLayoutForListView extends LinearLayout { private BaseAdapter adapter; private OnClickListener onClickListener = null; /** * 绑定布局 */ public void bindLinearLayout() { int count = adapter.getCount(); this.removeAllViews(); for (int i = 0; i < count; i++) { View v = adapter.getView(i, null, null); v.setOnClickListener(this.onClickListener); addView(v, i); } Log.v("countTAG", "" + count); } public LinearLayoutForListView(Context context) { super(context); }

2.2.2: 将自己之前的ListView布局文件替换为这个包下的布局文件


2.2.3 : 然后去替换Activity或Fragment中之前ListView的控件为LinearLayoutForListView,最后为其setAdapter适配数据即可

方式三:自定义MyListView直接继承系统的ListView,重写onMeasure()方法

/**
    * Describe: 自定义MyListView直接继承系统的ListView
    * 

* Author: Jack-Chen *

* Time 16/8/27 下午2:40 */ 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 defStyleAttr) { super(context, attrs, defStyleAttr); } /** * 解决ScrollView嵌套ListView显示不全问题 * @param widthMeasureSpec * @param heightMeasureSpec */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //heightMeasureSpec 参数1是32位的值 右移2位变成30位的值, MeasureSpec.AT_MOST是模式,ListView源码中应该要执行MeasureSpec.AT_MOST这个if // 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); // } //而不能让执行这个if,因为这个if里边刚好是listview的1个item的高度 // if (heightMode == MeasureSpec.UNSPECIFIED) { // heightSize = mListPadding.top + mListPadding.bottom + childHeight + // getVerticalFadingEdgeLength() * 2; // } heightMeasureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE>>2 , MeasureSpec.AT_MOST) ; super.onMeasure(widthMeasureSpec, heightMeasureSpec); } }

这个方法我相信绝大部分的人都是采用这用方式的,因为这种方式相对来说比较简单,直接拷贝过去用即可,但是为什么要这样去写,重写onMeasure()方法后,里边的参数为什么是右移2位,然后模式给他设置为MeasureSpec.AT_MOST呢,接下来我给大家来分析下,为什么这样去写,大神可以跳过哈

继承ListView后,大家可以直接点击super.onMeasure(widthMeasureSpec, heightMeasureSpec);进入ListView的源码,可以看到

    @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); //获取后面30位
        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;
    }

在这里要给大家说一下,widthMeasureSpec和heightMeasureSpec分别都包含了2个信息
Integer.MAX_VALUE是一个32位的值,右移两位会将Integer.MAX_VALUE变为一个30位的值,最后两位就是MeasureSpec.AT_MOST

那么由这个可知:

       final int widthMode = MeasureSpec.getMode(widthMeasureSpec);   //获取前两位
       final int heightMode = MeasureSpec.getMode(heightMeasureSpec); //获取前两位
      //获取宽高的值
       int widthSize = MeasureSpec.getSize(widthMeasureSpec); //获取后面30位
       int heightSize = MeasureSpec.getSize(heightMeasureSpec);

heightMode 就是MeasureSpec.AT_MOST
heightSize 就是Integer.MAX_VALUE>>2
因为高度heightMode传递的是MeasureSpec.AT_MOST,所以就只会进到if (heightMode == MeasureSpec.AT_MOST)中,而不会进到heightMode == MeasureSpec.UNSPECIFIED中

为什么使用Integer.MAX_VALUE>>2:
这个Integer.MAX_VALUE是表示32位的一个值,然后右移两位,表示有30为的值,表示大小,后边的MeasureSpec.AT_MOST是表示model,模式

为什么使用MeasureSpec.AT_MOST:
大家可以看到源码是重写onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法,其他的我们可以不去看他,直接看里边有2个if判断高度的,宽度的不需要看,其中第一个if是判断高度的模式 heightMode == MeasureSpec.UNSPECIFIED,里边代码表示距离上边+距离下边+子高度刚好表示一个item的高度,那么现在我们再来回过头想下,之前ScrollView嵌套ListView只显示一条,我们猜想它应该是走的这个if判断里边,我们要做的就是不要让它执行这个if,而是要让它执行下边的if (heightMode == MeasureSpec.AT_MOST)判断,而这里的判断可以直接点击进去heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);看下
而且它传递的也是上边获取的高度heightSize,到这里我们就知道为什么第二个参数是
MeasureSpec.AT_MOST,如果我们不重写onMeasure()方法,其实它里边的高度heightMeasureSpec默认是执行heightMode == MeasureSpec.UNSPECIFIED,所以高度才会显示不全

如果你觉得有帮助,可以关注我,我会持续更新博客,会将自己项目中遇到的问题、遇到的bug、以及解决方法都会分享出来,也许可能像我这样的文章或者解决方法网上一搜都有,不过也没有关系,自己觉得还是写出来会比较踏实,因为这些都是自己用过的、思考过的一些东西,还是觉得蛮有用的

你可能感兴趣的:(从源码角度分析ScrollView嵌套ListView显示不全问题)