ScrollView中的那些嵌套坑

ScrollView嵌套ListView,如果不做任何处理的情况下,一般Listview只会出现一项,这是因为ScrollView无法获取ListView的正常高度

布局示例
//布局情况一

  
  

//布局情况二

  
   ...
   
   ...
  
  

上面两种情况是我们在平时的使用中比较容易见到的情况,尤其是情况二。

先说如何解决问题:

第一种方法重写ListView中的onMeasure()方法

@Override 
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec,
        MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE>>2,MeasureSpec.AT_MOST));
}

第二种方式手动去计算ListView中每个Item的高度,然后将这个高度传给ListView,让ScrollView知道自己的子布局有多高,这样的话ListView就会正常显示了,这个方法在ListView.setAdapter()后调用即可。

public void measureListViewChildHeight(){
        ArrayAdapter listAdapter = (ArrayAdapter) mListView.getAdapter();
        if (listAdapter == null)
            return;
        int desiredWidth = View.MeasureSpec.makeMeasureSpec(mListView.getWidth(), View.MeasureSpec.UNSPECIFIED);
        int totalHeight = 0;
        View view = null;
        for (int i = 0; i < listAdapter.getCount(); i++) {
            //获取每个item的view
            view = listAdapter.getView(i, view, mListView);
            if (i == 0)
                view.setLayoutParams(new AbsListView.LayoutParams(desiredWidth, AbsListView.LayoutParams.WRAP_CONTENT));
            view.measure(desiredWidth, View.MeasureSpec.UNSPECIFIED);
            //计算总高度
            totalHeight += view.getMeasuredHeight();
        }
        ViewGroup.LayoutParams params = mListView.getLayoutParams();
        //加上divider的高度
        params.height = totalHeight + (mListView.getDividerHeight() * (listAdapter.getCount() - 1));
        mListView.setLayoutParams(params);
        mListView.requestLayout();
    }

注意:第二种解决方法不适用布局情况一,亲测在布局情况一下,ScrollView的getMeasureHeight()为0或者负数,这样的话ListView只能显示出一行的数据,虽然你正确的设置了ListView的LayoutParams,但是经过测试ListView.getMeasureHeight()的值为一行数据的高度,猜想是因为ListView在onMeasure()时获得一行Item的高度后就把设置死了,有一行Item的高度就能显示数据了,而且还可以滚动。

那为什么会产生这种情况?

我们打开ScrollView的源码,在最前面的类说明中我们看到了,官方都不建议我们使用这种嵌套行为,因为这样会导致ListView的所有重要优化失败,以处理大型列表,因为它有效地强制ListView显示其完整的项目列表,以填补ScrollView提供的无限容器。还会有滑动冲突等等问题

* Layout container for a view hierarchy that can be scrolled by the user,
* allowing it to be larger than the physical display. A ScrollView
* is a {@link FrameLayout}, meaning you should place one child in it
* containing the entire contents to scroll; this child may itself be a layout
* manager with a complex hierarchy of objects. A child that is often used
* is a {@link LinearLayout} in a vertical orientation, presenting a vertical
* array of top-level items that the user can scroll through.

* You should never use a ScrollView with a {@link ListView}, because
* ListView takes care of its own vertical scrolling. Most importantly, doing this
* defeats all of the important optimizations in ListView for dealing with
* large lists, since it effectively forces the ListView to display its entire
* list of items to fill up the infinite container supplied by ScrollView.
  
* The {@link TextView} class also
* takes care of its own scrolling, so does not require a ScrollView, but
* using the two together is possible to achieve the effect of a text view
* within a larger container.

我们来找找罪魁祸首ScrollView的onMeasure方法

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        if (!mFillViewport) {
            return;
        }

        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (heightMode == MeasureSpec.UNSPECIFIED) {
            return;
        }

        if (getChildCount() > 0) {
            final View child = getChildAt(0);
            int height = getMeasuredHeight();
            if (child.getMeasuredHeight() < height) {
                final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();

                int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                        mPaddingLeft + mPaddingRight, lp.width);
                height -= mPaddingTop;
                height -= mPaddingBottom;
                int childHeightMeasureSpec =
                        MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);

                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }
    }

从源码可以发现,ScrollView的高度是由它的子View决定的,而且它的子View的测量模式为MeasureSpec.EXACTLY模式。

从Android开发艺术探索一书由讲到View的测量模式,SepcMode有三种,每一种都有其特殊意义

  • UNSPECIFIED 父容器不对View有任何限制,要多大给多大,这种情况一般用于系统内部,表示一种测量的状态
  • EXACTLY 父容器已经检测出View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的值。它对于LayoutParams中的match_parant和具体数值两种情况
  • AT_MOST 父容器指定了一个可用大小即SpecSize,View的值不能大于这个值,具体是什么值要看不同View的具体实现。他对应于LayoutParams中的wrap_content

在这个模式下父布局需要知道子布局的精确高度,这样的话才能正常显示出来,想想这种做法也是挺合理的,毕竟这是一个滚动布局,整个布局的高度就像画布一样肯定是精确的,不然怎么滚动显示呢。但是也正是由于这种做法导致了如果子布局的高度不是什么精确值会导致各种显示异常。

我们在解法一的方法中也用到了AT_MOST,解法一的原理就是给定了ListView的最大值为INTEGER.MAX_VALUE>>2的大小,让它在这个范围内随便折腾。

你以为有了这两种解决方法就万事大吉了吗?

在上周我们项目中还是出现了ListView的高度显示异常,这次是最后两项的高度显示异常,我们的ListView的基类已经指定了AT_MOST模式还是出现了问题,可见问题并不像我们想象的那么简单。

//项目中的布局

  
    
     ...
     
     ...
    
  
  

问题分析:既然大部分的Item都已经显示出来了,只差最后最后两项的高度,那么极有可能是在测量高度的过程中出现了测量高度有偏差,所有的偏差值加起来正好等于最后两项的高度值。最后将Item的高度设置为精确值,发现就不会出现问题了。正好证明我的猜想是正确的。

总结

在实际开发中,我们还是要尽量去避免使用这种嵌套,毕竟官方都不建议我们去这样使用,而且还是有很多坑等着我们去踩。所以我们可以完全使用一个ListView配合多种布局,或者使用RecycleView去实现我们想要的功能.

你可能感兴趣的:(ScrollView中的那些嵌套坑)