1.getCount()方法:html
android提供了N多已經封裝好的適配器,但用得最多仍是BaseAdapter。若是寫一個類繼承BaseAdapter,則會看到它至少要覆寫四個方法:java
public class MAdapter extends BaseAdapter{ @Override public int getCount() { // TODO Auto-generated method stub return 0; } @Override public Object getItem(int position) { // TODO Auto-generated method stub return null; } @Override public long getItemId(int position) { // TODO Auto-generated method stub return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { // TODO Auto-generated method stub return null; } }
首先說一下getCount()方法,該方法返回一個int型的值,表示你的組件(ListView,GridView等)的長度,也就是要顯示的item的數量。若是你的getCount()返回值是0的話,列表一行都不會顯示,若是返回1,就只顯示一行。返回幾則顯示幾行。咱們通常會向adapter傳遞咱們的數據項list(好比mList),通常getCount()方法會這麼寫:android
@Override public int getCount() { // TODO Auto-generated method stub if (mList != null) return mList.size(); else return 0; }
但有時,好比你使用ListView,佈局給它的位置能顯示10個childitem,當傳入的mList數量小於10,這個listView就只顯示當前的子項數目而沒法佔滿空間。有時爲了美觀,會讓ListView至少顯示10個(此時要本身處理,把多出來的子項顯示爲默認樣式),因此也會這樣寫:緩存
private static final int ITEM_COUNT = 10; @Override public int getCount() { // TODO Auto-generated method stub if(mList != null && mList.size() > 10) return mList.size(); else { return ITEM_COUNT; } }
2.getView方法(重點是convertView參數的緩存機制)ide
那麼以ListView爲例,來講下系統如何繪製ListView(也就是如何加載,其餘須要適配器的組件均相似)。ListView 針對每一個item,要求 adapter 「返回一個視圖」 (getView()方法),也就是說ListView在開始繪製的時候,系統首先調用getCount()函數,根據他的返回值獲得ListView的長度,而後根據這個長度,調用getView()一行一行的繪製ListView的每一項。因而,咱們來看看getView()方法:函數
@Override public View getView(int position, View convertView, ViewGroup parent) { // TODO Auto-generated method stub return null; }
這個方法會返回一個View,顯示出來就是ListView的一個子項(Child)。該方法有3個參數:佈局
position:從0開始,能夠理解爲adapter中數據集合的下標。一般狀況下,這個position和你傳入adapter的mList.get(position)中的position對應使用。性能
convertView:這個convertView其實就是最關鍵的部分了。原理上講,當ListView滑動的過程當中 會有item被滑出屏幕 而再也不被使用 這時候Android會回收這個條目的view,這個view也就是這裏的convertView。優化
其實到此爲止咱們能夠總結出convertview的機制了,就是在初始顯示的時候,每次顯示一個item都調用一次getview方法可是每次調用的時候covertview爲空(由於尚未舊的view),當顯示完了以後。若是屏幕移動了以後,而且致使有些Item(也能夠說是view)跑到屏幕外面,此時若是還有新的item須要產生,則這些item顯示時調用的getview方法中的convertview參數就不是null,而是那些移出屏幕的view(舊view),咱們所要作的就是將須要顯示的item填充到這些回收的view(舊view)中去,最後注意convertview爲null的不只僅是初始顯示的那些item,還有一些是已經開始移入屏幕可是尚未view被回收的那些item。ui
上圖中,當item1滾出屏幕,而且一個新的項目從屏幕低端上來時,ListView再請求一個View。convertView此時不是空值了,它的值是item1。你只需設定新的數據而後返回convertView,沒必要從新new一個View。
最後一個parent參數暫時沒用到過。
不使用convertView的寫法,每次都會new一個View,邏輯上沒有任何問題,可是數據量很大的話,動不動就會OOM:
public View getView(int position, View convertView, ViewGroup parent) {
View view = new View(); //經過inflate等找到佈局 而後findViewById等 設置各個顯示的item return view; }
使用convertView,節省了new View的大量開銷:
public View getView(int position, View convertView, ViewGroup parent) { View view = null; if (convertView != null) { view = convertView; //複用了回收的view 只須要直接做內容填充的修改就行了 } else { view = new Xxx(...); //沒有供複用的view 按通常的作法新建view } return view; }
3.ViewHolder和getTag()、setTag()方法:
先上一份使用ViewHolder自定義adapter的典型寫法:
public class MarkerItemAdapter extends BaseAdapter { private Context mContext = null; private List<MarkerItem> mMarkerData = null; public MarkerItemAdapter(Context context, List<MarkerItem> markerItems) { mContext = context; mMarkerData = markerItems; } public void setMarkerData(List<MarkerItem> markerItems) { mMarkerData = markerItems; } @Override public int getCount() { int count = 0; if (null != mMarkerData) { count = mMarkerData.size(); } return count; } @Override public MarkerItem getItem(int position) { MarkerItem item = null; if (null != mMarkerData) { item = mMarkerData.get(position); } return item; } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder viewHolder = null; if (null == convertView) { viewHolder = new ViewHolder(); LayoutInflater mInflater = LayoutInflater.from(mContext); convertView = mInflater.inflate(R.layout.item_marker_item, null); viewHolder.name = (TextView) convertView.findViewById(R.id.name); viewHolder.description = (TextView) convertView .findViewById(R.id.description); viewHolder.createTime = (TextView) convertView .findViewById(R.id.createTime); convertView.setTag(viewHolder); } else { viewHolder = (ViewHolder) convertView.getTag(); } // set item values to the viewHolder: MarkerItem markerItem = getItem(position); if (null != markerItem) { viewHolder.name.setText(markerItem.getName()); viewHolder.description.setText(markerItem.getDescription()); viewHolder.createTime.setText(markerItem.getCreateDate()); } return convertView; } private static class ViewHolder { TextView name; TextView description; TextView createTime; } }
getTag()、setTag()方法很簡單:View中的setTag(Object)表示給View添加一個額外的數據,之後能夠用getTag()將這個數據取出來。
能夠看到ViewHolder就是一個內部類。當convertView爲null時,老老實實加載xml佈局文件,而後用findViewById把佈局文件中的各個組件對應到ViewHolder的各個屬性,並把ViewHolder用setTag()方法綁定到convertView。當convertView不爲空能夠複用時,直接使用getTag()方法取出綁定的ViewHolder,省去了findViewById()的開銷。
4.getItemViewType(int position)和getViewTypeCount()方法
可是上面的程序仍然有缺陷——當咱們的ListView中填充的item有多種形式時,好比微博中,有的item中包含圖片,有的item包含視頻,那麼必然的,咱們須要用到2種item的佈局方式。此時若是隻是單純判斷convertView是否存在,會形成回收的view不符合你當前須要的佈局,而相似轉換失敗出錯退出。這裏要提到Adapter中的另外2個方法:
public int getItemViewType(int position) {}
public int getViewTypeCount() {}
從方法名上 就能夠比較明顯的明白這2個的做用
下面附上一個demo代碼:
class MyAdapter extends BaseAdapter{ Context mContext; LinearLayout linearLayout = null; LayoutInflater inflater; TextView tex; final int VIEW_TYPE = 2; final int TYPE_1 = 0; final int TYPE_2 = 1; public MyAdapter(Context context) { mContext = context; inflater = LayoutInflater.from(mContext); } @Override public int getCount() { return listString.size(); } //每一個convert view都會調用此方法,得到當前所須要的view樣式 @Override public int getItemViewType(int position) { int p = position%6; if(p == 0) return TYPE_1; else if(p < 3) return TYPE_2; else return TYPE_1; } @Override public int getViewTypeCount() { return 2; } @Override public Object getItem(int arg0) { return listString.get(arg0); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { viewHolder1 holder1 = null; viewHolder2 holder2 = null; int type = getItemViewType(position); //無convertView,須要new出各個控件 if(convertView == null) { //按當前所需的樣式,肯定new的佈局 switch(type) { case TYPE_1: convertView = inflater.inflate(R.layout.listitem1, parent, false); holder1 = new viewHolder1(); holder1.textView = (TextView)convertView.findViewById(R.id.textview1); holder1.checkBox = (CheckBox)convertView.findViewById(R.id.checkbox); convertView.setTag(holder1); break; case TYPE_2: convertView = inflater.inflate(R.layout.listitem2, parent, false); holder2 = new viewHolder2(); holder2.textView = (TextView)convertView.findViewById(R.id.textview2); holder2.imageView = (ImageView)convertView.findViewById(R.id.imageview); convertView.setTag(holder2); break; } } else { //有convertView,按樣式,取得不用的佈局 switch(type) { case TYPE_1: holder1 = (viewHolder1) convertView.getTag(); break; case TYPE_2: holder2 = (viewHolder2) convertView.getTag(); break; } //設置資源 switch(type) { case TYPE_1: holder1.textView.setText(Integer.toString(position)); holder1.checkBox.setChecked(true); break; case TYPE_2: holder2.textView.setText(Integer.toString(position)); holder2.imageView.setBackgroundResource(R.drawable.icon); break; } } return convertView; } } //各個佈局的控件資源 class viewHolder1{ CheckBox checkBox; TextView textView; } class viewHolder2{ ImageView imageView; TextView textView; }
以上基本就是主要的內容了,下面再補充實際操做當中的一些Tips
*若是convertView上用Type區分有些繁瑣,或者不須要那麼複雜,只是不多有出現不一樣的狀況,那麼還能夠在取得convertView後,經過java提供的instanceof來判斷是否能夠強轉,若是不能強轉,就去新建一個View的作法,可是其實這種作法並不規範,因此仍是推薦上面的作法。
*第二個是關於ListView,對於純色的item背景,其實能夠直接設置BackgroundColor,而不要使用圖片,這一部分其實能夠有不小的提高,一樣的,對於任何純色的背景,應該儘可能去設置RGB顏色,而不是全用一張圖片作背景返回。
5.在onItemClickLinstener()中監聽點擊事件,更改Item背景的問題
我之前都是這麼寫的
mListView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
view.findViewById(R.id.background).setBackgroundResource(R.drawable.bg_seleted); } });
onItemClick()方法中第二個參數view就是mListView使用的adapter中,getView()方法返回的View,第三個參數position也對應getView()方法的position參數。這樣寫乍看之下沒有任何問題,可是若是在adapter中使用了convertView來優化適配器,那麼getView()方法返回的極可能是緩存的convertView。這彷佛也沒有問題,由於每次點擊時的item必然已經加載在可見的位置。可是,若是要這樣,我作了一個橫向的list,若是要他來顯示某些配置信息:
如上圖,點擊item以後就會使背景變化(加上白色背景框了),再點擊以後背景又會回到默認狀態,並且一頁最多顯示5個item。好,如今我把第二個、第三個,以及第30個圖標item點亮了,並把這些信息保存起來,以便下次進入程序時仍然讓這幾個item是點亮的狀態。因而,下次進入程序時,初始化階段我須要讀取這些保存的信息,把須要點亮的圖標item繪製出來。怎麼作呢?我是這樣作的:
mListView.getChildAt(idex).findViewById(R.id.background).setBackgroundResource(R.drawable.bg_seleted);
好,問題來了,空指針!
加載第2、三個item是沒有問題的,由於進入程序時listview會首先加載最早顯示的5個item,第6個呢,可能也會加載,這樣滑動的時候纔不至於措手不及。但是第30個就難說了。也許滑動到第二十多個的時候纔會加載——到了須要使用的時候才加載,這是性能最優的選擇。可是在初始化的時候mListView.getChild(30)返回就是null,很差意思,程序崩了……
怎麼辦呢?
被領導罵了無數次以後,終於認識到不能用上面那種語句。先回過頭來,看一下本文第3個標題下的那個典型BaseAdapter寫法,就是那個MarkerItemAdapter,它的構造中傳入了一個List<MarkerItem>,這個就是一般要顯示在listView裏的數據表,在MarkerItem類裏曾加一條屬性吧,好比int state = 0;在onItemClick()裏改變state的值:
markerItems.get(position).setState(1);
而後在adapter的getView()方法中根據item的state屬性來加載對應的背景便可。
@Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder viewHolder = null; if (null == convertView) { viewHolder = new ViewHolder(); LayoutInflater mInflater = LayoutInflater.from(mContext); convertView = mInflater.inflate(R.layout.item_marker_item, null); viewHolder.name = (TextView) convertView.findViewById(R.id.name); viewHolder.description = (TextView) convertView .findViewById(R.id.description); viewHolder.createTime = (TextView) convertView .findViewById(R.id.createTime); convertView.setTag(viewHolder); } else { viewHolder = (ViewHolder) convertView.getTag(); } // set item values to the viewHolder: MarkerItem markerItem = getItem(position); if (null != markerItem) { viewHolder.name.setText(markerItem.getName()); viewHolder.description.setText(markerItem.getDescription()); viewHolder.createTime.setText(markerItem.getCreateDate()); if (markerItem.getState() == 0) { holder.bgLayout.setBackgroundResource(R.drawable.bg_default); } else if (markerItem.getState() == 1) { holder.bgLayout.setBackgroundResource(R.drawable.bg_seleted); } } return convertView; }
這樣,到了須要加載的時候才加載,並且能加載出正確的數據。因此在操做有適配器的組件時,務必直接更改本身的數據集,而不是更改listView等的itemView纔是最穩妥的作法。最後不要忘了在點擊事件最後通知adapter更新:
mAdapter.notifyDataSetChanged();
多麼痛的領悟……
參考博文:1.http://blog.chinaunix.net/uid-11798215-id-3407345.html
2.http://mzh3344258.blog.51cto.com/1823534/889879
3.http://www.cnblogs.com/over140/archive/2011/03/23/1991100.html
4.http://www.cnblogs.com/mengdd/p/3254323.html