从源码角度分析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的总高度
     * <p>
     * Author: Jack-Chen
     * <p>
     * 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
     * <p>
     * Author: Jack-Chen
     * <p>
     * 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布局文件替换为这个包下的布局文件

<com.jackchen.LinearLayoutForListView 
    android:id="@+id/act_solution_3_mylinearlayout"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical" >

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

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

/**
    * Describe: 自定义MyListView直接继承系统的ListView
    * <p>
    * Author: Jack-Chen
    * <p>
    * 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、以及解决方法都会分享出来,也许可能像我这样的文章或者解决方法网上一搜都有,不过也没有关系,自己觉得还是写出来会比较踏实,因为这些都是自己用过的、思考过的一些东西,还是觉得蛮有用的

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。