ListView的item展开后完整显示

来源:互联网 发布:锋利的jquery源码 编辑:程序博客网 时间:2024/04/30 22:13

手机屏幕毕竟有限,当我们要显示较多数据时便不得不舍去一些次要信息,将主要信息优先显示,也使显示效果更加简洁美观。遇到类似的需求,我们使用最多的就是 ListView ,而如果每次点击一个 Item 都要跳转到下一页查看详情,查看另一个时还要返回列表重新进入另一条详情,使得操作繁琐体验降低。此时可隐藏和展开 Item 的 ListView 便应运而生,具体实现可以参考这里:
http://blog.csdn.net/a_running_wolf/article/details/50617094

主要采用的方法就是给每个Item添加一个标志,记录是否要展开。在getView()中,判断该标志,若是不需要展开的,隐藏不需要显示的;若需要显示,则将隐藏部分也显示出来。

但是这样做之后,确实实现了item展开的功能,却有一个用户体验的问题:
点击屏幕中靠下的几个订单,虽然代码中确实将隐藏项设置为可见了,可是在屏幕上看不到完整的展开后的效果。也就是说,用户点击了,但是看不到完全的展开的效果。
这时,就需要有一个自动向上滚动,来将这个item完整的展示给用户看。

先上一个效果图:

这里写图片描述

下面就是如何实现了。
先整理好思路:
想把item展开后显示完整,分两种情况,就是展开后,当前item就已经全部显示了。这种情况,就不需要做特别的动作,来滑动屏幕了。
另一种情况,就是展开后,当前item只能看见一部分。我们希望能将该item的余下部分都展示出来。

这样,就有几个问题需要解决:
1,怎么判断该item能全部展示?
2,若不能全部展示,该滑动屏幕多大距离来实现恰好的展示呢?
3,如何实现自动滑动呢?

首先要明白几个概念,屏幕的高度,与listView的高度,是两个概念。我们关注的是listView的高度。

我们可以通过跟踪展开前后的listView与item的位置信息,可以了解view的展示情况。
点击第一个item展开,再次点击收起,我跟踪打印信息:
主要是查看 item.getTop(),item.getBottom(),listView.getHeight(),item.getHeight()。

第一次:

onItemClick:65: position=0onItemClick:77: item.getTop=0 getBottom=89onItemClick:78: listView.getHeight=1024 item.getHeight=89

第二次:

onItemClick:65: position=0onItemClick:77: item.getTop=0 getBottom=290onItemClick:78: listView.getHeight=1024 item.getHeight=290

查看item的高度:item.getHeight,第一次为89,第二次为290。最开始可能不知道哪次是展开,哪次是未展开,但是通过这个高度值,就可以判断出来了,较大的那次为展开状态。

我们重点关注的是,item展开后,能否在listView中完整显示。
以listView为一个容器,看能否完整包含某个item,其实就是做一个比较:item.bottom<=listView.height
若符合条件的,就是能全部显示的;
若不满足条件的,则不能全部显示,需要进行滑动。

第二个问题:滑动多少?
很简单,就是滑动出不能显示的那部分:
滑动距离 = item.bottom - listView.height

3,如何实现自动滑动呢?
listView的滑动,有多种方法,我测试了7种滑动方法,简单介绍如下:

1,将View滑动到指定位置:

void scrollTo(int x, int y)

使用这个方法,执行之后,看起来视图变了位置,再次操作进行滑动时,发现它又先回到原来位置了,再开始滑动。就是在视觉上给人画面一闪的感觉,且回到原来位置,不符合操作者的想法。
原因:scrollTo()是View的方法,它用于滑动View的内容,而不是改变View本身所处的位置。
单独的View滑动很少见,通常是ViewGroup调用scroll方法滑动子控件的位置。
下面要说的几个滑动方法,是ListView自带的,或者继承自AbsListView的,都是ViewGroup。
我们跟踪一下代码,可以知道ListView是继承自ViewGroup的,继承关系如下:

ListView->AbsListView->AdapterView->ViewGroup

2,指定某个子项显示到listView的顶部:

void setSelection(int position)

这个方法没有动画,是直接到位。并且只能显示到顶部,太过简单粗暴,不是我们期望的优雅的姿势。

3,指定某项显示到距离listView的顶部一个距离的位置:

void setSelectionFromTop(int position, int y)

