關於ListView中convertView的緩存個數的探究


在面試的時候經常會被問到一個有關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>
View Code

我將其中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>
View Code

同時也是為了驗證測試方便的緣故,將其中的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     }
View Code

其中的代碼塊

  • 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
View Code

記錄顯示跟我們的預計相符。

此時一定要注意的是:因為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
View Code

對,就是又打印出了一條記錄!並且,此后無論如何滑動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>
View Code

為了與之前的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     }
View Code

其中,需要注意的有:

  • 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         }
View Code

代碼段的作用。

同時,有兩個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
View Code

此時的記錄結果跟所看到的效果是一致的。然后稍微向上滑動一下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
View Code

是的,此時又新創建了一個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
View Code

是的,沒錯,又新建了一個content布局!此后,無論再如何滑動listview,title布局和content布局都沒有再生成新的convertView!因為,在初始狀態下,兩個title顯示出來是該listview范圍內最大數目的同時出現title布局,而在滑動的過程中content最大可同時出現數目為6個。

綜述:

listview中通過recycler緩存已經生成的convertView來實現對item中不同的布局的復用。無論是單一類型,還是多種類型的item布局,其原理都是一樣的,同一種布局在滑動的過程中,最多在listview的顯示范圍內能同時顯示的最大數目,即為要生成的convertView的數目,其余就可以有足夠數量的布局來進行復用了。在有多種不同布局的情況下,getView通過首先調用getItemViewType(position)來查找不同類型的布局的緩存。當然,是在正確覆蓋adapter中關鍵方法的前提下,緩存都會正常的工作!

測試用工程源碼下載


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM