Osheep

时光不回头,当下最重要。

ScrollView 与ListView 滑动冲突解决

转载请标明出处:http://www.jianshu.com/p/4cbe7515ef12
本文出自:http://www.jianshu.com/u/a1251e598483

首先说声抱歉,真的好久没有动笔写东西了,如果说是因为工作太忙,那是扯淡,还是因为懒,你看今年马上要过去一半了,感叹光阴似箭,岁月蹉跎,遂动笔写下最近遇到的问题,以及解决方案。

需求:

实现一个页面上整体内容可以上下滚动,页面的上半部分是一些固定内容,下半部分是一个列表,列表中每个item都可以点击展开,展开的部分内容条目不固定,默认隐藏,点击时展开。

这样的布局也很常见对吧,下面用listview 就可以了,外面用scrollView包裹就可以实现页面内容整体上下滚动。但是你会返现下面的listview不管有几条数据都会默认展示第一条数据,多条数据只能在一条item高度的范围内滚动。这样的效果显然不是我们想要的,那就要解决。

解决办法

首先要了解ScrollView的工作原理,常用它来布局一个内容超过当前页面,需要往下滑才能看到完整内容。可以把ScrollView看作是一种特殊的、加长版的LinearLayout,系统在加载布局时其实要知道整个布局的长度,才能将整个页面显示出来。而ListView的item个数是会动态变化的,如果在ScrollView中加入ListView会让系统无法准确加载,导致上述情况的发生。

你可能已经在网上搜索过其他解决方案了。
1:给listview 设置固定的高度。这个我看着就觉得不靠谱,如果你的listview的数据多少是个固定值,你经过计算后那个高度就是你固定listview的高度,那你完全可以用这个,但是这个对于我们这些数据条数不固定的就不能用这种方法了;
2:可以根据ListView中Item数量,动态设置ListView的height,使全部item得到显示。
就是一个方法,动态计算listView 的数目,根据高度,计算出总高度。

 /**
     * scrollview与listview合用会出现listview只显示一行多点。此方法是为了定死listview的高度就不会出现以上状况
     * 算出listview的高度
     */
    public static void setListViewHeight(ListView listView) {
        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(1, 1);
            totalHeight += listItem.getMeasuredHeight();
        }
        ViewGroup.LayoutParams params = listView.getLayoutParams();
        params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1)) + listView.getPaddingTop() + listView.getPaddingBottom();
        listView.setLayoutParams(params);
    }

这个方案呢,的确可以解决scrollView 里有listView ,整体上下滚动,listView 显示不全的问题,正常的要解决这个问题的看到这就行了,已经可以解决你的问题了。

但是没办法解决我的问题,不要忘了我的需求是listView 可以正常滚动,在点击每个item时动态展开查看详情。其实就是在每个item中隐藏了部分布局,根据点击item事件动态控制,这部分的item是Visible 还是Gone的事,由于详情的数据默认是隐藏的,在计算item的高度时是没有,也不能把隐藏的布局高度也计算在内的,所以这个方案不行。
3: 自定义 一个MyListView 继承listView ,重写里面的 onMeasure()方法

/**

 * Integer.MAX_VALUE >> 2,如果不设置,系统默认设置是显示两条
 */
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2,
            MeasureSpec.AT_MOST);
    super.onMeasure(widthMeasureSpec, expandSpec);
}

了解自定义view 的肯定知道 onMeasure()是什么意思,makeMeasureSpec()方法中 Integer.MAX_VALUE >> 2
在Android中,一个控件所占的模式和大小是通过一个整数int来表示的,这里很多同学就疑惑了,一个int值是怎么来表示模式的大小的,这里来看一张图片:

《ScrollView 与ListView 滑动冲突解决》

原来,Android里面把int的最高2两位来表示模式,最低30位来表示大小。

测量View大小使用的是onMeasure函数,我们可以从onMeasure的两个参数中取出宽高的相关数据:

   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthsize = MeasureSpec.getSize(widthMeasureSpec);      //取出宽度的确切数值
        int widthmode = MeasureSpec.getMode(widthMeasureSpec);      //取出宽度的测量模式
        int heightsize = MeasureSpec.getSize(heightMeasureSpec);    //取出高度的确切数值
        int heightmode = MeasureSpec.getMode(heightMeasureSpec);    //取出高度的测量模式
    }

从上面可以看出 onMeasure 函数中有 widthMeasureSpec 和 heightMeasureSpec 这两个 int 类型的参数, 毫无疑问他们是和宽高相关的, 但它们其实不是宽和高, 而是由宽、高和各自方向上对应的测量模式来合成的一个值:
测量模式一共有三种, 被定义在 Android 中的 View 类的一个内部类View.MeasureSpec中:

3种模式
1):UNSPECIFIED模式,官方意思是:父布局没有给子布局强加任何约束,子布局想要多大就要多大,说白了就是不确定大小
2)EXACTLY模式,官方意思是:父布局给子布局限定了准确的大小,子布局的大小就是精确的,父亲给多大就是多大
3)AT_MOST模式,官方意思是:父布局给定了一个最大的值,子布局的大小不能超过这个值,当然可以比这个值小

private static final int MODE_SHIFT = 30;

public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;

不确定模式是0左移30位,也就是int类型的最高两位是00
精确模式是1左移30位,也就是int类型的最高两位是01
最大模式是是2左移30位,也就是int类型的最高两位是10

所以调用了makeMeasureSpec方法,这个方法是用来生成一个带有模式和大小信息的int值的,第一个参数Integer.MAX_VALUE >> 2,这个参数是传的一个大小值,为什么是这个值呢,我们现在已经知道了,我们要生成的控件,它的大小最大值是int的最低30位的最大值,我们先取Integer.MAX_VALUE来获取int值的最大值,然后左移2位就得到这个临界值最大值了
当然,我们在手机上的控件的大小不可能那么大,极限值就那么大,实际肯定比那个小,所以这个模式就得选择MeasureSpec.AT_MOST了,最后将生成的这个大小传递给父控件就可以了,super.onMeasure(widthMeasureSpec, expandSpec),这个函数只改变的是控件的高度,宽度没有改变,实际开发当中不管listview有多少条数据,都能一次性展现出来。

最后选用第三种方式完美实现。

点赞