在面試的時候經常會被問到一個有關ListView的問題:一個ListView的高度最多可以顯示5個item,但是卻有20條數據要顯示,問最多會有多少個convertView會被復用?或者如在ListView的Adapter中,在以Google推薦的方式進行view的復用時,convertView為null時要對convertView進行新建,那么新建的convertView最多會有多少個?或者convertView為null的情況下最多的個數是多少?
對這個問題的原理醞釀了好久,今天終於有時間對其進行驗證。寫了個測試用的Demo,用於分析兩種情況下null的convertView的個數:單一種類item和多種類item。
首先對最簡單、最常見的情況進行驗證:單一種類item。
首先建了一個測試工程:LVItemCountTest。里面只有一個Activity---MainActivity。
其中的布局文件非常簡單,activity_main.xml,詳情如下:

1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 2 xmlns:tools="http://schemas.android.com/tools" 3 android:id="@+id/container" 4 android:layout_width="match_parent" 5 android:layout_height="match_parent" 6 android:orientation="vertical" 7 tools:context="com.example.lvitemcounttest.MainActivity" 8 tools:ignore="MergeRootFrame" > 9 10 <TextView 11 android:layout_width="fill_parent" 12 android:layout_height="20dp" 13 android:text="ListView Item convertView test" /> 14 15 <ListView 16 android:id="@+id/listview" 17 android:layout_width="fill_parent" 18 android:layout_height="300dp" > 19 </ListView> 20 21 </LinearLayout>
我將其中ListView的高度設置為“300dp”,是為了測試的便利性考慮的。
其中的單一item的布局文件名字為listview_item_layout_1.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="wrap_content" 5 android:orientation="vertical" > 6 7 <TextView 8 android:id="@+id/content" 9 android:layout_width="match_parent" 10 android:layout_height="50dp" 11 android:background="#b4917d" 12 android:gravity="center" 13 android:text="AA" 14 android:textColor="#009933" 15 android:textSize="20sp" /> 16 17 </LinearLayout>
同時也是為了驗證測試方便的緣故,將其中的TextView的高度設置為“50dp”,背景設置為“#b4917d”,字體顏色設置為“#009933”,而字體的大小設置為“20sp”。
然后通過繼承BaseAdapter新建了一個Adapter類為SingleTypeItemAdapter,代碼詳情如下:

