你的位置:首页 > 操作系统

[操作系统]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.:

 1 <LinearLayout ="http://schemas.android.com/apk/res/android" 2   ="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   <TextView10     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.:

 1 <LinearLayout ="http://schemas.android.com/apk/res/android" 2   ="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   <TextView10     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.:

 1 <LinearLayout ="http://schemas.android.com/apk/res/android" 2   ="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   <TextView10     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 any19             // system-managed transient state.20             if (child.isAccessibilityFocused()) {21               child.clearAccessibilityFocus();22             }23             mRecycler.addScrapView(child, position);24           }25         }26       }27     } else {28       int bottom = getHeight() - incrementalDeltaY;29       if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {30         bottom -= listPadding.bottom;31       }32       for (int i = childCount - 1; i >= 0; i--) {33         final View child = getChildAt(i);34         if (child.getTop() <= bottom) {35           break;36         } else {37           start = i;38           count++;39           int position = firstPosition + i;40           if (position >= headerViewsCount && position < footerViewsStart) {41             // The view will be rebound to new data, clear any42             // system-managed transient state.43             if (child.isAccessibilityFocused()) {44               child.clearAccessibilityFocus();45             }46             mRecycler.addScrapView(child, position);47           }48         }49       }50     }51     //...52   }

在 trackMotionScroll 方法中,会根据不同的滑动方向,调用 addScrapView ,将滑出屏幕的view加到RecycleBin中:

 1     void addScrapView(View scrap, int position) { 2       final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams(); 3       if (lp == null) { 4         return; 5       } 6  7       lp.scrappedFromPosition = position; 8  9       // Remove but don't scrap header or footer views, or views that10       // should otherwise not be recycled.11       final int viewType = lp.viewType;12       if (!shouldRecycleViewType(viewType)) {13         return;14       }15 16       scrap.dispatchStartTemporaryDetach();17 18       // The the accessibility state of the view may change while temporary19       // detached and we do not allow detached views to fire accessibility20       // events. So we are announcing that the subtree changed giving a chance21       // to clients holding on to a view in this subtree to refresh it.22       notifyViewAccessibilityStateChangedIfNeeded(23           AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);24 25       // Don't scrap views that have transient state.26       final boolean scrapHasTransientState = scrap.hasTransientState();27       if (scrapHasTransientState) {28         if (mAdapter != null && mAdapterHasStableIds) {29           // If the adapter has stable IDs, we can reuse the view for30           // the same data.31           if (mTransientStateViewsById == null) {32             mTransientStateViewsById = new LongSparseArray<View>();33           }34           mTransientStateViewsById.put(lp.itemId, scrap);35         } else if (!mDataChanged) {36           // If the data hasn't changed, we can reuse the views at37           // their old positions.38           if (mTransientStateViews == null) {39             mTransientStateViews = new SparseArray<View>();40           }41           mTransientStateViews.put(position, scrap);42         } else {43           // Otherwise, we'll have to remove the view and start over.44           if (mSkippedScrap == null) {45             mSkippedScrap = new ArrayList<View>();46           }47           mSkippedScrap.add(scrap);48         }49       } else {50         if (mViewTypeCount == 1) {51           mCurrentScrap.add(scrap);52         } else {53           mScrapViews[viewType].add(scrap);54         }55 56         // Clear any system-managed transient state.57         if (scrap.isAccessibilityFocused()) {58           scrap.clearAccessibilityFocus();59         }60 61         scrap.setAccessibilityDelegate(null);62 63         if (mRecyclerListener != null) {64           mRecyclerListener.onMovedToScrapHeap(scrap);65         }66       }67     }

在 addScrapView 方法中,被回收的view会根据其类型加入 mScrapViews 中。

特别的,如果这个view处于TransientState(瞬态,view正在播放动画或其他情况),则会被存入 mTransientStateViewsById 、 mTransientStateViews 。

 

5.3 从View回收站获取View

Adapter的getView方法在AbsListView的 obtainView 中被调用:

 1   View obtainView(int position, boolean[] isScrap) { 2     Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView"); 3     isScrap[0] = false; 4     View scrapView; 5     scrapView = mRecycler.getTransientStateView(position); 6     if (scrapView == null) { 7       scrapView = mRecycler.getScrapView(position); 8     } 9     10     View child;11     if (scrapView != null) {12       child = mAdapter.getView(position, scrapView, this);13       if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {14         child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);15       }16       if (child != scrapView) {17         mRecycler.addScrapView(scrapView, position);18         if (mCacheColorHint != 0) {19           child.setDrawingCacheBackgroundColor(mCacheColorHint);20         }21       } else {22         isScrap[0] = true;23         // Clear any system-managed transient state so that we can24         // recycle this view and bind it to different data.25         if (child.isAccessibilityFocused()) {26           child.clearAccessibilityFocus();27         }28         child.dispatchFinishTemporaryDetach();29       }30     } else {31       child = mAdapter.getView(position, null, this);32       if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {33         child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);34       }35       if (mCacheColorHint != 0) {36         child.setDrawingCacheBackgroundColor(mCacheColorHint);37       }38     }39     40     //...41     42     return child;43   }

可以看到,对于不处于TransientState的View,将会尝试通过 getScrapView 方法获取回收的View,如果有,就会作为参数传入Adatper的getView方法中。

而 getScrapView 方法,其实就是先调用Adapter的 getItemViewType 方法取position对应的view类型,然后从 mScrapViews 中根据类型取view。

 1     View getScrapView(int position) { 2       if (mViewTypeCount == 1) { 3         return retrieveFromScrap(mCurrentScrap, position); 4       } else { 5         int whichScrap = mAdapter.getItemViewType(position); 6         if (whichScrap >= 0 && whichScrap < mScrapViews.length) { 7           return retrieveFromScrap(mScrapViews[whichScrap], position); 8         } 9       }10       return null;11     }

至此,我们简要了解了多类型的listview中,是如何在滑动屏幕时回收view并进行重用的。

而如何维护每个类型item对应的View数组,以及TransientState的维护,本篇文章就不做详细介绍了,有兴趣的读者可以着重研究一下AbsListView的源码。

 

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