上接??????仿B站Android客戶端系列(一)項(xiàng)目環(huán)境搭建
前言
任何項(xiàng)目幾乎都會(huì)用RecyclerView作為列表的實(shí)現(xiàn)。在傳統(tǒng)的開(kāi)發(fā)方式中,簡(jiǎn)單的單一類型數(shù)據(jù)列表非常容易實(shí)現(xiàn),當(dāng)需要支持多種數(shù)據(jù)類型及布局時(shí),我們的代碼往往會(huì)堆積在Adapter中,當(dāng)Adapter封裝了一些通用操作時(shí),該類更會(huì)顯得臃腫不堪,不便于維護(hù)。好的封裝會(huì)大大節(jié)省開(kāi)發(fā)效率,增強(qiáng)代碼的易讀性,這樣在開(kāi)發(fā)以及修改的過(guò)程中可以節(jié)省不少時(shí)間。
在瀏覽b站App時(shí)可以看到各個(gè)頁(yè)面的列表基本都有如下特性:支持下拉刷新、Loading、加載失敗、加載以及存在加載的各種狀態(tài)。這篇文章主要說(shuō)一下在FakeBiliBili項(xiàng)目的開(kāi)發(fā)過(guò)程中,對(duì)于Adapter封裝的思路和一些想法。
MultiType
這里先安利一個(gè)解決多類型問(wèn)題的Adapter庫(kù):
這是一個(gè)直觀、靈活、可靠、簡(jiǎn)單純粹的庫(kù),其中設(shè)計(jì)思想非常值得學(xué)習(xí)。
基礎(chǔ)的用法如下:
public class MainActivity extends AppCompatActivity {
private MultiTypeAdapter adapter;
/* Items 等同于 ArrayList<Object> */
private Items items;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRecyclerView = (RecyclerView) findViewById(R.id.list);
mAdapter = new MultiTypeAdapter();
//注冊(cè),數(shù)據(jù)類型對(duì)應(yīng)ViewBinder
mAdapter.register(TextBean.class, new TextViewBinder());
mAdapter.register(ImageBean.class, new ImageViewBinder());
mAdapter.register(VideoBean.class, new VideoViewBinder());
mRecyclerView.setAdapter(mAdapter);
//設(shè)置列表數(shù)據(jù)
Items<Object> items = new Items<>();
items.add(new ImageBean(R.drawable.image1));
items.add(new ImageBean(R.drawable.image2));
items.add(new TextBean("text1"));
items.add(new TextBean("text2"));
items.add(new TextBean("text3"));
items.add(new VideoBean(url));
mAdapter.setItems(items);
mAdapter.notifyDataSetChanged();
}
}
//ItemViewBinder示例
public class TextViewBinder extends ItemViewBinder<TextBean, TextViewBinder.ViewHolder> {
@NonNull @Override
protected ViewHolder onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
View view = inflater.inflate(R.layout.item_text, parent, false);
return new ViewHolder(view);
}
@Override
protected void onBindViewHolder(@NonNull ViewHolder holder, @NonNull TextBean textBean) {
holder.category.setText(textBean.tv_text);
}
static class ViewHolder extends RecyclerView.ViewHolder {
@NonNull private TextView tv_text;
ViewHolder(@NonNull View itemView) {
super(itemView);
tv_text = (TextView) itemView.findViewById(R.id.tv_text);
}
}
}
這個(gè)庫(kù)的核心是通過(guò)類型池TypePool來(lái)管理注冊(cè) binder 與 class 來(lái)實(shí)現(xiàn)不同類型布局的策略模式,MultiType內(nèi)部將復(fù)用這個(gè) binder 對(duì)象來(lái)生產(chǎn)所有相關(guān)的 item、views 和綁定數(shù)據(jù)。用法就是注冊(cè)綁定數(shù)據(jù)類型和 ItemViewBinder,然后向內(nèi)置的數(shù)據(jù)集合對(duì)象中添加數(shù)據(jù)然后通知刷新,Item便會(huì)根據(jù)集合中的順序依次顯示。
MultiType提供的 ItemViewBinder 沿襲了 RecyclerView.Adapter 的接口命名,很容易理解,這樣我們可以輕松的實(shí)現(xiàn)多種類型列表,而且代碼清晰、直觀,方便修改,相比傳統(tǒng)的寫法可以說(shuō)是省了不少時(shí)間和維護(hù)精力。詳細(xì)用法請(qǐng)移步該庫(kù)Wiki。
擴(kuò)展
通過(guò)MultiType的支持,現(xiàn)在沒(méi)有復(fù)雜類型列表的問(wèn)題了,但MultiType并不能滿足上拉加載、顯示Loading、加載失敗或是需要添加Header、Footer等其他需求,這時(shí)候需要我們自己擴(kuò)展來(lái)實(shí)現(xiàn),下面就來(lái)聊聊我的實(shí)現(xiàn)思路。
一.關(guān)于加載更多的擴(kuò)展
首先就是封裝加載更多這樣的常用功能,通常的寫法是
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_LOAD_MORE) {
return ...
}
return ...;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (getItemViewType(position) == TYPE_LOAD_MORE) {
...
} else {
...
}
}
@Override
public int getItemCount() {
return mDatas.size() + 1;//給出加載更多的位置
}
@Override
public int getItemViewType(int position) {
if (position == getItemCount()) {
return TYPE_LOAD_MORE;//當(dāng)顯示最后一個(gè)Item,使其為加載更多類型
} else {
return ...
}
}
那么基本思路就是繼承MultiTypeAdapter并重寫這幾個(gè)方法,當(dāng)需要顯示我們定制的通用Item時(shí),在繼承類中實(shí)現(xiàn),當(dāng)需要顯示MultiType職責(zé)內(nèi)的Item時(shí),歸還給父類MultiTypeAdapter實(shí)現(xiàn)。但是當(dāng)我嘗試?yán)^承MultiTypeAdapter重寫這些方法時(shí),發(fā)現(xiàn)作者給這些方法都加上了final,那么便無(wú)法繼承擴(kuò)展。開(kāi)始不是很理解,還提了issue給作者,得到的回復(fù)是這樣的:
使用 final 意在避免用戶自定義破壞了封裝并且歸結(jié)認(rèn)為是 MultiType 的問(wèn)題。如果你需要覆寫這些 final 方法,你應(yīng)該考慮采用組合而非繼承,即創(chuàng)建一個(gè) Adapter 包含 MultiTypeAdapter 而不是繼承 MultiTypeAdapter。
MultiTypeAdapter 并不是為繼承而設(shè)計(jì)的類?!禘ffective Java》一書中指出:使類和成員的可訪問(wèn)性最小化,并且要么為繼承而設(shè)計(jì),并提供文檔說(shuō)明,要么就禁止繼承。在 Kotlin 語(yǔ)言設(shè)計(jì)中,也是遵循了這個(gè)原則,所有類默認(rèn)都是 final,所有方法默認(rèn)都是 final,除非特意標(biāo)注 open.
看完豁然開(kāi)朗!其實(shí)《Effective Java》也讀過(guò),但平常寫代碼時(shí)沒(méi)有注意,導(dǎo)致做了很多過(guò)度設(shè)計(jì)。
所以我需要用一個(gè)裝飾模式來(lái)完成需求的擴(kuò)展,這里修改一下:
protected RecyclerView.Adapter innerAdapter;
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_LOAD_MORE) {
return ...
}
return innerAdapter.onCreateViewHolder(parent, viewType);//交給目標(biāo)adapter處理
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if (getItemViewType(position) == TYPE_LOAD_MORE) {
...
return;
}
innerAdapter.onBindViewHolder(holder, position);//交給目標(biāo)adapter處理
}
@Override
public int getItemCount() {
return innerAdapter.getItemCount() + 1;//給出加載更多的位置
}
@Override
public int getItemViewType(int position) {
if (position == getItemCount() - 1) {
return TYPE_LOAD_MORE;//當(dāng)顯示最后一個(gè)Item,使其為加載更多類型
}
innerAdapter.getItemViewType(position);//交給目標(biāo)adapter處理
}
這也就是很多開(kāi)源庫(kù)都會(huì)用到的裝飾模式寫法,比如鴻陽(yáng)的baseAdapter,可惜有很多bug并且不維護(hù)了。
二.完善
基礎(chǔ)的骨架有了,接下來(lái)就是進(jìn)一步完善這個(gè)adapterWraaper,這里說(shuō)一下寫代碼過(guò)程中值得注意的地方和一些坑。
1.兼容GridLayoutManager和StaggeredGridLayoutManager
當(dāng)使用這兩種LayoutManager時(shí),需要對(duì)我們自己實(shí)現(xiàn)的ViewType在不同LayoutManager時(shí)做一些特殊處理,否則當(dāng)列數(shù)大于1時(shí),加載更多item便不能撐滿一行。
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
recyclerView.addOnScrollListener(mOnScrollListener);
if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
final GridLayoutManager layoutManager = (GridLayoutManager) recyclerView.getLayoutManager();
final GridLayoutManager.SpanSizeLookup oldSpanSizeLookup = layoutManager.getSpanSizeLookup();
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (getItemViewType(position) == ITEM_TYPE_LOAD_MORE) {
return layoutManager.getSpanCount();
} else {
return oldSpanSizeLookup.getSpanSize(position);
}
}
});
}
innerAdapter.onAttachedToRecyclerView(recyclerView);
}
@Override
public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
if (isBaseViewHolder(holder)) {
innerAdapter.onViewAttachedToWindow(holder);
} else {
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
if (lp != null && lp instanceof StaggeredGridLayoutManager.LayoutParams) {
StaggeredGridLayoutManager.LayoutParams p = (StaggeredGridLayoutManager.LayoutParams) lp;
p.setFullSpan(true);
}
}
}
2. ItemDecoration與SpanSizeLookup
由于我們運(yùn)用裝飾模式的處理了GridLayoutManager時(shí)帶來(lái)的跨度問(wèn)題,但此時(shí)作用于GridLayoutManager中的SpanSizeLookup是我們的裝飾類,這可能會(huì)引發(fā)一些問(wèn)題。例如我有時(shí)會(huì)在ItemDecoration中直接把SpanSizeLookup對(duì)象當(dāng)做參數(shù)傳進(jìn)來(lái),用于判斷該項(xiàng)的跨度。
public class CustomItemDecoration extends RecyclerView.ItemDecoration {
private GridLayoutManager.SpanSizeLookup spanSizeLookup;
public CustomItemDecoration(GridLayoutManager.SpanSizeLookup spanSizeLookup) {
this.spanSizeLookup = spanSizeLookup;
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
...
int spanSize = spanSizeLookup.getSpanSize(position);
...
}
這樣顯然會(huì)出現(xiàn)意料之外的問(wèn)題,這時(shí)候我們不能直接把SpanSizeLookup傳進(jìn)來(lái),而是通過(guò)recyclerView對(duì)象獲得,像這樣:
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
...
GridLayoutManager.SpanSizeLookup spanSizeLookup = ((GridLayoutManager) parent.getLayoutManager()).getSpanSizeLookup();
int spanSize = spanSizeLookup.getSpanSize(position);
...
}
3.不要忘記其他重寫方法的處理
詳細(xì)代碼見(jiàn)該項(xiàng)目中
三.效果





四.其他
1.Header和Footer
關(guān)于Header和Footer的問(wèn)題,MultiType作者給出了解決方案:
MultiType 其實(shí)本身就支持 HeaderView、FooterView,只要?jiǎng)?chuàng)建一個(gè) Header.class - HeaderViewBinder 和 Footer.class - FooterViewBinder 即可,然后把 new Header() 添加到 items 第一個(gè)位置,把 new Footer() 添加到 items 最后一個(gè)位置。需要注意的是,如果使用了 Footer View,在底部插入數(shù)據(jù)的時(shí)候,需要添加到 最后位置 - 1,即倒二個(gè)位置,或者把 Footer remove 掉,再添加數(shù)據(jù),最后再插入一個(gè)新的 Footer.
最后
僅僅是個(gè)人理解,不合理和不完善的地方還請(qǐng)留言指教,謝謝!
項(xiàng)目地址:FakeBiliBili
還可以的話就賞個(gè)star吧!(≧▽≦)/