Android 介面卡模式(ListView與Adapter)
Android 設計模式系列文章Android 23種設計模式
一、前言
介面卡模式就是將兩個不相容的類融合在一起。通過轉換使他們可以相容的工作。Android程式碼中最常見的介面卡就是Adapter了。ListView、GridView、RecyclerView都使用Adapter,Adapter的作用都一樣,把高度定製化的item view和ListView分開。item view通過一個Adapter和ListView聯絡到一起。解耦而不失高度可定製。
二、介面卡模式定義
將一個類的介面轉換成客戶希望的另一個介面。介面卡模式讓那些介面不相容的類可以一起工作
三、例子
我們先來看下介面卡模式的例子。學習到底什麼是介面卡模式。
3.1、我們舉一個出水口出水量的例子。出水量有大有小,於是先定義兩個介面
public interface BigOutlet { public void bigOutlet(); } public interface SmallOutlet { public void smallOutlet(); }
3.2、然後有一個出水口water tap。出水量大。
public class BigWaterTap implements BigOutlet { private static final String TAG = WaterTap.class.getSimpleName(); @Override public void bigOutlet() { Log.d(TAG,"bigOutlet"); } }
3.3、定義介面卡
現在需求來了,我要出水口既能大量出水,也可以小量出水。而我們不能去更改BigWaterTap,因為通常很多時候一個類擬定好了過後,我們無法再去修改了。也沒有原始碼。給它再繼承SmallOutlet這個介面。我們需要的是另外的辦法來添加出水量小的方法。這個時候介面卡模式就派上用場了。
介面卡模式寫法有兩種,這裡先看第一種寫法叫類介面卡模式
類介面卡模式
public class ClassWaterTapAdapter extends BigWaterTap implements SmallOutlet { private static final String TAG = ClassWaterTapAdapter.class.getSimpleName(); @Override public void smallOutlet() { Log.d(TAG,"smallOutlet"); } }
呼叫
ClassWaterTapAdapter classWaterTapAdapter = new ClassWaterTapAdapter(); classWaterTapAdapter.bigOutlet(); classWaterTapAdapter.smallOutlet();
輸出這裡就省略了。我們可以看到介面卡模式就是把兩個不相容的類結合到了一起,即可以出水量大,也可以出水量小了。達到了融合的作用。而不用去改變原來的類。然後看下另一種寫法。物件介面卡模式,其實就是代理模式的寫法。
物件介面卡模式
public class ProxyWaterTapAdapter implements SmallOutlet { private static final String TAG = ProxyWaterTapAdapter.class.getSimpleName(); private BigWaterTap bigWaterTap; public ProxyWaterTapAdapter(BigWaterTap bigWaterTap) { this.bigWaterTap = bigWaterTap; } public void adapterBigOutlet() { bigWaterTap.bigOutlet(); } @Override public void smallOutlet() { Log.d(TAG,"smallOutlet"); } }
呼叫
ProxyWaterTapAdapter proxyWaterTapAdapter = new ProxyWaterTapAdapter(new BigWaterTap()); proxyWaterTapAdapter.adapterBigOutlet(); proxyWaterTapAdapter.smallOutlet();
一目瞭然,就是用代理的方式,擁有BigWaterTap來呼叫了bigOutlet。ProxyWaterTapAdapter就達到了相容的目的。
4、小結
1、現在我們隊介面卡模式有個清晰的認識了。介面卡就是不改變原有類的基礎上,讓它相容別的介面方法,以實現新的功能,達到相容的目的。
2、在我們開發過程中,筆者強烈建議最好還是使用物件介面卡模式這種寫法。物件介面卡模式比類物件適配模式好處就是更加靈活,且不會暴露被適配者。因為繼承了過後Adapter類中也有了一樣的方法。
四、ListView與介面卡模式
4.1、為了避免讀者混淆,我先簡單用上面的例子模擬一下Listview和介面卡ListAdapter是如何工作的。
先寫介面卡ListWaterTapAdapter,等價於ListAdapter
public abstract class ListWaterTapAdapter implements SmallOutlet{ public abstract void middleOutlet(); }
ListWaterTapAdapter抽象類,需要我們客戶使用的時候具體去實現。然後重新寫一下BigWaterTap,因為它本身有的功能就是bigOutlet()。等價於listview
public class ListViewWaterTap implements BigOutlet{ private static final String TAG = ListViewWaterTap.class.getSimpleName(); private ListWaterTapAdapter listWaterTapAdapter; @Override public void bigOutlet() { Log.d(TAG,"bigOutlet"); } public void setAdapter(ListWaterTapAdapter listWaterTapAdapter) { this.listWaterTapAdapter = listWaterTapAdapter; } public void smallToBigOutlet() { listWaterTapAdapter.smallOutlet(); int i = 100; while (i-- > 0); listWaterTapAdapter.middleOutlet(); } }
這裡的ListViewWaterTap並非介面卡。我們的介面卡就是ListWaterTapAdapter。setAdapter來擁有介面卡抽象類,呼叫抽象方法,讓客戶自己去實現smallOutlet和middleOutlet這兩個方法。接著我們看下呼叫
ListViewWaterTap listViewWaterTap = new ListViewWaterTap(); ListWaterTapAdapter listWaterTapAdapter = new ListWaterTapAdapter() { @Override public void middleOutlet() { Log.d(TAG,"middleOutlet"); } @Override public void smallOutlet() { Log.d(TAG,"smallOutlet"); } }; listViewWaterTap.setAdapter(listWaterTapAdapter); listViewWaterTap.bigOutlet(); listViewWaterTap.smallToBigOutlet();
這裡就是和listview與adapter一樣的用法了。並不是標準的介面卡模式寫法。但確實最經典的介面卡模式應用。下面我們來分析一下listview和adapter的關係。
4.2、ListView和ListAdapter
首先說下ListView用介面卡模式的目的就是讓listview的每個item可以客戶自己高度定製化。你自己去實現就行了。無論你如何定製item使用就是一個view。listview用介面卡模式就完美的達到了這個效果。
以下有射獵原始碼的地方,來自Android P。"..."代表省略程式碼
4.2.1、首先來一個普通示例
listView = (ListView) findViewById(R.id.listView); MyAdapter myAdapter = new MyAdapter(this,mListTile); public class MyAdapter extends BaseAdapter { private LayoutInflater layoutInflater; List<String> litTile; public MyAdapter(Context context, List<String> listTile) { layoutInflater = LayoutInflater.from(context); this.litTile = listTile; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { holder = new ViewHolder(); convertView = layoutInflater.inflate(R.layout.layout_item,null); holder.title = (TextView) convertView.findViewById(R.id.title); convertView.setTag(holder); } else { holder = (ViewHolder)convertView.getTag(); } holder.title.setText(litTile.get(position)); return convertView; } class ViewHolder{ public TextView title; } // 篇幅原因,省略getCount、getItem和getItemId
這是我們最普通的運用listview的寫法了。然後我們從介面卡講起,先看adapter是個啥
listview.java的setAdapter方法
@Override public void setAdapter(ListAdapter adapter) { ... }
看看BaseAdapter的整合關係,然後開始講介面卡BaseAdapter
public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter { ... } public interface ListAdapter extends Adapter { ... } public interface Adapter { ... }
4.2.2、介面卡
為了節約篇幅我把註釋刪了。看Adapater程式碼如下:
public interface Adapter { void registerDataSetObserver(DataSetObserver observer); void unregisterDataSetObserver(DataSetObserver observer); int getCount(); Object getItem(int position); long getItemId(int position); boolean hasStableIds(); View getView(int position, View convertView, ViewGroup parent); static final int IGNORE_ITEM_VIEW_TYPE = AdapterView.ITEM_VIEW_TYPE_IGNORE; int getItemViewType(int position); int getViewTypeCount(); static final int NO_SELECTION = Integer.MIN_VALUE; boolean isEmpty(); default @Nullable CharSequence[] getAutofillOptions() { return null; } }
1>根據上面的繼承關係,BaseAdapter跟開始舉例一樣,抽象類來定義介面卡,要實現的介面是Adapter
2> 可以看到Adapter就是介面,介面的這些方法是要提供給ListView內部使用的。我們自己實現Adapter,完成這些介面,或者抽象方法。
4.2.3、listview如何使用adapter
首先看下listView的繼承關係
public class ListView extends AbsListView { ... } public abstract class AbsListView extends AdapterView<ListAdapter> implements TextWatcher, ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener, ViewTreeObserver.OnTouchModeChangeListener, RemoteViewsAdapter.RemoteAdapterConnectionCallback { ... } public abstract class AdapterView<T extends Adapter> extends ViewGroup { ... }
ListView就是一個ViewGroup,然後通過主要的邏輯實現程式碼就在ListView.java和AbsListView.java了
先講一個getCount
在AbsListView.java的onAttachedToWindow方法
protected void onAttachedToWindow() { super.onAttachedToWindow(); ... if (mAdapter != null && mDataSetObserver == null) { mDataSetObserver = new AdapterDataSetObserver(); mAdapter.registerDataSetObserver(mDataSetObserver); // Data may have changed while we were detached. Refresh. mDataChanged = true; mOldItemCount = mItemCount; mItemCount = mAdapter.getCount(); } }
把view關聯到window的時候,mAdapter.getCount(),這個getConut是我們繼承時寫的,就確定了我們這個listView有多少個item了。
再梳理一下getView是如何把item view加載出來的
大致流程如下,這裡就只列出簡化的程式碼了,本文主要理解介面卡模式的思想
1>AbsListView:onlayout
protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); ... layoutChildren(); .... }
ViewGroup是組合模式,它在呼叫onlayout的時候呼叫layoutChildren來佈局子控制元件,layoutChildren在AbsListView是一個空實現,實現程式碼在ListView
2>ListView:layoutChildren
protected void layoutChildren() { ... switch (mLayoutMode) { ... case LAYOUT_FORCE_BOTTOM: sel = fillUp(mItemCount - 1, childrenBottom); adjustViewsUpOrDown(); break; case LAYOUT_FORCE_TOP: mFirstPosition = 0; sel = fillFromTop(childrenTop); adjustViewsUpOrDown(); break;
呼叫到layoutChildren後來佈局item view.
3>ListView:fillDown
private View fillDown(int pos, int nextTop) { ... while (nextTop < end && pos < mItemCount) { // is this the selected item? boolean selected = pos == mSelectedPosition; View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected); nextTop = child.getBottom() + mDividerHeight; if (selected) { selectedView = child; } pos++; }
每個子view都是呼叫makeAndAddView然後呼叫AbsListView的obtainView方法
private View makeAndAddView(int position, int y, boolean flow, int childrenLeft, boolean selected) { ... final View child = obtainView(position, mIsScrap);
4>AbsListView:obtainView
View obtainView(int position, boolean[] outMetadata) { ... final View scrapView = mRecycler.getScrapView(position); final View child = mAdapter.getView(position, scrapView, this); if (scrapView != null) { if (child != scrapView) { // Failed to re-bind the data, return scrap to the heap. mRecycler.addScrapView(scrapView, position); } else if (child.isTemporarilyDetached()) { outMetadata[0] = true; // Finish the temporary detach started in addScrapView(). child.dispatchFinishTemporaryDetach(); } } ... }
這裡用到了mAdapter.getView。從ListView佈局每個item view的過程來看,最後佈局使用view的時候就用到了我們去實現的getView方法返回的view。
我們對listview優化的時候,為啥寫法是判斷if (convertView == null)
接著分析上面第四步的程式碼
mRecycler.getScrapView是獲得可複用的view,然後帶入mAdapter.getView(position, scrapView, this);
如果還沒有被加入到快取list則
if (child != scrapView) { // Failed to re-bind the data, return scrap to the heap. mRecycler.addScrapView(scrapView, position); }
所以我們寫程式碼的時候則這樣來優化判斷,當然獲得快取後資料也是原來的。所以我們要重新設定title
if (convertView == null) { holder = new ViewHolder(); convertView = layoutInflater.inflate(R.layout.layout_item,null); holder.title = (TextView) convertView.findViewById(R.id.title); convertView.setTag(holder); } else { holder = (ViewHolder)convertView.getTag(); } holder.title.setText(litTile.get(position));
這裡ListView和ListAdapter的關係就梳理到這裡了。有興趣的同學還可以去看看GridView,RecyleView喔。比如RecycleView就是ListView的一個升級版,RecycleView定義了ViewHolder的機制。更加巧妙。
五、總結
介面卡模式就是將原本不相容的介面融合在一起,以便更好的協同合作。當然設計模式不是一成不變的,litview的adapter就是很好的一個變化,讓UI更加高度可定製化而不失自身實現。
優點:
1、把介面和類結合,通過介面卡可以讓介面定義的功能更好的複用。
2、擴充套件性好,不光可呼叫自己開發的功能,還自然的擴充套件了介面定義的其它功能。
缺點:
不易濫用,如果你的程式碼有n多個介面卡,你想想那場面,呼叫十分凌亂,還不如直接修改原始碼設計更好的public api。
還是那句話,設計模式主要理解它的精髓。並不是一成不變。多思考是否應該使用這個設計模式才能事半功倍。