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