AdaptervVew算是Android中比較重要的一類元件,因為涉及到資料和控制元件的互動,所以往往是很麻煩的。作為資料和控制元件的中間媒介adapter(介面卡),它有著多種規定的形態,同時也允許我們能夠自定義adapter的種類,以至於能夠適應不同型別的資料,讓它們正常顯示在view上。adapter中比較難以理解的,就算是我們在自定義adapter的時候,通過繼承baseadapter重寫的getview函式。這個函式的玄機尤其在listview中體現的很好。本文,按照作者學習思考的過程把這個難以理解的知識點講解給大家,希望對大家有用。
(作者:徐冉。文章首發在他的個人部落格。 )
AdapterView&Adapter家族
adapterview就是和資料有關的控制元件,如listview,gridview,spinnerview等等,只要是在展示的過程中需要和資料互動的view都屬於這一類。要使用adapterview,需要遵循三個步驟,這些在之前的博文也已經提到過:
- 建立資料來源
- 建立adapter,並且將資料來源和adapter繫結在一起
- 將裝載好的adapter繫結到我們想要展示的控制元件上面。
資料來源
一般來講可能是string的陣列用來存放一些文字資訊,也可能是int型的陣列用來存放資源的ID,甚至複雜一點的,比如list容器,裡面的元素型別是一個map容器,還有我們手機通訊錄中資料庫檔案中的資訊。這些都可以作為合法的資料來源來進行展示操作。
介面卡
既然有不同的資料來源,那麼自然而然就需要不同的介面卡來裝載和配置這些資料。關於adapter,有兩張神圖,能讓你一下子就明白。
第一張圖片,很好的向你展示了資料來源,介面卡,目的控制元件三者之間是如何工作的以及他們的關係。第二張圖,向我們展示了adapter家族內部分的繼承關係。從圖中我們可以看出,Adapter是最大的一個介面,其次被兩個listadapter和spinneradapter所重寫,到這裡為止還都是介面而已。之後有了一個叫做BaseAdapter的抽象類來重寫這些介面,從baseadater再向下,我們就會發現此時的arrayadapter , simpleadapter, cursoradapterz這三個類,才是我們在程式碼中正常能夠通過例項化物件來使用的類。然而這三個adapter卻各有分工。
- Arrayadapter:它所處理的資料來源,主要是一個string型別的陣列,因為我們在建立adapter例項並且繫結資料來源的時候,我們會同時指定一個展示這些資料的佈局,若要利用系統中比較簡單的佈局的話,只能顯示一行字在textview上面。
- Simlpeadapter:為了滿足我們自定義的需求,這種介面卡繫結的資料來源,可以在一定的限制範圍內,任意的組織我們的原始資料結構,比如list<map<String,Object>>。
- Cursoradapter:主要用於資料庫的內容
展示控制元件
Listview,Gridview, Spinner等等,屬於adapterview型別的都可以。
AdapterView例項展示
以下例項AdapterView用的是ListView
ArrayAdapter
依然是嚴格按照前面的三個步驟來寫程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package com.example.test; import android.app.Activity; import android.app.ListActivity; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.widget.ArrayAdapter; public class MainActivity extends ListActivity { private ArrayAdapter<String>adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //setContentView(R.layout.activity_main); //1建立資料來源 String[] items={"haha","heihei","hehe"}; //2.建立adapter並且繫結資料來源 adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, items); //3.將adapter繫結到view上面 setListAdapter(adapter); } } |
SimpleAdapter
simpleadapter給了我們很大的發揮空間,能夠讓我們自定義資料,自定義展示資料的樣式。
首先,我們要定義一個展示我們每一條資料的xml佈局檔案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" > <ImageView android:layout_marginLeft="20dp" android:layout_marginTop="20dp" android:id="@+id/imageView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/member" /> <TextView android:id="@+id/textView1" android:layout_marginLeft="20dp" android:layout_marginTop="20dp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Large Text" android:textAppearance="?android:attr/textAppearanceLarge" /> </LinearLayout> |
這個佈局檔案,會顯示一個圖片,顯示一個textview,上面將會替換成我們資料來源中對應的內容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
package com.example.test; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import android.app.Activity; import android.app.ListActivity; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.widget.ArrayAdapter; import android.widget.SimpleAdapter; public class MainActivity extends ListActivity { private SimpleAdapter madapter; private List<Map<String, Object>>list; private Map<String, Object>map; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //setContentView(R.layout.activity_main); //1建立資料來源 list = new ArrayList<Map<String,Object>>(); map = new HashMap<String, Object>(); map.put("name", "xuran"); map.put("img", R.drawable.member); list.add(map); map = new HashMap<String, Object>(); map.put("name", "doubi"); map.put("img", R.drawable.member); list.add(map); map = new HashMap<String, Object>(); map.put("name", "hehe"); map.put("img", R.drawable.member); list.add(map); //2.建立adapter並且繫結資料來源 madapter = new SimpleAdapter(this, list,R.layout.layout_simple, new String[]{"name","img"}, new int[]{R.id.imageView1,R.id.imageView1}); //3.將adapter繫結到view上面 setListAdapter(madapter); } } |
SimpleAdapter的實現相對來說複雜一些,無論是資料來源的建立還是介面卡的建立,都和之前的有所區別。 其中需要終點理解的就是adapter的建立並同時繫結資料的過程。這個函式的引數一共有四個:
- 表示adapter所在的上下文
- 資料來源
- 我們要展示資料來源的樣式檔案(xml的佈局檔案,可以用我們自定義的,也可以用系統提供的)
- 後面兩個引數是兩個陣列,前面一個陣列對應著資料來源每一個元素中map容器的兩個鍵值,後面一個陣列代表了我們用於展示資料的佈局檔案中控制元件的id,意思就是每一個資料來源中,和name鍵對應的值,放置在R.id.imageView1上面顯示,和img鍵值對應的資料放置在R.id.imageView1上面,都是一個一一對應的資料對映關係。
這是一個比較複雜的adapter的使用過程,資料型別和介面卡的建立都豐富了很多。
SimpleCursorAdapter
這個介面卡的資料來源和我們之前遇到過的不同,不是我們在程式中現生成的資料,而是讀取的通訊錄中的資訊。手機中是有一個資料庫的檔案來儲存我們通訊錄中聯絡人的各種資訊的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class MainActivity extends ListActivity { private SimpleCursorAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //setContentView(R.layout.activity_main); //1建立資料來源 //通過查詢通訊錄資訊來獲取資料來源,在cursor物件中 Cursor cursor = getContentResolver().query(People.CONTENT_URI, null, null, null, null); //2.建立adapter並且繫結資料來源 adapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_1, cursor, new String[]{People.NAME}, new int[]{android.R.id.text1}); //3.將adapter繫結到view上面 setListAdapter(adapter); } } |
> 這裡需要提出一點的是,因為我們在展示資料的的時候,用的是系統提供的佈局檔案,在這個佈局檔案中有一個textview,他的id是預設好了的text1。所以我們能夠把通訊錄中的人名資訊寫入到那個textview上面。 # 自定義Adapter與GetView函式 ## 為什麼要自定義adapter 有的時候,我們可能不想用系統自帶的adapter,因為能夠例項化物件的adapter類都是從BaseAdapter這個抽象類繼承而來,為了瞭解adapter的內部實現,我們當然可以自己定義的一個類繼承自BaseAdapter的類,來實現我們自己的adapter。另一方面,我們可能會在顯示一些資料的同時新增一些按鈕,並且為他們對映指定的事件。如果有這樣的需求,那麼系統的adapter是不能夠滿足你的,我們可以把資料來源中的資料通過adater對映到控制元件上,但是沒辦法把按鈕的點選事件對映到對應的Button控制元件上。這樣的話,我們就需要深入瞭解一下listview中每一個item是怎麼形成的。 自定義佈局檔案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" > <ImageView android:id="@+id/stu_imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="10dp" android:layout_marginTop="25dp" android:src="@drawable/member" /> <LinearLayout android:layout_width="205dp" android:layout_height="wrap_content" android:layout_marginLeft="25dp" android:layout_marginTop="15dp" android:orientation="vertical" > <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="5dp" android:orientation="horizontal" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="學生ID " /> <TextView android:id="@+id/stu_id" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="1" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="5dp" android:orientation="horizontal" > <TextView android:id="@+id/name_tag" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="學生姓名 " /> <TextView android:id="@+id/stu_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="15dp" android:text="徐冉" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="5dp" android:orientation="horizontal" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="學生年齡 " /> <TextView android:id="@+id/stu_age" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="1" /> </LinearLayout> </LinearLayout> <CheckBox android:id="@+id/stu_checkBox" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="10dp" android:layout_marginTop="15dp" /> </LinearLayout> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
public class MainActivity extends ListActivity { private ListView mListView; // view private MyAdapter mAdapter; // adapter private List<Map<String, Object>> itemsource; // data source private Map<String, Object> mmap; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 1建立資料來源 itemsource = new ArrayList<Map<String,Object>>(); itemsource = getData(); // 2建立adapter mAdapter = new MyAdapter(this); // 3繫結adapter到listview setListAdapter(mAdapter); } private List<Map<String, Object>> getData() { // load data mmap = new HashMap<String, Object>(); mmap.put("id", "1"); mmap.put("name", "徐冉"); mmap.put("age", "21"); mmap.put("img", R.drawable.currency_icon_chf); itemsource.add(mmap); mmap = new HashMap<String, Object>(); mmap.put("id", "2"); mmap.put("name", "大黃"); mmap.put("age", "15"); mmap.put("img", R.drawable.currency_icon_cny); itemsource.add(mmap); mmap = new HashMap<String, Object>(); mmap.put("id", "3"); mmap.put("name", "大史"); mmap.put("age", "23"); mmap.put("img", R.drawable.currency_icon_dkk); itemsource.add(mmap); mmap = new HashMap<String, Object>(); mmap.put("id", "4"); mmap.put("name", "二軒"); mmap.put("age", "28"); mmap.put("img", R.drawable.currency_icon_eur); itemsource.add(mmap); mmap = new HashMap<String, Object>(); mmap.put("id", "5"); mmap.put("name", "幹順"); mmap.put("age", "21"); mmap.put("img", R.drawable.currency_icon_sek); itemsource.add(mmap); mmap = new HashMap<String, Object>(); mmap.put("id", "6"); mmap.put("name", "趙龍"); mmap.put("age", "21"); mmap.put("img", R.drawable.currency_icon_nzd); itemsource.add(mmap); mmap = new HashMap<String, Object>(); mmap.put("id", "7"); mmap.put("name", "富美"); mmap.put("age", "18"); mmap.put("img", R.drawable.currency_icon_myr); itemsource.add(mmap); mmap = new HashMap<String, Object>(); mmap.put("id", "8"); mmap.put("name", "小灰灰"); mmap.put("age", "91"); mmap.put("img", R.drawable.currency_icon_nok); itemsource.add(mmap); return itemsource; } public class ViewHolder { TextView mid; TextView mname; TextView mage; ImageView mimg; CheckBox mcheck; } public class MyAdapter extends BaseAdapter { private LayoutInflater inflater; private ViewHolder holder; public MyAdapter(Context context) { // Obtains the LayoutInflater from the given context. inflater = LayoutInflater.from(context); holder = new ViewHolder(); // TODO Auto-generated constructor stub } @Override public int getCount() { // TODO Auto-generated method stub return itemsource.size(); } @Override public Object getItem(int position) { // TODO Auto-generated method stub return itemsource.get(position); } @Override public long getItemId(int position) { // TODO Auto-generated method stub return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { // TODO Auto-generated method stub if (convertView == null) { convertView = inflater.inflate(R.layout.listview_item, null); holder.mid = (TextView) convertView.findViewById(R.id.stu_id); holder.mname = (TextView) convertView .findViewById(R.id.stu_name); holder.mage = (TextView) convertView .findViewById(R.id.stu_age); holder.mcheck = (CheckBox) convertView .findViewById(R.id.stu_checkBox); holder.mimg = (ImageView) convertView .findViewById(R.id.stu_imageView); convertView.setTag(holder); } else holder = (ViewHolder) convertView.getTag(); //投放資料 holder.mid.setText((String) itemsource.get(position).get("id")); holder.mname .setText((String) itemsource.get(position).get("name")); holder.mage.setText((String) itemsource.get(position).get("age")); holder.mimg.setImageResource((Integer) itemsource.get(position).get("img")); return convertView; } } } |
使用自定義的adapter和使用系統提供的adapter步驟是一樣的,在這裡就不多說了。首先主要說一下我們自己實現的adapter類,以及listview是怎麼形成。
自己實現adapter類
通過繼承BaseAdapter來實現我們自己的adapter類,同時我們必須重寫baseadapter裡面所提供的成員方法。為什麼我們要把這個類定義在Activity的類中,是因為如果我們再定義一個單獨的類的話,adapter這個類中需要用到Activity類中的一些資料,傳遞起來很麻煩,還有一個原因就是,這個類本來也就是在這個activity中用,所以定義為一個內部類是比較方便的。
ListView是怎麼形成的
listview在開始繪製之前,首先要呼叫getcount這個函式,這個函式返回了我們要在這個Listview上面顯示多少個item,有了這個資料我們才能夠繼續繪製Listview. 然後根據這個長度開始呼叫getview函式進行繪製listview中的每一行。
重中之重—GetView函式詳解
我們一般在給一個activity設定一個檢視的時候,都用的是setcontentview這個方法來直接指定佈局檔案,但是activity中早已經內建了指定檢視的工具—-LayoutInflater.這個工具就像是一個壓力泵,能夠把佈局檔案壓縮成一個檢視,呈現出來。它的作用類似於 findViewById(), 不同點是LayoutInflater是用來找layout下xml佈局檔案,並且例項化!而findViewById()是找具體xml下的具體 widget控制元件.
函式一共有三個引數,position標識我們現在正在繪製listview中第幾個item,converview相當於一個view控制元件的快取裝置,它將我們定義好顯示沒一行item的佈局檔案壓縮成一個檢視,佈局中的部分view都在它裡面。在建立adapter的時候,通過adapter的建構函式,定義了一個LayoutInfalter,並且獲取到當前activity的LayoutInflater。之後,通過inflater壓縮xml檔案形成一個檢視,賦值convertview。因為一個佈局檔案中所有的控制元件展示,都是一個item,因為佈局中所有的控制元件應該以一個整體出現。所以,我們定義了一個class–>ViewHolder,裡面的成員就是我們沒一行item佈局檔案中的控制元件集合。通過findviewbyid我們找到了佈局當中的每一個view,並且最後把相應的資料投放在view上面予以顯示。
細心觀察的人會發現,getview函式裡面有兩句話貌似作用不是很明確:
1 2 |
convertView.setTag(holder); holder = (ViewHolder) convertView.getTag(); |
我們都知道Listview中有很多的item,數量少的話,我們每次在繪製listview的每一行的時候,都需要重新findviewbyid來查詢並且建立一系列item裡面所需要的view。那麼如果我們的listview有幾億個item呢,不但findviewbyid會花費大量的時間,如果每一個item都重新inflate的話,那我們的記憶體空間也受不了。實際上Android為我們提供了一套重複利用的機制叫做“Recycler”。
在一個完整的ListView第一次出現時,每個Item都是Null的,listview建立的item數,只能夠充滿螢幕為止。假設一屏上最多顯示7個item。當我們滑動Listview的時候,舊的item滑出去,新的item滑進來,那麼這個滑進來滑出去的過程,在程式的內部是怎麼實現的呢。
先來看張圖:
我之前在前面說到,convertview相當於一個快取器,在程式剛開始執行,第一屏listview的所有item出現在螢幕中時,convertView為零。隨後,如果我們向上滑動,item1滑出螢幕,item8滑入螢幕,此時convertview將之前滑出去的檢視的資訊記錄下來,當有新的item滑進來,如果他們所用的都是同一個xml佈局檔案壓縮成的view檢視,那麼我們就不再去重新inflate一個檢視,直接利用剛剛滑出去的那個item的檢視,更新一下資料就可以了。這樣的話,就能夠實現重複利用,不用再多花費時間和空間。
當item2也滑出去的時候,item9滑進來,item9得到的將是item2之前的檢視,然後更新一下資料即可。以後一直按照這樣的規律:item10—item3, item11—item4。
雖然我們已經重複利用了之前繪製的檢視,但是在更新資料之前,我們總得通過findviewbyid來找到相應的控制元件進行資料更新操作。為了能夠再次優化這一部分,我們在第一屏listview的item建立的時候,就執行settag函式,它為每一個檢視繫結一個viewholder物件,每一個item檢視對應這麼一個viewholder物件,當第一屏的listview繪製完成的時候,每一個檢視都攜帶一個查詢完成的viewholder物件,這個viewholder物件的成員裡面已經存好了對應檢視內的控制元件(如textview,button)。當convertView不為0時(已經開始滑動),重複利用已經建立的view檢視的時候,使用getTag()方法獲取繫結的ViewHolder物件,這樣就避免了findViewById對控制元件的層層查詢,而是快速定位到控制元件。
可以在getview函式中打出相應的log資訊來觀察規律
總結
Adapter和AdapterView還是android中比較複雜的部分,像listview就是一個很好的例子,listview還有很多優化的方式,在面試中也經常會考到,如果不優化,很可能就會造成卡頓的現象讓使用者的體驗不好。我一開始就錯在加了gettag函式沒有加settag函式,因為當時並不明白這是什麼意思。所以學知識,還是要保證一個強烈的求知慾,不能為了完成任務就去copy一些程式碼,效果是出來了,但是原理你不明白,下次碰到變形的情況還是不會,如果一旦明白了原理,說不準在會的基礎上,還能夠加以改進,得到得到更好的效果。