ListView之多种类型Item

Posted Snser

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ListView之多种类型Item相关的知识,希望对你有一定的参考价值。

一、概述

一般而言,listview每个item的样式是一样的,但也有很多应用场景下不同位置的item需要不同的样式。

拿微信举例,前者的代表作是消息列表,而后者的典型则是聊天会话界面。

本文重点介绍后者,也就是多类型item的listview的实现思路和方法,比如实现一个这样的聊天会话页面:

 

二、实现思路

2.1 第一种思路用“一种类型”变相实现多种类型

这种思路其实与 ListView之点击展开菜单 这篇文章的原理一样,每个item的布局都包含所有类型的元素:

 

对于每个item,根据实际类型,控制“日期”、“发出的消息”、“接收的消息”这三部分的显示/隐藏即可。

这种思路的优势在于好理解,是单一类型的listview的扩展,却并不适合本文描述的应用场景。

因为每个item实际上只会显示“日期”、“发出的消息”、“接收的消息”中的一种,所以每个item都inflate出来一个“全家桶”layout再隐藏其中的两个,实在是一种资源浪费。

 

2.2 第二种思路:利用Adapter原生支持的多类型

其实 android.widget.Adapter 类已经原生支持了多种类型item的模式,并提供了 int getViewTypeCount();  int getItemViewType(int position); 两个方法

只不过在 android.widget.BaseAdapter 中对这两个方法进行了如下的默认实现:

1 public int getViewTypeCount() {
2     return 1;
3 }
4 
5 public int getItemViewType(int position) {
6     return 0;
7 }

那我们要做的就是根据实际的数据,对这两个方法进行正确的返回。

