BaseAdapter為自訂ListView、GridView和Spinner版面時,常需要實作的接合器(Adapter)。實作BaseAdapter時必須要謹慎小心,否則將會大大影響到執行效能,可以參考本篇文章提供的方式,來實作出效能不錯的BaseAdapter。
實作BaseAdapter
BaseAdapter為實作ListAdapter和SpinnerAdapter介面(Interface)的抽象類別(Abstract Class),通常我們會建立一個獨立的.java檔案來繼承(Extend)BaseAdapter。繼承了BaseAdapter後,需要實作出以下幾個抽象方法(Abstract Method):
public int getCount(); //取得項目(Item)的數量。通常數量就是從建構子傳入的陣列或是集合大小。
public Object getItem(int position); //取得在這個position位置上的項目(Item)。position通常是資料在陣列或是集合上的位置。
public long getItemId(int position); //取得這個position位置上項目(Item)的ID,一般用position的值即可。
public View getView(int position, View convertView, ViewGroup parent); //通常會設定與回傳convertView作為顯示在這個position位置的項目(Item)的View。
偷懶實作BaseAdapter
如果要快速地實作出BaseAdapter,以在ListView中顯示出JSONArray中的字串為例子,可以寫成以下的程式,:
public class TextAdapter extends BaseAdapter {
private Context context;
private LayoutInflater li;
// 清單的資料,常用一個可變動的陣列或是集合來儲存,在此以JSONArray為例
private JSONArray array;
// 紀錄getView重新排版(inflate)的次數.此為研究觀察用,在實際應用時不需紀錄次數
private static int counter = 0;
public TextAdapter(Context context, JSONArray array) {
this.context = context;
this.array = array;
this.li = LayoutInflater.from(context);
}
@Override
public int getCount() {
return array.length();
}
@Override
public Object getItem(int position) {
try {
return array.getString(position);
} catch (JSONException e) {
return null;
}
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
counter++; //次數+1
convertView = li.inflate(R.layout.list_text, parent, false);
String text = (String) getItem(position);
TextView tv = (TextView) convertView.findViewById(R.id.tv);
tv.setText(text);
Log.i("-getView-", String.valueOf(counter));
return convertView;
}
}
執行結果如下,確實將JSONArray裡存放的字串都顯示出來了:
但是用這種方式的話,每次當ListView變動(拉動捲軸或是資料改變)後,執行覆寫的getView方法時,都需要再inflate一次layout,並且重新使用findViewById抓取layout中View的參考值。拉動一下ListView後,再查看counter變數紀錄的值的話,就會發現inflate的次數居然輕易的就高達上千次。重複的動作實在是做太多了,而且如果layout中的元件很多的話,將會花上不少時間在重新inflate上。當然,如果不是使用inflate Layout的方式來排版,而是自己使用addView方法來排的話,也還是會需要在呼叫getView方法時一直重新addView。因此這樣的寫法會嚴重造成程式執行效能的低落。
建議實作BaseAdapter的方式
先釐清BaseAdapter的資料項目(Item)數量是否會變動
固定的項目數量
一開始在實體化BaseAdapter時,將資料以固定陣列等方式傳進BaseAdapter的建構子中即可。
變動的項目數量
一開始在實體化BaseAdapter時,將資料以可動陣列或是集合的方式傳進BaseAdapter的建構子中即可,往後如果項目數量有變動的話,直接改變陣列或是集合後,使用BaseAdapter提供的notifyDataSetChanged方法來通知View資料已改變。千萬不要重新建立一個新的陣列或是集合,再使用setAdapter的方式將重新new出來的BaseAdapter指定給View,這種作法是效能最差的。
如果預期資料變化不會很大,同一個View應只在初始化時,new出一個BaseAdapter並使用setAdapter方法指定給View
如果預期資料變化不會太大,則只在Activity的onCreate階段或是Fragment的onCreateView階段時,new出一個BaseAdapter並使用setAdapter方法將BaseAdapter指定給View。如果後來資料有變動,使用BaseAdapter的notifyDataSetChanged方法來通知View進行更新即可。為了方便,可以在setAdapter前後,將BaseAdapter的參考以物件變數記下。
使用setAdapter方法後,View的狀態會重置,焦點將會移動到第一個項目;使用notifyDataSetChanged方法後,View的狀態不會重置,僅改變有變動的部份,焦點會保留在原本的位置。
避免一直重新inflate Layout
就是本文一開始提到的懶人實作法出現的問題。事實上,BaseAdapter會紀錄不同位置下的convertView物件。此處所說的位置並不是只資料項目在陣列或是集合內的position,而是資料項目在View上顯示的位置。也就是說,先前已經inflate過Layout,是會儲存在記憶體中的。getView方法所傳入的convertView參數,就是先前這個View的位置上使用getView方法傳回的View,如果沒有,就是null。所以,我們可以利用這已經inflate的convertView,再另外紀錄這convertView下的View的參考,就不用在getView時每次都去inflate Layout和findViewByID,或是addView了!
紀錄convertView下的View的參考的方式,可以另外實作一個class來紀錄,可以利用View所提供的Tag功能,將另外實作並實體化出的類別物件存進View的Tag欄位。
程式可以撰寫成如下:
public class TextAdapter extends BaseAdapter {
private Context context;
private LayoutInflater li;
// 清單的資料,常用一個可變動的陣列或是集合來儲存,在此以JSONArray為例
private JSONArray array;
// 紀錄getView重新排版(inflate)的次數.此為研究觀察用,在實際應用時不需紀錄次數
private static int counter = 0;
public TextAdapter(Context context, JSONArray array) {
this.context = context;
this.array = array;
this.li = LayoutInflater.from(context);
}
@Override
public int getCount() {
return array.length();
}
@Override
public Object getItem(int position) {
try {
return array.getString(position);
} catch (JSONException e) {
return null;
}
}
@Override
public long getItemId(int position) {
return position;
}
private static class ViewHolder {
TextView tv;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
counter++; // 次數+1
convertView = li.inflate(R.layout.list_text, parent, false);
holder = new ViewHolder();
holder.tv = (TextView) convertView.findViewById(R.id.tv);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
String text = (String) getItem(position);
holder.tv.setText(text);
Log.i("-getView-", String.valueOf(counter));
return convertView;
}
}
隨便拉動套用這個BaseAdapter的ListView,發現counter變數並沒有明顯的增長,而且操作上明顯變順了!