zoukankan      html  css  js  c++  java
  • 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                         }
    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 any
    42                         // 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 that
    10             // 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 temporary
    19             // detached and we do not allow detached views to fire accessibility
    20             // events. So we are announcing that the subtree changed giving a chance
    21             // 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 for
    30                     // 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 at
    37                     // 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 can
    24                 // 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] 

  • 相关阅读:
    算法解读:基本的算法
    算法解读:数据结构
    算法解读:s变量和数组
    软件测试之构建测试---BVT
    什么是算法?——算法轻松入门
    大道至简——软件工程实践者的思想读书笔记四
    软件测试之安装测试
    大道至简——软件工程实践者的思想读书笔记三
    软件测试人员成长必备知识篇
    大道至简——软件工程实践者的思想读书笔记二
  • 原文地址:https://www.cnblogs.com/snser/p/5539749.html
Copyright © 2011-2022 走看看