大家好,又见面了,我是你们的朋友全栈君。
一、概述
一般而言,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 中对这两个方法进行了如下的默认实现:
代码语言:javascript复制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:
代码语言:javascript复制 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:
代码语言:javascript复制{
"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:
代码语言:javascript复制 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:
代码语言:javascript复制 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:
代码语言:javascript复制 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:
代码语言:javascript复制 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的处理
代码语言:javascript复制 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中对应都做了什么:
代码语言:javascript复制 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 方法:
代码语言:javascript复制 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中:
代码语言:javascript复制 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 中被调用:
代码语言:javascript复制 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。
代码语言:javascript复制 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]
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/154873.html原文链接:https://javaforall.cn