名字有點唬人,其實就是組合了幾個封裝類可以方便實現RecyclerView
的多視圖,畢竟「框架」這個詞在我看來仍是指具備必定規模量級及重點技術的代碼體系,但僅就解決特定問題而言也不妨被冠以這個名號。同時它真的是「超輕量」總共不過4個類,不超過130行代碼~java
咱們已經有了一個無需類型強轉的通用ViewHolder(ItemViewHolder),一個ViewHolder對象能夠找到全部視圖實例。並且它是徹底獨立的, 沒有引入任何自定義類或者任何第三方依賴;即便沒有這個「框架」,也徹底能夠拆出來用在其餘地方。android
適配器(Adapter)是與控件關聯的, 是控件對其子視圖列表的一種抽象。抽象了什麼?由具體定義決定。好比列表控件的適配器(不管是之前的ListView
, 如今RecyclerView
, 以及其它的如ViewPager
)通常抽象了三個屬性:git
控件適配是SDK關聯的,框架的ItemAdapter
也是基於RecyclerView.Adapter
github
適配器(Adapter) 是容器控件對子控件的總體抽象,相應位置的元素沒有做出任何限制,position
對應的元素能夠是接口返回的一個具體數據,也能夠是從本地獲取的應用數據。框架要作的一個工做就是對元素數據類型進行抽象, 可是數據類型千差萬別,沒法對數據元素自己的屬性作統一操做,結果就是變成像MultiType
庫那樣,用範型抽象全部的數據元素,而後經過註冊數據類型(.class
)到數據綁定器類型(ItemViewBinder.class
)的映射,反射獲得綁定器實例,其中有大量的對象類型強轉。segmentfault
框架不對數據元素作抽象,而是針對操做做抽象,即adapter對每一個position
元素的操做做抽象;用一個簡單的List
數據結構持有抽象實例;由於一樣有綁定操做,因此姑且也叫作綁定器ItemBinder
。ViewHolder就用咱們以前的通用ViewHolder(ItemViewHolder
),結合前面說到adapter有三個重要屬性,因而有:微信
public interface ItemBinder { void onBindViewHolder(ItemViewHolder holder, int position); int getViewType(); } public class ItemAdapter extends RecyclerView.Adapter<ItemViewHolder> { private final List<ItemBinder> mBinders = new ArrayList<>(10); @Override public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) { ItemBinder binder = mBinders.get(position); binder.onBindViewHolder(holder, position); } @Override public int getItemCount() { return mBinders.size(); } @Override public int getItemViewType(int position) { return mBinders.get(position).getViewType(); } public void setBinders(List<ItemBinder> binders) { mBinders.clear(); mBinders.addAll(binders); } }
對於Adapter而言,元素僅僅是ItemBinder
,它不關心ItemBinder
是用哪一種數據類型,又是怎樣把數據填充到ViewHolder中。數據結構
RecyclerView
經過RecyclerView.Adapter
的getItemViewType
接口返回的數值來標識一個視圖類型。與ListView
不一樣的是這個viewType能夠不是連續的,RecyclerView
能夠本身感知設置了多少種viewType
(內部其實就是用了SparseArray
)。經過viewType
的標識, RecyclerView.Adapter
的onCreateViewHolder
來建立相應的視圖類型。一般咱們不得不本身創建viewType
和RecyclerView.ViewHolder
的映射關係,除了稍有點煩瑣以外並無多大的問題。app
注意:咱們走到了到框架的一個關鍵點,就是創建viewType
和視圖實例建立之間的關係。框架
已經找不到是在哪一個庫裏,當看到把視圖資源id(layoutId)直接做爲viewType
返回的時候,被這種天才想法折服了。首先就是用資源id自己就能夠建立視圖;其次是充分利用了viewType
能夠不連續的性質;再次是不一樣的資源id自然的對應不一樣的視圖類型,也就是說,自己就是多視圖類型的;最後的最後就是這種實現提供了巨大的靈活性,包括代碼複用和資源的複用,這點後面專門說一下。因而有:ide
public interface ItemBinder { void onBindViewHolder(ItemViewHolder holder, int position); @LayoutRes int getLayoutId(); } public class ItemAdapter extends RecyclerView.Adapter<ItemViewHolder> { private final List<ItemBinder> mBinders = new ArrayList<>(10); @NonNull @Override public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup container, int viewType) { return new ItemViewHolder(LayoutInflater.from(container.getContext()).inflate( viewType, container, false)); } @Override public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) { ItemBinder binder = mBinders.get(position); binder.onBindViewHolder(holder, position); } @Override public int getItemCount() { return mBinders.size(); } @Override public int getItemViewType(int position) { return mBinders.get(position).getLayoutId(); } public void setBinders(List<ItemBinder> binders) { mBinders.clear(); mBinders.addAll(binders); } }
咱們以前被getItemViewType
的默認值0給誤導了,思惟慣性讓咱們認爲viewType
能夠和ViewHolder
是割裂的,但其實它們能夠是統一的!
剩下的工做簡單明瞭,實現具體的ItemBinder
類型,將具體的數據填充到視圖,好比:
public HomeBannerBinder implements ItemBinder { private final HomeBanner mItem; HomeBannerBinder(HomeBanner banner) { mItem = banner; } void onBinderViewHolder(ItemViewHolder holder, int position) { ImageView bg = holder.findViewById(R.id.background); if (bg != null) { ImageManager.load(bg, mItem.bg_url); } } }
這裏的複用不是recyclerView
對視圖內存對象的複用,而是代碼層面的複用,包括聲明資源的xml代碼。
把layoutId做爲viewType
到底帶來怎樣的靈活複用呢?
能夠先舉例常見的微信朋友圈列表:顯然,不少朋友圈內容都是不一樣的,有視頻有圖片有文本,或者它們的結合,處理2張圖片的佈局和處理9張圖片的佈局顯示也是不一樣的;可是每一條朋友圈佈局有不少相同的地方:都有頂部的用戶頭像與用戶名稱,都有底部點贊和評論佈局。那麼問題來了:怎樣聲明不一樣的視圖類型,但沒必要重複書寫這些同樣的地方?
這固然不是難事,好比一個視頻朋友圈佈局可寫成這樣circle_item_video.xml
<RelativeLayout> <include layout="@layout/circle_item_top" /> <include layout="@layout/circle_item_layer_video" /> <include layout="@layout/circle_item_bottom" /> </RelativeLayout>
音頻朋友圈佈局circle_item_audio.xml
就把@layout/circle_item_layer_video
換成@layout/circle_item_layer_audio
,依次類推。
這麼作徹底能夠實現,隨着類型的增多,佈局文件相應增長便可;然而一旦發生變動呢?只要涉及相同佈局的部分都必須改一遍!(好比把RelativeLayout
變成android.support.constraint.ConstraintLayout
)並且實際的狀況不必定這麼簡單,可能由於各類緣由視圖的層次比較深,而且都沒辦法放在include中,一旦視圖對象變多,視圖層次變深, 這種冗餘就讓人難以忍受了,對一個有追求的碼畜來講,確定但願只更改一處地方便可。
若是layoutId做爲viewType要如何實現剛纔的複用呢?顯然他們必須是不一樣的viewType
(若是同樣會發生什麼?),那麼他們固然是不一樣的layoutId,但不一樣的layoutId就沒法避免上面那樣的問題,這時候就用到android的匿名資源(anonymous),就是對一個資源聲明一個引用,而這個引用自己做爲一個資源,即<item name="def" type="drawable">@drawable/abc</item>
,結合以上的例子就是circle_item.xml
:
<RelativeLayout> <include layout="@layout/circle_item_top" /> <ViewStub /> <include layout="@layout/circle_item_bottom" /> </RelativeLayout>
中間的部分可經過延遲加載的方式設置成不一樣的View,甚至全部不一樣的部分均可以以ViewStub
的形式嵌在佈局當中。refs.xml
:
<resources> <item name="circle_item_video" type="layout">@layout/circle_item</item> <item name="circle_item_audio" type="layout">@layout/circle_item</item> <item name="circle_item_pic_1" type="layout">@layout/circle_item</item> <item name="circle_item_pic_9" type="layout">@layout/circle_item</item> </resources>
也就是說都引用同一份的佈局資源!可他們由於不一樣的layoutId
進而能夠被recyclerView
看成不一樣的viewType
!
按照以前的思路也必然但願只在一處更改點贊和評論功能。因此有一個基類:
public class CircleItemBinder implements ItemBinder { @Override public getLayoutId() { return R.layout.circle_item; } @Override void onBindViewHolder(ItemViewHolder holder, int position) { bindComment(holder); bindLike(holder); } private void bindComment(ItemViewHolder holder) { } private void bindLike(ItemViewHolder holder) { } }
各種型的binder相似:
public class CircleVideoBinder extends CircleItemBinder { @Override public getLayoutId() { return R.layout.circle_item_video; } @Override void onBindViewHolder(ItemViewHolder holder, int position) { super.onBindViewHolder(holder, position); ... } } public class CircleAudioBinder extends CircleItemBinder { @Override public getLayoutId() { return R.layout.circle_item_audio; } @Override void onBindViewHolder(ItemViewHolder holder, int position) { super.onBindViewHolder(holder, position); ... } }
點贊和評論功能的代碼就可徹底複用!這一切只是用了layoutId做爲了viewType!
至此,框架的全貌已呈現:
public interface ItemBinder { @LayoutRes int getLayoutId(); void onBindViewHolder(ItemViewHolder holder, int position); } public class ItemAdapter extends RecyclerView.Adapter<ItemViewHolder> { private final List<ItemBinder> mBinders = new ArrayList<>(10); @NonNull @Override public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup container, int viewType) { return new ItemViewHolder(LayoutInflater.from(container.getContext()).inflate( viewType, container, false)); } @Override public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) { ItemBinder binder = mBinders.get(position); binder.onBindViewHolder(holder, position); } @Override public int getItemCount() { return mBinders.size(); } @Override public int getItemViewType(int position) { return mBinders.get(position).getLayoutId(); } public void setBinders(List<ItemBinder> binders) { mBinders.clear(); appendBinders(binders); } }
咱們以前的通用ViewHolder也羅列在這裏:
public class ItemViewHolder extends RecyclerView.ViewHolder { private final SparseArrayCompat<View> mCached = new SparseArrayCompat<>(10); public ItemViewHolder(View itemView) { super(itemView); } public <T extends View> T findViewById(@IdRes int resId) { int pos = mCached.indexOfKey(resId); View v; if (pos < 0) { v = itemView.findViewById(resId); mCached.put(resId, v); } else { v = mCached.valueAt(pos); } @SuppressWarnings("unchecked") T t = (T) v; return t; } }
通常都還要定義一個基礎類ItemBaseBinder
,全部派生類的可能會共享某個操做, 這個基礎類接收資源id做爲構造函數參數:
public class ItemBaseBinder implements ItemBinder { private final int mLayoutId; public ItemBaseBinder(@layoutRes int layoutId) { mLayoutId = layoutId; } @Override public void onBindViewHolder(ItemViewHolder holder, int position) { } @Override public int getLayoutId() { return mLayoutId; } }
其他的工做就只是派生具體的業務類了,就像以前舉例那樣!這一切不過130行代碼!
MutiType
的差別MutiType
庫一樣有綁定器ItemViewBinder
但注意他的綁定是隻有一個實例,而咱們的ItemAdapter
是把綁定器做爲元素對象,一個數據對應一個綁定器因此他有多個實例,實際上這個綁定器是對數據的抽象。
真的,MutiType
把這一切搞的太複雜了!可悲的是還有不少人在用……
有了這個框架,靈活性不只一點沒有損失,並且更加簡潔,MutiType
那坨類型強轉和反射操做能夠進博物館了。
一大篇說下來有點累贅,直接上代碼就能看明白的,關鍵是思考的過程與解決問題的思路。全部的框架到底解決了什麼問題,這纔是最須要了解和學習的,不然框架是學不完的。而一旦咱們有了思路與目標,實現一個框架也並非難事。這套小框架實踐已經很長時間了,能夠覆蓋絕大多數狀況,效果出奇的好,比MutiType
那坨「不知道高到哪裏去了」。
須要注意的有2點
onBindViewHolder
方法只作數據填充不該該作數據處理這點其實和框架沒有關係,照樣仍是有許多人在Adapter的onBindViewHolder
作着數據處理
由於方法getLayoutId
是接口,意味着在運行時能夠返回不一樣的layoutId,從而動態的更改視圖類型,不過須要與Adatper的notifyItemChanged
配合使用
ItemAdapter.setBinders
的方法實現體在更新了實例後沒有調用notifyDataSetChanged
, 這個操做應該由外部決定,雖然此處是必要的,但很容易形成冗餘的更新。
框架也很是容易根據具體的須要和場景進行擴展。
列表嵌套列表的狀況下,要如何抽象呢,其實只要對應視圖就行。最外層的列表(一級列表)有一個特殊ItemBinder
類型,這個類型自己也能夠持有多個ItemBinder
提供給內層列表(二級列表):
public class ItemContainerBinder extends ItemBaseBinder { private final ItemAdapter mAdapter = new ItemAdapter(); @Override public void onBinderViewHolder(ItemViewHolder holder, int position) { RecyclerView secondary = holder.findViewById(R.id.secondary); if (secondary != null) { if (secondary.getAdapter() != mAdapter) { secondary.setAdapter(mAdapter); } if (secondary.getLayoutManager() == null) { secondary.setLayoutManager(new LinearLayoutManager(secondary.getContext()); } } } public void setBinders(List<ItemBinder> binders) { mAdapter.setBinders(binders); } ... }
在這裏還能夠利用之前提過的重用LayoutManager!
在運行過程當中只須要更新列表某一項的狀況其實很是常見,不少時候不能只更新視圖,還要調用Adapter.notifyItemChanged
(像前文所提的動態更新列表視圖類型)。也就是Adapter持有ItemBinder
,而ItemBinder
須要再調用Adapter的方法,若是再讓ItemBinder
去引用Adapter
,這種強耦合必然不是一個好的設計。
針對這個框架的實現,這時候首先須要將ItemBinder
內部的變化通知出來,可是通知的時機應該由ItemBinder
實現體來決定,外部去被動響應。這固然是最簡單的觀察者模式了,因而有:
public interface ItemBinder { ... void setOnChangeListener(OnChangeListener listener); interface OnChangeListener { void onItemChanged(ItemBinder item, int payload); } } public class ItemBaseBinder implements ItemBinder { ... private OnChangeListener mChangeListener; @Override publi final void setChangeListener(OnChangeListener listener) { mChangeListener = listener; } public final void notifyItemChange(int payload) { if (mChangeListener != null) { mChangeListener.onItemChanged(this, payload); } } }
這裏的payload
借鑑了RecyclerView.Adapter
,只不過類型由Object
變成了int
,表明了局部更新須要攜帶的信息。在ItemBinder
實現體內部,由於某項數據變動須要通知到外部就只需調用notifyItemChange
方法,將變動傳遞出去,由外部做出具體響應:
List<ItemBinder> binders = new ArrayList<>(); ... ItemBinder special = new XXXYYYBinder(...); specail.setChangeListener(new ItemBinder.OnChangeListener() { @Override public void onItemChanged(ItemBinder item, int payload) { int pos = mAdapter.indexOf(item); if (pos >= 0) { mAdapter.notifyItemChanged(pos,...); } } }); binders.add(special); ... mAdapter.setBinders(binders);