名字有點唬人,其實就是組合了幾個封裝類能夠方便實現RecyclerView
的多檢視,畢竟“框架”這個詞在我看來還是指具有一定規模量級及重點技術的程式碼體系,但僅就解決特定問題而言也不妨被冠以這個名號。同時它真的是“超輕量”總共不過4個類,不超過130行程式碼~
檢視抽象
我們已經有了一個無需型別強轉的通用ViewHolder(ItemViewHolder),一個ViewHolder物件可以找到所有檢視例項。而且它是完全獨立的, 沒有引入任何自定義類或者任何第三方依賴;即使沒有這個“框架”,也完全可以拆出來用在其他地方。
控制元件介面卡
介面卡(Adapter)是與控制元件關聯的, 是控制元件對其子檢視列表的一種抽象。抽象了什麼?由具體定義決定。比如列表控制元件的介面卡(無論是以前的ListView
, 現在RecyclerView
, 以及其它的如ViewPager
)一般抽象了三個屬性:
- 數量 getItemCount()
- 操作 onBindView(ViewHolder holder, int position),onCreateView
- 型別 getViewType(int position)
控制元件適配是SDK關聯的,框架的ItemAdapter
也是基於RecyclerView.Adapter
元素抽象
介面卡(Adapter) 是容器控制元件對子控制元件的整體抽象,相應位置的元素沒有作出任何限制,position
對應的元素可以是介面返回的一個具體資料,也可以是從本地獲取的應用資料。框架要做的一個工作就是對元素資料型別進行抽象, 但是資料型別千差萬別,無法對資料元素本身的屬性做統一操作,結果就是變成像MultiType
庫那樣,用範型抽象所有的資料元素,然後通過註冊資料型別(.class
)到資料繫結器型別(ItemViewBinder.class
)的對映,反射得到繫結器例項,其中有大量的物件型別強轉。
框架不對資料元素做抽象,而是針對操作作抽象,即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
的對映關係,除了稍有點煩瑣之外並沒有多大的問題。
注意:我們走到了到框架的一個關鍵點,就是建立viewType
和檢視例項建立之間的關係。
已經找不到是在哪個庫裡,當看到把檢視資源id(layoutId)直接作為viewType
返回的時候,被這種天才想法折服了。首先就是用資源id本身就可以建立檢視;其次是充分利用了viewType
可以不連續的性質;再次是不同的資源id天然的對應不同的檢視型別,也就是說,本身就是多檢視型別的;最後的最後就是這種實現提供了巨大的靈活性,包括程式碼複用和資源的複用,這點後面專門說一下。於是有:
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 {
private final YourVideoData mItem;
public CircleVideoBinder(YourVideoData data) {
mItem = data;
}
@Override
public getLayoutId() {
return R.layout.circle_item_video;
}
@Override
void onBindViewHolder(ItemViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
TextView title = holder.findViewById(R.id.video_title);
if (title != null) {
title.setText(mItem.title);
}
...
}
}
public class CircleAudioBinder extends CircleItemBinder {
private final YourAudioData mItem;
public CircleAudioBinder(YourAudioData data) {
mItem = data;
}
@Override
public getLayoutId() {
return R.layout.circle_item_audio;
}
@Override
void onBindViewHolder(ItemViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
ImageView album = holder.findViewById(R.id.audio_album);
if (album != null) {
ImageLoader.load(album, mItem.album_background);
}
...
}
}
複製程式碼
點贊和評論功能的程式碼就可完全複用!這一切只是用了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);
複製程式碼