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變數並沒有明顯的增長,而且操作上明顯變順了!