可以通过设置偏移值来实现恰好完整展示item:

    y=listView.getHeight()-item.getHeight();

这个方法没有过渡动画,但是已经能实现我们的功能了。

4,做相对滑动,滑动距离为指定的item的个数,平滑滑动:

    void smoothScrollByOffset(int offset)

注意这个参数offset,它不是指滑动距离,而是指的item的个数。
我通过试验,发现是这样调用:

    listView.smoothScrollByOffset(1);

能够实现将底部展开的item显示完整的效果,但是有个小的问题:
若未展示部分较少时,会多展示一个item出来。
如果这个多出来的item是之前展开过的,那么,这个多出来的部分,会导致屏幕滑动相当长一段距离,会让用户感觉比较奇怪,所以,这个方法也不太合适。

5,显示出指定项,平滑滑动(若该项已经完整显示,则不滑动):

void smoothScrollToPosition(int position)

通过测试,这个方法其实是相当符合我们的要求的。如果显示全了的item,没有屏幕滑动,如果指定position的item没显示全,会进行相应的滑动,进行完整的展示出来。

继续研究:

6,在指定时间内,滑动相对距离

void smoothScrollBy(int distance, int duration)

可以通过设置distance来实现恰好完整展示item:

distance=item.getBottom()-listView.getHeight();

另外一个参数duration,是设置滑动的时间,以ms为单位。
这个方法相比较于上一个方法,好处在于可以指定滑动的时间,也就是平滑移动的效果我们可以自己控制,更灵活一些。

7,指定某项从顶部向下偏移的距离,在指定时间内滑动到位

void smoothScrollToPositionFromTop(int position, int offset, int duration)

可以通过设置offset来实现恰好完整展示item:

offset=listView.getHeight()-item.getHeight();

它也有设置滑动的时间的参数duration,对于实现本功能而言,与上一个方法效果相同。

通过试验这7种滑动,最后有4种方法可以实现目标。

void setSelectionFromTop(int position, int y)void smoothScrollToPosition(int position)void smoothScrollBy(int distance, int duration)void smoothScrollToPositionFromTop(int position, int offset, int duration)

如果考虑到平滑滑动的效果的话,去掉快速滑动的setSelectionFromTop(),还有3种。
在这3种方法中,

void smoothScrollToPosition(int position)

是最简单的选择,只有一个参数position,不需要计算滑动的距离。

然而,再仔细对比这3种平滑移动的效果,就会发现smoothScrollToPosition()由于没有设置移动时间的参数,导致平滑移动的效果比较一般,不如另外两个灵活。

然而,smoothScrollToPosition()有一个好处,就是对于顶部未显示完整的item,它也能调整为完整显示!

那么,既然我们为了追求最佳的效果,就算是使用下面两个函数,

void smoothScrollBy(int distance, int duration)void smoothScrollToPositionFromTop(int position, int offset, int duration)

也要实现同样的效果:不仅完整展示底部的item,也要展示完整顶部的item。

其实只要真的关注到了这个问题,解决起来也并不复杂:
添加上顶部未显示全的判断 item.top<0
然后向下滑动指定距离即可,具体到这两个方法,按如下形式调用:

listView.smoothScrollToPositionFromTop(position, 0, 300);listView.smoothScrollBy(itemTop, 300);

现在,怎么滑动的问题已经解决了。
那么,在那个地方来调用这个滑动的方法呢?

首先,想一下,何时滑动?
自然是在点击之后。所以滑动方法的调用,也是在点击动作之后。

这个点击,通常在onItemClick()中处理。不过,有时我们会发现ListView的onItemClick执行不到,这是为什么呢?

在网上查到的通常的说法,是由于item中有button或checkBox。可是在实际使用ListView过程中,我遇到过不符合这种说法的情况,我做了一个测试:
在一个没有button、checkBox的item中,对一个LinearLayout使用了onClickListener,就不能走onItemClick了。
而对于一个有button的item,只要adapter里面没有出现onClickListener,就仍然会走onItemClick。
由此可见,核心问题在于,就是在ListView的适配器类中有setOnClickListener(),截获了OnClick 事件,导致ListView获取不到OnClick 事件了。OnClick 的响应优先级:子控件(元控件)> 父布局,但是不像 onTouch 事件有Boolean 返回值那样,OnClick 事件是没有返回值的,即是“阻断式响应”,不会再响应它所归属的上层控件。

