从源码角度分析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、以及解决方法都会分享出来,也许可能像我这样的文章或者解决方法网上一搜都有,不过也没有关系,自己觉得还是写出来会比较踏实,因为这些都是自己用过的、思考过的一些东西,还是觉得蛮有用的

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,340评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,762评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,329评论 0 329
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,678评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,583评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,995评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,493评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,145评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,293评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,250评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,267评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,973评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,556评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,648评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,873评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,257评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,809评论 2 339