本文采用第二种思路实现多种类型item的listview。

   [转载请保留本文地址:http://www.cnblogs.com/snser/p/5539749.html

三、开始干活

3.1 首先准备好listview的数据和三种item布局

ListViewMultiTypeActivity$JsonListData:

 1     private static class JsonListData {
 2         public static class Message {
 3             public static final int TYPE_COUNT = 3;
 4             public static final int TYPE_DATE = 0x00;
 5             public static final int TYPE_TXT_SENT = 0x01;
 6             public static final int TYPE_TXT_RECV = 0x02;
 7             public int type;
 8             public String txt;
 9             public long time;
10         }
11         public List<Message> messages = new ArrayList<Message>();
12     }
View Code

listview_multitype_data.json:

{
    "messages": [
        {
            "type": 0,
            "time": 1467284175
        },
        {
            "type": 1,
            "txt": "你好"
        },
        {
            "type": 2,
            "txt": "你才好"
        },
        {
            "type": 1,
            "txt": "对话,指两个或更多的人用语言交谈,多指小说或戏剧里的人物之间的"
        },
        {
            "type": 2,
            "txt": "京东童书节低至300减180"
        },
        {
            "type": 1,
            "txt": "http://www.cnblogs.com/snser/"
        },
        {
            "type": 2,
            "txt": "京东商城目前已成长为中国最大的自营式电商企业,2015年第三季度在中国自营式B2C电商市场的占有率为56.9%。"
        },
        {
            "type": 0,
            "time": 1467289175
        },
        {
            "type": 1,
            "txt": "京东金融现已建立七大业务板块,分别是供应链金融、消费金融、众筹、财富管理、支付、保险、证券,陆续推出了京保贝、白条、京东钱包、小金库、京小贷、产品众筹、私募股权融资、小白理财等创新产品"
        },
        {
            "type": 2,
            "txt": "您目前没有新消息"
        },
        {
            "type": 2,
            "txt": "黑炎凝聚,竟是直接化为了一头仰天长啸的黑色巨鸟,而后它仿佛是发现了牧尘飘荡的意识,化为一道黑色火焰,眼芒凶狠的对着他的意识暴冲而来"
        },
        {
            "type": 0,
            "time": 1467294175
        },
        {
            "type": 2,
            "txt": "国务院罕见派出民间投资督查组:活力不够形势严峻"
        },
        {
            "type": 1,
            "txt": "那一道清鸣,并不算太过的响亮,但却是让得牧尘如遭雷击,整个身体都是僵硬了下来,脑子里回荡着嗡嗡的声音。"
        },
        {
            "type": 2,
            "txt": "据海关统计,今年前4个月,我国进出口总值7.17万亿元人民币,比去年同期(下同)下降4.4%。其中,出口4.14万亿元,下降2.1%;进口3.03万亿元,下降7.5%;贸易顺差1.11万亿元,扩大16.5%。"
        },
        {
            "type": 1,
            "txt": "在介绍算法的时空复杂度分析方法前,我们先来介绍以下如何来量化算法的实际运行性能,这里我们选取的衡量算法性能的量化指标是它的实际运行时间。"
        },
        {
            "type": 2,
            "txt": "你拍一"
        },
        {
            "type": 2,
            "txt": "我拍一"
        },
        {
            "type": 1,
            "txt": "一二三四五六七"
        }
    ]
}
View Code

ListViewMultiTypeActivity.onCreate 

 1     protected void onCreate(Bundle savedInstanceState) {
 2         super.onCreate(savedInstanceState);
 3         setContentView(R.layout.listview_multi_type);
 4         
 5         JsonListData data = null;
 6         try {
 7             InputStream is = getResources().getAssets().open("listview_multitype_data.json");
 8             InputStreamReader isr = new InputStreamReader(is);
 9             Gson gson = new GsonBuilder().serializeNulls().create();
10             data = gson.fromJson(isr, JsonListData.class);
11         } catch (Exception e) {
12             e.printStackTrace();
13         }
14         
15         if (data != null && data.messages != null) {
16             mList = (ListView)findViewById(R.id.listview_multi_type_list);
17             mList.setAdapter(new MultiTypeAdapter(ListViewMultiTypeActivity.this, data.messages));
18         }
19     }

listview_multi_type_item_date.xml

 1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 2     xmlns:tools="http://schemas.android.com/tools"
 3     android:layout_width="match_parent"
 4     android:layout_height="match_parent"
 5     android:background="#EEEEEE"
 6     android:orientation="vertical"
 7     tools:context="${relativePackage}.${activityClass}" >
 8 
 9     <TextView
10         android:id="@+id/listview_multi_type_item_date_txt"
11         android:layout_width="wrap_content"
12         android:layout_height="wrap_content"
13         android:layout_gravity="center_horizontal"
14         android:layout_margin="6dp"
15         android:padding="3dp"
16         android:background="#CCCCCC"
17         android:textColor="@android:color/white"
18         android:textSize="12sp"
19         android:text="2015年3月25日 18:44" />
20     
21 </LinearLayout>
View Code

listview_multi_type_item_txt_sent.xml

 1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 2     xmlns:tools="http://schemas.android.com/tools"
 3     android:layout_width="match_parent"
 4     android:layout_height="match_parent"
 5     android:background="#EEEEEE"
 6     android:orientation="vertical"
 7     tools:context="${relativePackage}.${activityClass}" >
 8 
 9     <TextView
10         android:id="@+id/listview_multi_type_item_txt_sent_txt"
11         android:layout_width="wrap_content"
12         android:layout_height="wrap_content"
13         android:maxWidth="250dp"
14         android:layout_gravity="right"
15         android:layout_margin="4dp"
16         android:paddingTop="5dp"
17         android:paddingBottom="5dp"
18         android:paddingRight="10dp"
19         android:paddingLeft="5dp"
20         android:background="@drawable/listview_multi_type_item_txt_sent_bg"
21         android:textColor="@android:color/black"
22         android:textSize="13sp"
23         android:text="发出的消息"
24         android:autoLink="web" />
25     
26 </LinearLayout>
View Code

listview_multi_type_item_txt_recv.xml

 1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 2     xmlns:tools="http://schemas.android.com/tools"
 3     android:layout_width="match_parent"
 4     android:layout_height="match_parent"
 5     android:background="#EEEEEE"
 6     android:orientation="vertical"
 7     tools:context="${relativePackage}.${activityClass}" >
 8 
 9     <TextView
10         android:id="@+id/listview_multi_type_item_txt_recv_txt"
11         android:layout_width="wrap_content"
12         android:layout_height="wrap_content"
13         android:maxWidth="250dp"
14         android:layout_gravity="left"
15         android:layout_margin="4dp"
16         android:paddingTop="5dp"
17         android:paddingBottom="5dp"
18         android:paddingRight="5dp"
19         android:paddingLeft="10dp"
20         android:background="@drawable/listview_multi_type_item_txt_recv_bg"
21         android:textColor="@android:color/black"
22         android:textSize="13sp"
23         android:text="接收的消息"
24         android:autoLink="web" />
25     
26 </LinearLayout>
View Code

 

3.2 重头戏在于Adapter的处理

  1     private class MultiTypeAdapter extends BaseAdapter {
  2         private LayoutInflater mInflater;
  3         private List<JsonListData.Message> mMessages;
  4         private SimpleDateFormat mSdfDate = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss", Locale.getDefault());
  5         
  6         public MultiTypeAdapter(Context context, List<JsonListData.Message> messages) {
  7             mInflater = LayoutInflater.from(context);
  8             mMessages = messages;
  9         }
 10         
 11         private class DateViewHolder {
 12             public DateViewHolder(View viewRoot) {
 13                 date = (TextView)viewRoot.findViewById(R.id.listview_multi_type_item_date_txt);
 14             }
 15             public TextView date;
 16         }
 17         
 18         private class TxtSentViewHolder {
 19             public TxtSentViewHolder(View viewRoot) {
 20                 txt = (TextView)viewRoot.findViewById(R.id.listview_multi_type_item_txt_sent_txt);
 21             }
 22             public TextView txt;
 23         }
 24         
 25         private class TxtRecvViewHolder {
 26             public TxtRecvViewHolder(View viewRoot) {
 27                 txt = (TextView)viewRoot.findViewById(R.id.listview_multi_type_item_txt_recv_txt);
 28             }
 29             public TextView txt;
 30         }
 31         
 32         @Override
 33         public int getViewTypeCount() {
 34             return JsonListData.Message.TYPE_COUNT;
 35         }
 36         
 37         @Override
 38         public int getItemViewType(int position) {
 39             return getItem(position).type;
 40         }
 41         
 42         @Override
 43         public int getCount() {
 44             return mMessages.size();
 45         }
 46 
 47         @Override
 48         public JsonListData.Message getItem(int position) {
 49             return mMessages.get(position);
 50         }
 51 
 52         @Override
 53         public long getItemId(int position) {
 54             return position;
 55         }
 56 
 57         @Override
 58         public View getView(int position, View convertView, ViewGroup parent) {
 59             switch (getItemViewType(position)) {
 60                 case JsonListData.Message.TYPE_DATE:
 61                     return handleGetDateView(position, convertView, parent);
 62                 case JsonListData.Message.TYPE_TXT_SENT:
 63                     return handleGetTxtSentView(position, convertView, parent);
 64                 case JsonListData.Message.TYPE_TXT_RECV:
 65                     return handleGetTxtRecvView(position, convertView, parent);
 66                 default:
 67                     return null;
 68             }
 69         }
 70         
 71         private View handleGetDateView(int position, View convertView, ViewGroup parent) {
 72             if (convertView == null) {
 73                 convertView = mInflater.inflate(R.layout.listview_multi_type_item_date, parent, false);
 74                 convertView.setTag(new DateViewHolder(convertView));
 75             }
 76             if (convertView != null && convertView.getTag() instanceof DateViewHolder) {
 77                 final DateViewHolder holder = (DateViewHolder)convertView.getTag();
 78                 holder.date.setText(formatTime(getItem(position).time));
 79             }
 80             return convertView;
 81         }
 82         
 83         private View handleGetTxtSentView(int position, View convertView, ViewGroup parent) {
 84             if (convertView == null) {
 85                 convertView = mInflater.inflate(R.layout.listview_multi_type_item_txt_sent, parent, false);
 86                 convertView.setTag(new TxtSentViewHolder(convertView));
 87             }
 88             if (convertView != null && convertView.getTag() instanceof TxtSentViewHolder) {
 89                 final TxtSentViewHolder holder = (TxtSentViewHolder)convertView.getTag();
 90                 holder.txt.setText(getItem(position).txt);
 91             }
 92             return convertView;
 93         }
 94         
 95         private View handleGetTxtRecvView(int position, View convertView, ViewGroup parent) {
 96             if (convertView == null) {
 97                 convertView = mInflater.inflate(R.layout.listview_multi_type_item_txt_recv, parent, false);
 98                 convertView.setTag(new TxtRecvViewHolder(convertView));
 99             }
100             if (convertView != null && convertView.getTag() instanceof TxtRecvViewHolder) {
101                 final TxtRecvViewHolder holder = (TxtRecvViewHolder)convertView.getTag();
102                 holder.txt.setText(getItem(position).txt);
103             }
104             return convertView;
105         }
106         
107         private String formatTime(long time) {
108             return mSdfDate.format(new Date(time * 1000));
109         }
110     }

可以看到, int getViewTypeCount();  int getItemViewType(int position); 的处理是非常清晰的。

需要注意的在于,ViewType必须在 [0, getViewTypeCount() - 1] 范围内

 

3.3 ViewHolder为何能正确的工作

回顾一下单一类型的listview,其ViewHolder的工作机制在于系统会将滑出屏幕的item的view回收起来,并作为getView的第二个参数 convertView 传入。

那么,在多种类型的listview中,滑出屏幕的view与即将滑入屏幕的view类型很可能是不同的,那这么直接用不就挂了吗?

其实不然,android针对多种类型item的情况已经做好处理了,如果getView传入的 convertView 不为null,那它一定与当前item的view类型是匹配的。

所以,在3.2节中对ViewHolder的处理方式与单类型的listview并没有本质区别,却也能正常的工作。

  [转载请保留本文地址:http://www.cnblogs.com/snser/p/5539749.html] 

四、demo工程

保存下面的图片,扩展名改成 .zip 即可

  [转载请保留本文地址:http://www.cnblogs.com/snser/p/5539749.html] 

五、番外篇 —— ListView回收机制简要剖析

在3.3节中简单介绍了android系统会处理好多类型item的回收和重用,那具体是怎么实现的呢?

下面简要剖析一下支持多种类型item的listview中,View回收的工作机制。

5.1 View回收站的初始化

ListView的父类AbsListView中定义了一个内部类RecycleBin,这个类维护了listview滑动过程中,view的回收和重用。

在ListView的 setAdapter 方法中,会通过调用 mRecycler.setViewTypeCount(mAdapter.getViewTypeCount()) 来初始化RecycleBin。

让我们看下RecycleBin中对应都做了什么:

 1         public void setViewTypeCount(int viewTypeCount) {
 2             if (viewTypeCount < 1) {
 3                 throw new IllegalArgumentException("Can\'t have a viewTypeCount < 1");
 4             }
 5             //noinspection unchecked
 6             ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
 7             for (int i = 0; i < viewTypeCount; i++) {
 8                 scrapViews[i] = new ArrayList<View>();
 9             }
10             mViewTypeCount = viewTypeCount;
11             mCurrentScrap = scrapViews[0];
12             mScrapViews = scrapViews;
13         }

看源码,说白了就是创建了一个大小为 getViewTypeCount() 数组 mScrapViews ,从而为每种类型的view维护了一个回收站,此外每种类型的回收站自身又是一个View数组。

这也就解释了为什么ViewType必须在 [0, getViewTypeCount() - 1] 范围内。

 

5.2 View回收站的构建和维护

AbsListView在滑动时,会调用 trackMotionScroll 方法:

 1     boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
 2         //...
 3         final boolean down = incrementalDeltaY < 0;
 4         //...
 5         if (down) {
 6             int top = -incrementalDeltaY;
 7             if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
 8                 top += listPadding.top;
 9             }
10             for (int i = 0; i < childCount; i++) {
11                 final View child = getChildAt(i);
12                 if (child.getBottom() >= top) {
13                     break;
14                 } else {
15                     count++;
16                     int position = firstPosition + i;
17                     if (position >= headerViewsCount && position < footerViewsStart) {
18                         // The view will be rebound to new data, clear any
19                         // system-managed transient state.
20                         if (child.isAccessibilityFocused()) {
21                             child.clearAccessibilityFocus();
22 以上是关于ListView之多种类型Item的主要内容,如果未能解决你的问题,请参考以下文章

Listview和RecyclerView区别

RecyclerView 展示多种类型Item数据

Listview实现不同类型的布局

刷新同一活动中剩余的当前片段(ListView 数据)

item的Ontouch和listview的setOnItemClick冲突求解。

Flutter 仿企业微信多选-listview可见item位置