这样,我们也就明确了,调用滑动方法的地方,有两处:
1,ListView的onItemClick()中;
2,adapter中展开动作的OnClick()中;

确定调用的地方了,还有一个调用的时机。直接在onItemClick()中调用,是不能实现完全展示Item的效果的。其实在点击的那个时刻,虽然设置了View的可见,但是ListView还没有做Measure动作,所以获取不到展开后的Item的尺寸。我们可以使用一个延迟来实现:

parent.postDelayed(new Runnable() {

然后在里面的run()方法中获取Item的新的尺寸。例如,我测试了,使用延迟100ms就能获取到执行Measure之后的值。

下面是我在onItemClick()中的实现:

    @Override    public void onItemClick(final AdapterView<?> parent, final View item, final int position, long id) {        LogUtil.logWithMethod(new Exception(),"position="+position);        int flagExpand = 1-Integer.parseInt(datas.get(position).get("flagExpand"));        datas.get(position).put("flagExpand",""+flagExpand);        // 查看ListView与当前item的位置信息(点击时刻的)        LogUtil.logWithMethod(new Exception(),"item.getTop="+item.getTop()+" getBottom="+item.getBottom());        LogUtil.logWithMethod(new Exception(),"listView.getHeight="+listView.getHeight()+" item.getHeight="+item.getHeight());        //要获取点击之后的item的视图信息,需要做一个延迟,等待系统测量完成        parent.postDelayed(new Runnable() {            @Override            public void run() {                // 查看ListView与当前item的位置信息(点击动作之后的--完成展开或收回动作的)                int listViewHeight = listView.getHeight();                int viewBottom = item.getBottom();//item.getTop() + item.getHeight(); //展开后的高度                LogUtil.logWithMethod(new Exception(),"listView.getHeight="+listViewHeight+" item.getHeight="+item.getHeight());                LogUtil.logWithMethod(new Exception(),"viewBottom="+viewBottom+" item.getBottom="+item.getBottom());                int itemTop = item.getTop();                if(itemTop < 0){                    LogUtil.logWithMethod(new Exception(),"scroll offset="+itemTop);//                  listView.smoothScrollToPositionFromTop(position, 0, 300);//指定某项从顶部向下偏移的距离,在指定时间内滑动到位                    listView.smoothScrollBy(itemTop, 300);//做相对移动,在指定时间内完成//                  listView.setSelectionFromTop(position,0);//快速滑动//                  listView.setSelection(position);//快速滑动                } else {                    //只有不能完整显示的,才需要上滑                    if( viewBottom> listViewHeight) {                        LogUtil.logWithMethod(new Exception(),"scroll offset="+(viewBottom-listViewHeight));                        listView.smoothScrollBy(viewBottom-listViewHeight, 300);//做相对移动,在指定时间内完成//                      listView.smoothScrollToPositionFromTop(position, listViewHeight-item.getHeight(), 300);//指定某项从顶部向下偏移的距离,在指定时间内滑动到位//                      listView.setSelectionFromTop(position,listViewHeight-item.getHeight());//快速滑动,参数不同,指定希望达到的位置//                      listView.smoothScrollByOffset(1);//若展示不全的,向下滑动到展示全。若未展示部分较少的,会多展示一个item//                      listView.scrollTo(0, viewBottom-listViewHeight);//快速滑动。且效果不好,item的位置会闪回。只是显示的画面变了,没有改变View本身所处的位置                    }                }                //虽然是平滑移动,但是速度还是比较快的,滑动速度不能指定//              listView.smoothScrollToPosition(position);//显示出指定项,平滑滑动  //点击的,就已经是可见的,不需要滑动了  //最简便的方法,顶部显示不全的,也向下滑动            }        },100);        adapter.notifyDataSetChanged();    }

而对于adapter的getView()里面的OnClick(),需要做一些调整:
1,调整item、listView的获取;
2,转移到 onClick()中,使用final传递参数;

这里就不展示代码了,前面的例子已经将逻辑讲清楚了。
有需要的同学,可以到demo中去看具体的实现。

demo地址:
http://download.csdn.net/detail/lintax/9848239

参考:
http://blog.csdn.net/a_running_wolf/article/details/50617094
http://blog.csdn.net/lilybaobei/article/details/8142987

原创粉丝点击