Android之ListView的getItemViewType和getViewTypeCount
PS:感覺這兩個方法其實還是很容易理解的,也算是給我其他兩個朋友寫的吧,幫他們搞清楚這兩個方法的用法和概念。同時還有一些小細節問題需要註意。
學習內容:
1.getItemViewType和getViewTypeCount
getItemViewType和getViewTypeCount是ListView中實現復雜列表的兩個相關的方法,普通的ListView中Item是相同的,那麽我們只需要實現Adapter中四個抽象方法即可,但是如果頁面中Item長得比較的復雜呢?比如說這個。
比如說這個列表項,其實也不是很復雜,這種類型的Item也有其他的實現方式,比如說在Adapter中實現SectionIndexer也是可以實現的,但是我們就拿這個來說明一下問題,如果一個Item第一種類型是TextView,第二種類型是ImageView+Button+TextView呢,那麽這樣復雜的列表我們就需要使用getItemViewType()和getTypeViewCount()兩個方法去實現了。這兩個方法理解起來還是比較容易的,獲取Item中Type的類型以及Item中Type的相關數量。廢話就不多說了,直接說實現方式。
public class ListAdapter extends BaseAdapter {
/**
* Item類型,int值.必須從0開始依次遞增.
* */
private static final int TYPE_TITLE = 0;
private static final int TYPE_CONTENT = 1;
/**
* Item Type 的數量
* */
private static final int TYPE_ITEM_COUNT = 2;
/**
* 數據
* */
private List<Company> mData = new ArrayList<>();
private Context context;
public ListAdapter(Context context,List<Company>mData){
this.context = context;
this.mData = mData;
}
@Override
public int getCount() {
return mData.size();
}
@Override
public Object getItem(int position) {
return mData.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup viewGroup) {
/**
* 不同類型的ViewHolder
* */
TitleViewHolder titleViewHolder = null;
CompanyViewHolder contentViewHolder = null;
/**
* 對類型進行判斷,分別inflate不同的布局.
* */
switch (getItemViewType(position)){
case TYPE_TITLE:
titleViewHolder = new TitleViewHolder();
if(convertView == null){
convertView = View.inflate(context, R.layout.view_holder_company_index,null);
titleViewHolder.title = (TextView) convertView.findViewById(R.id.tv_title);
//setTag()
convertView.setTag(titleViewHolder);
}else{
//getTag();
titleViewHolder = (TitleViewHolder) convertView.getTag();
}
titleViewHolder.title.setText(mData.get(position).getName());
break;
case TYPE_CONTENT:
contentViewHolder = new CompanyViewHolder();
if(convertView == null){
convertView = View.inflate(context,R.layout.view_holder_company,null);
contentViewHolder.content = (TextView) convertView.findViewById(R.id.tv_content);
convertView.setTag(contentViewHolder);
}else{
contentViewHolder = (CompanyViewHolder) convertView.getTag();
}
contentViewHolder.content.setText(mData.get(position).getCode());
break;
}
return convertView;
}
/**
* 根據position獲取Item的類型
* */
@Override
public int getItemViewType(int position) {
if(TextUtils.isEmpty(mData.get(position).getCode())){
return TYPE_TITLE;
}else{
return TYPE_CONTENT;
}
}
/**
* 返回Item Type的總數量
* */
@Override
public int getViewTypeCount() {
return TYPE_ITEM_COUNT;
}
static class TitleViewHolder{
TextView title;
}
static class CompanyViewHolder{
TextView content;
}
}
- 首先我們需要為不同的Item設置不同的數值,int值,因為getItemViewType返回的是int值,所以需定義成int,必須從0開始,依次遞增。原因我後續會做出解釋。
- 重寫getItemViewType和getViewTypeCount方法,getViewTypeCount返回Item的類型總數,getViewTypeCount則需要進行判斷,判斷方式一般都是通過JavaBean中的相關字段來判斷的,因此這塊不需要過於糾結。只需要根據position獲取Item的具體類型進行判斷然後就返回就可以了。
- 定義ViewHolder,根據類型的不同需要定義多個ViewHolder,減少findViewById()的次數。
- 重寫getView()中的相關方法,在getView中首先根據position獲取Item的類型去加載不用的布局,這裏同時會setViewType為不同類型的Item設置RecycleBin,解決ListView由於多個類型Item的復用問題。不清楚RecycleBin機制的讀者可以去看下ListView的復用機制這裏說到了RecycleBin,如果不懂這個機制是看不明白下面的解釋的。
- 最後根據傳遞過來的數據setAdapter然後為Item進行賦值就完成了。
大體的一個思路就是這樣實現的,這裏需要說一下為什麽定義Item的類型的時候必須要從0開始,依次遞增,那麽原因是什麽呢?如果我們有三種類型,我們將Item定義成1,2,4,那麽勢必會出現ArrayIndexOutOfBoundsException,也就是所謂的數組越界,我上網查了很多資料都說會出現異常,並且Google也確實標明了,Note:
Integers must be in the range 0 to getViewTypeCount()
- 1. IGNORE_ITEM_VIEW_TYPE
can also be returned.但是看到這裏就沒有後續了,沒人會去說明這個問題是怎樣發生的,為什麽要這樣去定義。可能我就是閑的蛋疼的那種人,不弄明白確實感到不舒服。我在解釋一下具體的原因:
其實發生這種情況一般都是我們在下拉的時候出現的問題,在第一次加載第一頁的時候是不會直接出現崩潰現象的,那麽心細的讀者可能會明白這有可能是ListView在復用時出現的問題,其實卻是就是ListView復用機制導致的。我們來看一下這個方法:
/**
*ListView在針對不同Item復用時會調用這個方法
*為每一種不同的Item設置一個RecycleBin,用於復用.
*/
public void setViewTypeCount(int viewTypeCount) {
if (viewTypeCount < 1) {
throw new IllegalArgumentException("Can‘t have a viewTypeCount < 1");
}
// noinspection unchecked
/**
* 根據viewTypeCount的數量設置一個ArrayList.
* 同時為每一個Item再設置一個ArrayList,用來存儲ScrapView.
* 相當於一個二維數組來維護每一個Item的ScrapView數組.
* 這裏也就相當於為不同的Item設置單獨的RecycleBin.
*/
ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
for (int i = 0; i < viewTypeCount; i++) {
scrapViews[i] = new ArrayList<View>();
}
//保存ViewTypeCount,也就 = 2
mViewTypeCount = viewTypeCount;
//當前的Scrap是第一個Item的ScrapView數組.
mCurrentScrap = scrapViews[0];
//mScrapViews就保存了一個二維數組維護的RecycleBin.
mScrapViews = scrapViews;
}
這是具體的數據結構,簡單理解就是每一個Item都有對應的ScrapView數組。這裏其實並不是出問題的地方,我們都知道Item一旦被移出了屏幕,首先會Detach掉,然後被加入到mScrapView數組中(廢棄View池),那麽在addScrapView的時候就會出現異常.
void addScrapView(View scrap) {
AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
if (lp == null) {
return;
}
// Don‘t put header or footer views or views that should be ignored
// into the scrap heap
int viewType = lp.viewType;
if (!shouldRecycleViewType(viewType)) {
if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
removeDetachedView(scrap, false);
}
return;
}
/**
*核心代碼就是這塊,由於我們mViewTypeCount != 1 的,因此if條件不成立.
*因此會執行else代碼.
*/
if (mViewTypeCount == 1) {
dispatchFinishTemporaryDetach(scrap);
mCurrentScrap.add(scrap);
} else {
dispatchFinishTemporaryDetach(scrap);
mScrapViews[viewType].add(scrap);
}
if (mRecyclerListener != null) {
mRecyclerListener.onMovedToScrapHeap(scrap);
}
}
問題就在於這個else代碼當中,我們可以看到mScrapViews[viewType].add(scrap)代碼執行了,我們前面圖片上顯示的,mScrapViews[]是很據Item的種類數量new出來的,由於我們Item總數是兩種類型,那麽mScrapViews[].length = 2,但是這裏是mScrapView[viewtype],viewtype是什麽,其實就是我們getItemViewType的返回值,如果我們將類型定義成2和3,那麽他會訪問mScrapView[2]和mScrapView[3],可想而知,一定會出現ArrayIndexOutOffBoundsException.這就是數組越界的真正原因。
基本就解釋完了,貼上一個從GitHub蕩下來的源代碼:Demo下載
Android之ListView的getItemViewType和getViewTypeCount