• Android ListView实现不同item的方法和原理分析


    ListView实现不同item的方法和原理分析


    问题抛出
    Listviewandroid里面的重要组件,用来显示一个竖向列表,这个没有什么问题;但是有个时候列表里面的item不是一样的,如下图,列表里面应该有3种类型的item


     
    1. 头像在左边的气泡Item ,比如今天下午我就不出来了,...”
    2. 头像在右边的气泡Item,比如那就等着我发你好吧
    3. 单张图片显示圆角图片item
    几种Item的风格是完全不同的,那么怎么实现呢?


    实现方法
    实现的方法我这里可以列举出两种
    1. 每个Item的布局文件包含气泡,左右头像和圆角图片,然后根据不同的条件做不同的逻辑判断,控制不同Item的显示和隐藏。
    比如如果是接受信息,而且是文字的话,就显示左图片和文字;隐藏图片和右边图片,等等。
    显而易见,这种方法很繁琐,很笨重,而且会导致item的布局文件很大,从而影响listview的效率(加载xml文件是需要时间的)
    2. 使用listview提供的方法,实现的步骤如下:


    a. 主要重写ListAdapternewView(), getItemViewType(),getViewItemTypeCount()几个方法,如下:

    01 public class MyCursorAdapter extends CursorAdapter {
    02  
    03 publicMyCursorAdapter(Context context, Cursor c, boolean autoRequery) {
    04  
    05 super(context, c,autoRequery);
    06  
    07 // TODO Auto-generatedconstructor stub
    08  
    09 }
    10  
    11  
    12  
    13 publicMyCursorAdapter(Context context, Cursor c, int flags) {
    14  
    15 super(context, c, flags);
    16  
    17 // TODO Auto-generatedconstructor stub
    18  
    19 }
    20  
    21  
    22  
    23 public void bindView(Viewarg0, Context arg1, Cursor arg2) {
    24  
    25  
    26  
    27 }
    28  
    29  
    30  
    31  
    32 publicView newView(Context arg0, Cursor arg1, ViewGroup arg2) {
    33  
    34 if (接受文字){
    35  
    36 return 接受文字view
    37  
    38 }else if(发送文字){
    39  
    40 return 发送文字view
    41  
    42  
    43 }else{
    44  
    45 return 圆角图片view
    46  
    47 }
    48  
    49 }
    50  
    51  
    52  
    53  
    54 public int getItemViewType(int position) {
    55  
    56 StringitemValue = getCursor().getString(position);
    57  
    58  
    59  
    60 //下面的代码只是模拟判断逻辑
    61  
    62  
    63 //另外,这个序号是从0开始索引的,由于我们有3种类型的item,所以返回0,1,2,请参考getViewTypeCount()
    64  
    65 if(itemValue.equals("接收文字信息")){//如果是接受文字信息,则显示布局1
    66  
    67 return0;
    68  
    69 }else if(itemValue.equals("发送文字信息")){//如果是发送文字信息,则显示布局2
    70  
    71 return1;
    72  
    73 }
    74  
    75  
    76 return 2;//显示单张图片
    77  
    78  
    79 }
    80  
    81  
    82  
    83  
    84 public intgetViewTypeCount() {
    85  
    86 return 3;//有3种类型的item,所以返回3
    87  
    88 }
    89 }

    嗯,所做的差不多就是这么多,另外就是要准备3item布局文件,也就是newView里面要调用的3个布局文件
    对了,在bindView的时候最好对view进行null的检查,因为3个布局文件里面的view是不同的,或者要分开进行bind,不然有可能会有空指针异常。


     原理分析
    上面第2种实现方法确实比较灵活,那listview是怎么实现的呢?
    而且我们知道listviewitem是可以复用的,那么为什么它不会复用错位呢?比如第2种类型的item,结果找到了缓存中第1种类型的item,就像本来要显示一个发送图片,结果找到发送文字的item,那么复用的时候肯定有问题,因为发送文字的item中根本没有ImageView,只有TextView来显示文字的。
    1. 文件路径
    frameworksasecorejavaandroidwidgetAbsListView.java
    代码

    01 /**
    02  
    03 * The data set used to store unused viewsthat should be reused during the next layout
    04  
    05 * to avoid creating new ones
    06  
    07 */
    08  
    09 final RecycleBin mRecycler = newRecycleBin();
    10 View obtainView(int position,boolean[] isScrap) {
    11  
    12 ...
    13  
    14  
    15  
    16 final View scrapView =mRecycler.getScrapView(position);
    17  
    18 ...
    19 }

    ListView的一个item要显示的时候,就会调用AbsListView.obtainView()方法,比如滑动的时候,滑动出一个Item
    AbsListview会向RecycleBin请求一个scrapView,这个RecycleBinlistview里面的一个重要机制,简单来说,就是它缓存了那些不在屏幕内的listview item,相当于一个垃圾箱,然后当有新的item需要显示的时候,它会首先向垃圾箱里面请求一个已经不显示的item,如果有这样的item的话,就直接拿过来,然后调用下bindView,重新bind下数据就可以了。
    如果没有这样的item就会调用newView去创建一个item view


    滑动的时候,它会不断地把滑出屏幕的item添加到RecycleBin这个垃圾箱里面。
    这样就实现了一个循环,listview不管有多少数据,不管滑动多少次,真正通过newView产生的item view其实就是一个屏幕内最多容纳的item数目,形成一个链条结构,不断回收,不断复用。


    那接下来看看mRecycler.getScrapView(position)的实现

    01 private ArrayList<View>[] mScrapViews;
    02  
    03  
    04 private ArrayList<View> mCurrentScrap;
    05  
    06  
    07 View getScrapView(int position) {
    08  
    09 if (mViewTypeCount ==1) {
    10  
    11 return retrieveFromScrap(mCurrentScrap,position);
    12  
    13 } else {
    14  
    15 int whichScrap =mAdapter.getItemViewType(position);
    16  
    17 if (whichScrap>= 0 && whichScrap < mScrapViews.length) {
    18  
    19 returnretrieveFromScrap(mScrapViews[whichScrap], position);
    20  
    21 }
    22  
    23 }
    24  
    25 return null;
    26  
    27 }

    这个viewTypeCount就是ListAdaptergetViewTypeCount()方法返回的,默认实现就是返回1,如果没有重写的话,在setAdapter的时候调用,代表的是listview里面会有多少种类型的item,如下:

    01 public void setAdapter(ListAdapter adapter) {
    02  
    03 ...
    04  
    05 super.setAdapter(adapter);
    06  
    07  
    08  
    09 if (mAdapter != null){
    10  
    11  
    12  
    13  
    14 mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
    15  
    16  
    17  
    18 }
    19 ..
    20 }

    如果为viewTypeCount==1的话,也就是只有一种类型的item,那么直接从mCurrentScrap里面获取即可。


    那如果有多个类型的item的话,怎么办呢?
    首先,调用我们重写的getItemViewType(int position)来获取到这种类型的item的索引号
    int whichScrap = mAdapter.getItemViewType(position);
    然后根据这个索引号whichScrapmScrapView数组里面获取到一个垃圾箱,然后再从垃圾箱里面去获取这个类型的被回收的Item
    这样就解决了复用错误的问题,比如把第2种类型的item复用了缓存中第1种类型的Item,这样就解决了第三章开头说的那个复用错位的问题。


    2. Listview是怎么把一个item添加到垃圾箱?
    那么,我们来拿一个简单的情景来举例子,比如滑动的时候
    文件路径:
    frameworksasecorejavaandroidwidgetAbsListView.java
    代码:

    001 /**
    002  
    003 * Track a motion scroll
    004  
    005 *
    006  
    007 * @param deltaY Amount tooffset mMotionView. This is the accumulated delta since the motion
    008  
    009 *
    010 began. Positive numbers mean the user'sfinger is moving down the screen.
    011  
    012 * @param incrementalDeltaYChange in deltaY from the previous event.
    013  
    014 * <a href="http://home.51cto.com/index.php?s=/space/34010" target="_blank">@return</a> true if we'realready at the beginning/end of the list and have nothing to do.
    015  
    016 */
    017  
    018 boolean trackMotionScroll(intdeltaY, int incrementalDeltaY) {
    019  
    020 ...
    021  
    022  
    023  
    024 if (down) {//向上滚动
    025  
    026  
    027 int top =-incrementalDeltaY;
    028  
    029 ...
    030  
    031 for (int i = 0; i< childCount; i++) {
    032  
    033 final View child= getChildAt(i);
    034  
    035 if(child.getBottom() >= top) {
    036  
    037 break;
    038  
    039 } else {
    040  
    041 count++;
    042  
    043 int position= firstPosition + i;
    044  
    045 if (position>= headerViewsCount && position < footerViewsStart) {
    046  
    047 // Theview will be rebound to new data, clear any
    048  
    049 //system-managed transient state.
    050  
    051 if(child.isAccessibilityFocused()) {
    052  
    053 child.clearAccessibilityFocus();
    054  
    055 }
    056  
    057 mRecycler.addScrapView(child, position);
    058  
    059 }
    060  
    061 }
    062  
    063 }
    064  
    065 } else {//向下滚动
    066  
    067  
    068  
    069 int bottom = getHeight() -incrementalDeltaY;
    070  
    071 if ((mGroupFlags& CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
    072  
    073 bottom -=listPadding.bottom;
    074  
    075 }
    076  
    077 for (int i =childCount - 1; i >= 0; i--) {
    078  
    079  
    080 final View child = getChildAt(i);
    081  
    082 if(child.getTop() <= bottom) {
    083  
    084 break;
    085  
    086 } else {
    087  
    088 start = i;
    089  
    090 count++;
    091  
    092 int position= firstPosition + i;
    093  
    094 if (position>= headerViewsCount && position < footerViewsStart) {
    095  
    096 // Theview will be rebound to new data, clear any
    097  
    098 //system-managed transient state.
    099  
    100 if(child.isAccessibilityFocused()) {
    101  
    102 child.clearAccessibilityFocus();
    103  
    104 }
    105  
    106 mRecycler.addScrapView(child, position);
    107  
    108 }
    109  
    110 }
    111  
    112 }
    113  
    114 }

    如果向上滚动的话,那么就判断itembottom-滚动距离 >=0?如果是,那么说明这个item还是可见的,不应该添加到垃圾箱;否则就不可见了。
    这个判断逻辑要结合下手机屏幕坐标系来理解,如下:
     



    图有点丑,勿见怪,坐标原点(0,0)是在屏幕的左上方,这个有点特别。
    那么如果是向上滑动话的,我们要判断某个item是否被滑动出了屏幕,就是判断这个itembottom – 向上滚动量< 0?比如一个item 的最下面的边界是50px那个地方,然后向上滚动了60px,那么肯定已经滑动出了屏幕,对不对?
    也就是50 – 60 = -10 <0
    如果只是滑动了40px,那么这个item应该还有10px留着屏幕上面,这个时候肯定不能被回收,因为它对于用户还是可见的。
    也就是 50 – 40 = 10 >0
    如果刚好滑动了50px,按照listview 的逻辑,这个item也是不回收的。如下:

    1 if (child.getBottom() >=top) {
    2  
    3  
    4 break;
    5  
    6 }

    再排除是否是listview header或者footer,如果不是的话,那就是listview的内容item了,应该添加到垃圾箱里面。

    01 else {
    02  
    03 count++;
    04  
    05 int position= firstPosition + i;
    06  
    07 if (position>= headerViewsCount && position < footerViewsStart) {
    08  
    09 // Theview will be rebound to new data, clear any
    10  
    11 //system-managed transient state.
    12  
    13 if(child.isAccessibilityFocused()) {
    14  
    15 child.clearAccessibilityFocus();
    16  
    17 }
    18  
    19 mRecycler.addScrapView(child,position);
    20  
    21 }
    22  
    23 }

    为了清理内存,它会先清理掉这个itemview的一些属性,然后调用mRecycler.addScrapView(child, position);添加到垃圾箱。



    那如果是向下滑动呢?
    根据上面手机的坐标系,这个时候肯定是判断itemtop和整个ListView的高度以及滚动距离。应该是top+ 滚动距离 > 整个ListView的高度,这个时候说明item已经不可见;如果top + 滚动距离 <= 整个ListView的高度,就说明这个item还是可见的。

    1 int bottom = getHeight() -incrementalDeltaY;
    2 if (child.getTop() <=bottom) {//仍然可见
    3  
    4  
    5 break;
    6 }

    好,分析完这个滚动的计算逻辑后,来看看如何把view添加到垃圾箱的。

    01 void addScrapView(View scrap,int position) {
    02  
    03 ...
    04  
    05  
    06  
    07 if(mViewTypeCount == 1) {
    08 //如果只有一种item类型,直接添加
    09  
    10  
    11 mCurrentScrap.add(scrap);
    12  
    13 } else {//如果有多种item类型,找到viewType对应的垃圾箱添加
    14  
    15  
    16 mScrapViews[viewType].add(scrap);
    17  
    18 }
    19  
    20 ...
    21 }

      小结
    1. 这篇帖子总结Listview中如果有多种类型的item的实现方式和原理。
    2. 多个Item实现的原理主要就是AbsListView中有个mScrapViews数组,它的大小对应着Item类型的数目,也就是getItemTypeCount的返回大小。这个mScrapViews里面根据viewType的值,把不同类型的item存放在不同的ArrayList里面;
    然后获取的时候再根据这个viewType首先来找到对应的ArrayList垃圾箱,然后再从ArrayList垃圾箱里面找到同一个类型的缓存item,当然如果没有找到,就会调用newView新建。
    3. 分析了滚动的情况下,listview判断item是否可见的实现原理,它是根据item的坐标来判断的。

    转至 http://bbs.51cto.com/thread-1168539-1.html

  • 相关阅读:
    Agile PLM bom表结构学习笔记
    火狐书签同步服务无法同步问题解决
    EasyExcel使用笔记
    给EasyUI查询按钮添加回车事件.
    让bat异常之后不直接关闭窗口的办法.
    DROP TABLE、TRUNCATE TABLE和DELETE的区别
    DUMPBIN工具的使用
    Java Build Path 详解
    用eclipse中打开已存在的Java Project
    .net core、微服务 开源项目
  • 原文地址:https://www.cnblogs.com/wangzehuaw/p/5383600.html
Copyright © 2020-2023  润新知