編輯:關於android開發
一、概述
一般而言,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:

listview_multitype_data.json:

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:

listview_multi_type_item_txt_sent.xml:

listview_multi_type_item_txt_recv.xml:

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]
Android事件分發機制淺談(一),android淺談
Android事件分發機制淺談(一),android淺談---恢復內容開始--- 一、是什麼 我們首先要了解
硅谷新聞8--TabLayout替換ViewPagerIndicator,tablayoutindicator
硅谷新聞8--TabLayout替換ViewPagerIndicator,tablayoutindicator 1.關聯庫 compile com.android.sup
Android Demo手機獲取驗證碼
Android Demo手機獲取驗證碼 注冊很多app或者網絡賬戶的時候,經常需要手機獲取驗證碼,來完成注冊,那時年少,只是覺得手機獲取驗證碼這件事兒很好玩,並沒有關心太
自定義View--一個簡單地圓形Progress效果,view--圓形progress
自定義View--一個簡單地圓形Progress效果,view--圓形progress先看效果圖吧 我們要實現一個自定義的再一個圓形中繪制一個弧形的自定義View,思路