1 private static class SingleTypeItemAdapter extends BaseAdapter { 2 private Context context; 3 private List<String> list; 4 5 public SingleTypeItemAdapter(Context context, List<String> list) { 6 // TODO Auto-generated constructor stub 7 this.context = context; 8 this.list = list; 9 } 10 11 @Override 12 public int getCount() { 13 // TODO Auto-generated method stub 14 return list.size(); 15 } 16 17 @Override 18 public String getItem(int position) { 19 // TODO Auto-generated method stub 20 return list.get(position); 21 } 22 23 @Override 24 public long getItemId(int position) { 25 // TODO Auto-generated method stub 26 return position; 27 } 28 29 @Override 30 public View getView(int position, View convertView, ViewGroup parent) { 31 // TODO Auto-generated method stub 32 ViewHolder1 contentHolder = null; 33 if (convertView == null) { 34 Log.i("Null ConvertView", position + ""); 35 convertView = LayoutInflater.from(context).inflate( 36 R.layout.listview_item_layout_1, null); 37 contentHolder = new ViewHolder1(); 38 contentHolder.content = (TextView) convertView 39 .findViewById(R.id.content); 40 convertView.setTag(contentHolder); 41 } else { 42 contentHolder = (ViewHolder1) convertView.getTag(); 43 } 44 contentHolder.content 45 .setText(list.get(position) + "---" + position); 46 return convertView; 47 } 48 49 private static class ViewHolder1 { 50 TextView content; 51 } 52 53 }
其中的代碼塊
- Log.i("Null ConvertView", position + "");
是為了在LogCat中查看測試結果。
同時,為了方便地在LogCat中查看測試的記錄,新建了一個Log Filter,名字為LVItemCountTest,其中的“By Log Cat”的值設置為“Null ConvertView”。
然后通過設置給布局中的listview,應用剛打開時,其中的效果為:
從中可以看到,此時listview中顯示了6個item,符合300dp/50dp=6的計算。同時LogCat中顯示如下的記錄:

1 10-08 15:39:25.711: I/Null ConvertView(30721): 0 2 3 10-08 15:39:25.721: I/Null ConvertView(30721): 1 4 5 10-08 15:39:25.721: I/Null ConvertView(30721): 2 6 7 10-08 15:39:25.731: I/Null ConvertView(30721): 3 8 9 10-08 15:39:25.741: I/Null ConvertView(30721): 4 10 11 10-08 15:39:25.751: I/Null ConvertView(30721): 5
記錄顯示跟我們的預計相符。
此時一定要注意的是:因為listview的高度恰好顯示了6條item,而沒有出現有頂部的item顯示一半,同時底部也只顯示item的一半的情況。如果出現了這種情況,記錄會出現什么呢?
好的,我們輕輕地將listview下滑,下滑的距離不超過一個item的高度,效果圖如下:
然后查看Logcat,顯示如下:

1 10-08 15:39:25.711: I/Null ConvertView(30721): 0 2 3 10-08 15:39:25.721: I/Null ConvertView(30721): 1 4 5 10-08 15:39:25.721: I/Null ConvertView(30721): 2 6 7 10-08 15:39:25.731: I/Null ConvertView(30721): 3 8 9 10-08 15:39:25.741: I/Null ConvertView(30721): 4 10 11 10-08 15:39:25.751: I/Null ConvertView(30721): 5 12 13 10-08 15:39:29.631: I/Null ConvertView(30721): 6
對,就是又打印出了一條記錄!並且,此后無論如何滑動listview,緩慢滑動也好,快速滑到底或者滑到頂也好,記錄不會再次發生變化,這說明這個listview總共生成了7個convertView!
好的,總結一下:因為listview高度為300dp,而一個item的高度為50dp,所以,在剛顯示的時候恰好顯示了6條記錄,而在滑動的過程中,因為出現了頂部和底部同時顯示不完整的item,此時屏幕中最多出現了7個item。此后,所有的7個item的convertView可以進行復用了,就不再新建convertView。
那么好,當listview可以顯示多種item的時候,情況又是怎么樣的呢?
同樣是出於方便測試的原因,我們再次新建了一個與之前item的布局高度相同的新的布局,來模擬另外一種item顯示效果,同時,只設置了兩種item布局(因為多種布局的時候,原理是跟兩種布局的原理一致的)。
新的item布局為listview_item_layout_2.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="wrap_content" 5 android:orientation="vertical" > 6 7 <TextView 8 android:id="@+id/title" 9 android:layout_width="match_parent" 10 android:layout_height="50dp" 11 android:background="#d0db96" 12 android:gravity="left|center_vertical" 13 android:text="BB" 14 android:textColor="#990033" 15 android:textSize="25sp" /> 16 17 </LinearLayout>
為了與之前的item布局相區分,將其中的TextView的高度設置為“50dp”,背景設置為“#d0db96”,字體顏色設置為“#990033”,而字體的大小設置為“25sp”。
然后,新建了一個BaseAdapter,名字為MultiTypeItemAdapter,代碼細節如下:

1 private static class MultiTypeItemAdapter extends BaseAdapter { 2 private Context context; 3 private List<String> list; 4 private static final int TYPE_TITLE = 0x000; 5 private static final int TYPE_CONTENT = 0x001; 6 private static final int TYPE_COUNT = 2; 7 8 public MultiTypeItemAdapter(Context context, List<String> list) { 9 // TODO Auto-generated constructor stub 10 this.context = context; 11 this.list = list; 12 } 13 14 @Override 15 public int getItemViewType(int position) { 16 // TODO Auto-generated method stub 17 if (position % 5 == 0) { 18 return TYPE_TITLE; 19 } else { 20 return TYPE_CONTENT; 21 } 22 } 23 24 @Override 25 public int getViewTypeCount() { 26 // TODO Auto-generated method stub 27 return TYPE_COUNT; 28 } 29 30 @Override 31 public int getCount() { 32 // TODO Auto-generated method stub 33 return list.size(); 34 } 35 36 @Override 37 public String getItem(int position) { 38 // TODO Auto-generated method stub 39 return list.get(position); 40 } 41 42 @Override 43 public long getItemId(int position) { 44 // TODO Auto-generated method stub 45 return position; 46 } 47 48 @Override 49 public View getView(int position, View convertView, ViewGroup parent) { 50 // TODO Auto-generated method stub 51 ViewHolder1 contentHolder = null; 52 ViewHolder2 titleHolder = null; 53 switch (getItemViewType(position)) { 54 case TYPE_TITLE: 55 if (convertView == null) { 56 Log.i("Null ConvertView", position / 5 + "---Title"); 57 convertView = LayoutInflater.from(context).inflate( 58 R.layout.listview_item_layout_2, null); 59 titleHolder = new ViewHolder2(); 60 titleHolder.title = (TextView) convertView 61 .findViewById(R.id.title); 62 convertView.setTag(titleHolder); 63 } else { 64 titleHolder = (ViewHolder2) convertView.getTag(); 65 } 66 break; 67 case TYPE_CONTENT: 68 if (convertView == null) { 69 Log.i("Null ConvertView", position / 5 + "---Title---" 70 + position % 5 + "---Content"); 71 convertView = LayoutInflater.from(context).inflate( 72 R.layout.listview_item_layout_1, null); 73 contentHolder = new ViewHolder1(); 74 contentHolder.content = (TextView) convertView 75 .findViewById(R.id.content); 76 convertView.setTag(contentHolder); 77 } else { 78 contentHolder = (ViewHolder1) convertView.getTag(); 79 } 80 break; 81 82 default: 83 break; 84 } 85 switch (getItemViewType(position)) { 86 case TYPE_TITLE: 87 titleHolder.title.setText(list.get(position) + "---" + position 88 / 5); 89 break; 90 case TYPE_CONTENT: 91 contentHolder.content.setText(list.get(position) + "---" 92 + position / 5 + "---" + position % 5); 93 break; 94 95 default: 96 break; 97 } 98 return convertView; 99 } 100 101 private static class ViewHolder1 { 102 TextView content; 103 } 104 105 private static class ViewHolder2 { 106 TextView title; 107 } 108 109 }
其中,需要注意的有:
- private static final int TYPE_TITLE = 0x000;//表示類別Title,從0開始。
- private static final int TYPE_CONTENT = 0x001;//表示類別Content,為1。
- private static final int TYPE_COUNT = 2;//表示類別的數目,本例中只安排了兩種item布局效果。
特別注意:其中類別的int類型表示要從0開始,如果兩種類別分別為1和2的話,會拋出IndexOutOfBoundException,顯示“length is 2, index is 2”的異常信息。
同時,每隔4條content,顯示一個title的布局。即

1 @Override 2 public int getItemViewType(int position) { 3 // TODO Auto-generated method stub 4 if (position % 5 == 0) { 5 return TYPE_TITLE; 6 } else { 7 return TYPE_CONTENT; 8 } 9 }
代碼段的作用。
同時,有兩個static的內部類ViewHolder1和ViewHolder2分別用來保存content和title的可復用TextView控件。然后通過在getView中通過getItemViewType(position)來對不同的布局進行分別處理。
通過將該Adapter設置給listview,運行,初始情況下的效果圖顯示如圖:
此時,恰好顯示了6個item,其中有2個title,4個content布局。此時的LogCat顯示如下 :

1 10-08 15:42:59.061: I/Null ConvertView(30928): 0---Title 2 3 10-08 15:42:59.061: I/Null ConvertView(30928): 0---Title---1---Content 4 5 10-08 15:42:59.061: I/Null ConvertView(30928): 0---Title---2---Content 6 7 10-08 15:42:59.071: I/Null ConvertView(30928): 0---Title---3---Content 8 9 10-08 15:42:59.071: I/Null ConvertView(30928): 0---Title---4---Content 10 11 10-08 15:42:59.071: I/Null ConvertView(30928): 1---Title
此時的記錄結果跟所看到的效果是一致的。然后稍微向上滑動一下listview,達到如下圖所示的效果:
此時,頂部的title並沒有完全隱藏掉,而底部的content也沒有完全顯示出來。再看此時的log,顯示如下 :

1 10-08 15:42:59.061: I/Null ConvertView(30928): 0---Title 2 3 10-08 15:42:59.061: I/Null ConvertView(30928): 0---Title---1---Content 4 5 10-08 15:42:59.061: I/Null ConvertView(30928): 0---Title---2---Content 6 7 10-08 15:42:59.071: I/Null ConvertView(30928): 0---Title---3---Content 8 9 10-08 15:42:59.071: I/Null ConvertView(30928): 0---Title---4---Content 10 11 10-08 15:42:59.071: I/Null ConvertView(30928): 1---Title 12 13 10-08 15:44:13.891: I/Null ConvertView(30928): 1---Title---1---Content
是的,此時又新創建了一個content布局!然后繼續向上滑動,達到如下的效果:
是的,此時第一個title布局已經完全隱藏,但是第一個content布局還沒有完全隱藏,底部的content也還沒有完全顯示出來!再看此時的log,顯示如下:

1 10-08 15:42:59.061: I/Null ConvertView(30928): 0---Title 2 3 10-08 15:42:59.061: I/Null ConvertView(30928): 0---Title---1---Content 4 5 10-08 15:42:59.061: I/Null ConvertView(30928): 0---Title---2---Content 6 7 10-08 15:42:59.071: I/Null ConvertView(30928): 0---Title---3---Content 8 9 10-08 15:42:59.071: I/Null ConvertView(30928): 0---Title---4---Content 10 11 10-08 15:42:59.071: I/Null ConvertView(30928): 1---Title 12 13 10-08 15:44:13.891: I/Null ConvertView(30928): 1---Title---1---Content 14 15 10-08 15:44:45.751: I/Null ConvertView(30928): 1---Title---2---Content
是的,沒錯,又新建了一個content布局!此后,無論再如何滑動listview,title布局和content布局都沒有再生成新的convertView!因為,在初始狀態下,兩個title顯示出來是該listview范圍內最大數目的同時出現title布局,而在滑動的過程中content最大可同時出現數目為6個。
綜述:
listview中通過recycler緩存已經生成的convertView來實現對item中不同的布局的復用。無論是單一類型,還是多種類型的item布局,其原理都是一樣的,同一種布局在滑動的過程中,最多在listview的顯示范圍內能同時顯示的最大數目,即為要生成的convertView的數目,其余就可以有足夠數量的布局來進行復用了。在有多種不同布局的情況下,getView通過首先調用getItemViewType(position)來查找不同類型的布局的緩存。當然,是在正確覆蓋adapter中關鍵方法的前提下,緩存都會正常的工作!