基於 Multitype 開源庫封裝更好用的RecyclerView.Adapter
MultiType 這個專案,至今 v3.x 穩定多時,考慮得非常多,但也做得非常剋制。原則一直是 直觀、靈活、可靠、簡單純粹(其中直觀和靈活是非常看重的)。
這是 MultiType 框架作者給出的專案簡述。
作為一個 RecyclerView 的 Adapter 框架,感覺這專案的設計非常的優雅,而且可以滿足很多常用的需求,而且像作者所說,該專案非常剋制,沒有因為便利而加入一些會導致專案臃腫的功能,它只提供了資料的繫結,其他的功能我們只需要稍微加以封裝就可以實現。
為什麼要封裝
如果還沒用過這個庫的先去看看作者的Android-MultiType-3.0" rel="nofollow,noindex">文件
我們先來看看框架的原始用法:
Step 1. 建立一個 class,它將是你的資料型別或 Java bean / model. 對這個類的內容沒有任何限制。示例如下:
public class Category { @NonNull public final String text; public Category(@NonNull String text) { this.text = text; } } 複製程式碼
Step 2. 建立一個 class 繼承 ItemViewBinder.
ItemViewBinder 是個抽象類,其中 onCreateViewHolder 方法用於生產你的 item view holder, onBindViewHolder 用於繫結資料到 Views. 一般一個 ItemViewBinder 類在記憶體中只會有一個例項物件,MultiType 內部將複用這個 binder 物件來生產所有相關的 item views 和繫結資料。示例:
public class CategoryViewBinder extends ItemViewBinder<Category, CategoryViewBinder.ViewHolder> { @NonNull @Override protected ViewHolder onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { View root = inflater.inflate(R.layout.item_category, parent, false); return new ViewHolder(root); } @Override protected void onBindViewHolder(@NonNull ViewHolder holder, @NonNull Category category) { holder.category.setText(category.text); } static class ViewHolder extends RecyclerView.ViewHolder { @NonNull private final TextView category; ViewHolder(@NonNull View itemView) { super(itemView); this.category = (TextView) itemView.findViewById(R.id.category); } } } 複製程式碼
Step 3. 在 Activity 中加入 RecyclerView 和 List 並註冊你的型別,示例:
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); RecyclerView recyclerView = (RecyclerView) findViewById(R.id.list); /* 注意:我們已經在 XML 佈局中通過 app:layoutManager="LinearLayoutManager" * 給這個 RecyclerView 指定了 LayoutManager,因此此處無需再設定 */ adapter = new MultiTypeAdapter(); /* 註冊型別和 View 的對應關係 */ adapter.register(Category.class, new CategoryViewBinder()); adapter.register(Song.class, new SongViewBinder()); recyclerView.setAdapter(adapter); /* 模擬載入資料,也可以稍後再載入,然後使用 * adapter.notifyDataSetChanged() 重新整理列表 */ items = new Items(); for (int i = 0; i < 20; i++) { items.add(new Category("Songs")); items.add(new Song("drakeet", R.drawable.avatar_dakeet)); items.add(new Song("許岑", R.drawable.avatar_cen)); } adapter.setItems(items); adapter.notifyDataSetChanged(); } } 複製程式碼
我把作者文件中的事例搬了過來,可以看到,使用還是非常簡易的,沿用了原生 ViewHolder 的用法,上手很快。
- 但是這也是一個非常不便的問題,因為作者沒有進一步的封裝,所以我們還需要為每個 Binder 去配置一個 ViewHolder ,所以我們還是做了很多重複性的工作。
- 並且在 Adapter 或 Binder 中沒有為我們提供 Item 的點選反饋介面,這樣就導致我們的點選萬一依賴到 Activity 或者 Fragment 的一些變數的話,又需要我們去寫一個 Callback 。
所以我們的封裝就是為了解決上面的兩個問題。
封裝
問題
上面說到我們封裝就是要解決上面提到的兩個問題,讓其更好用:
- 封裝 ViewHolder
- 新增點選事件
- 新增 Sample Binder
- 新增Header、Footer
第三點是隨便新增上去的,用於只有一個 TextView 的 Item。
方案
1. 封裝ViewHolder
思路其實很簡單,就是建立一個 BaseViewHolder 來代替我們之前需要頻繁建立的 ViewHolder.
廢話少說,看程式碼:
public class BaseViewHolder extends RecyclerView.ViewHolder { private View mView; private SparseArray<View> mViewMap = new SparseArray<>();// 1 public BaseViewHolder(View itemView) { super(itemView); mView = itemView; } //返回根View public View getView() { return mView; } /** * 根據View的id來返回view例項 */ public <T extends View> T getView(@IdRes int ResId) { View view = mViewMap.get(ResId); if (view == null) { view = mView.findViewById(ResId); mViewMap.put(ResId, view); } return (T) view; } } 複製程式碼
整個類就一個方法getView
的兩個過載,沒有引數的 那個返回我們 Item 的根 View ,有引數的那個可以根據控制元件的 Id 來返回相對應 View。
在getView(@IdRes int ResId)
方法中,我們用 ResId 為鍵,View 為值的 SparseArray 來儲存當前 ViewHolder 的各種View,然後首次載入(即mViewMap
沒有對應的值)時就用findViewById
方法來獲取相對View並存起來,然後複用的時候就可以直接重mViewMap
中獲取相對於的值(View)來進行資料繫結。
接著,為了方便,我們可以新增一系列的方法在此類中,例如:
public BaseViewHolder setText(@IdRes int viewId, @StringRes int strId) { TextView view = getView(viewId); view.setText(strId); return this; } public BaseViewHolder setImageResource(@IdRes int viewId, @DrawableRes int imageResId) { ImageView view = getView(viewId); view.setImageResource(imageResId); return this; } 複製程式碼
這樣一來,我們就可以在 Binder 類的onBindViewHolder中進行更加簡便的資料繫結,例如:
@Override protected void onBindViewHolder(@NonNull BaseViewHolder holder, @NonNull T item) { holder.setText(R.id.name,“張三”); holder.setImageResource(R.id.avatar,R.mimap.icon_avatar); } 複製程式碼
2. 封裝 ItemBinder
為了解決我們上面問題中的第2點,我們需要封裝一個 ItemBinder 來實現我們的功能。程式碼如下:
public abstract class LwItemBinder<T> extends ItemViewBinder<T, LwViewHolder> { private OnItemClickListener<T> mListener; private OnItemLongClickListener<T> mLongListener; private SparseArray<OnChildClickListener<T>> mChildListenerMap = new SparseArray<>(); private SparseArray<OnChildLongClickListener<T>> mChildLongListenerMap = new SparseArray<>(); protected abstract View getView(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent); protected abstract void onBind(@NonNull LwViewHolder holder, @NonNull T item); @NonNull @Override protected final LwViewHolder onCreateViewHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { return new LwViewHolder(getView(inflater, parent)); } @Override protected final void onBindViewHolder(@NonNull LwViewHolder holder, @NonNull T item) { bindRootViewListener(holder, item); bindChildViewListener(holder, item); onBind(holder, item); } /** * 繫結子View點選事件 * * @param holder * @param item */ private void bindChildViewListener(LwViewHolder holder, T item) { //點選事件 for (int i = 0; i < mChildListenerMap.size(); i++) { int id = mChildListenerMap.keyAt(i); View view = holder.getView(id); if (view != null) { view.setOnClickListener(v -> { OnChildClickListener<T> l = mChildListenerMap.get(id); if (l!=null){ l.onChildClick(holder,view,item); } }); } } //長按點選 for (int i = 0; i < mChildLongListenerMap.size(); i++) { int id = mChildLongListenerMap.keyAt(i); View view = holder.getView(id); if (view != null) { view.setOnClickListener(v -> { OnChildLongClickListener<T> l = mChildLongListenerMap.get(id); if (l != null) { l.onChildLongClick(holder,view, item); } }); } } } /** * 繫結根view * * @param holder * @param item */ private void bindRootViewListener(LwViewHolder holder, T item) { //根View點選事件 holder.getView().setOnClickListener(v -> { if (mListener != null) { mListener.onItemClick(holder, item); } }); //根View長按事件 holder.getView().setOnLongClickListener(v -> { boolean result = false; if (mLongListener != null) { result = mLongListener.onItemLongClick(holder, item); } return result; }); } /** * 點選事件 */ public void setOnItemClickListener(OnItemClickListener<T> listener) { mListener = listener; } /** * 點選事件 * * @param id 控制元件id,可傳入子view ID * @param listener */ public void setOnChildClickListener(@IdRes int id, OnChildClickListener<T> listener){ mChildListenerMap.put(id,listener); } public void setOnChildLongClickListener(@IdRes int id, OnChildLongClickListener<T> listener){ mChildLongListenerMap.put(id,listener); } /** * 長按點選事件 */ public void setOnItemLongClickListener(OnItemLongClickListener<T> l) { mLongListener = l; } /** * 長按點選事件 * * @param id 控制元件id,可傳入子view ID */ public void removeChildClickListener(@IdRes int id){ mChildListenerMap.remove(id); } public void removeChildLongClickListener(@IdRes int id){ mChildLongListenerMap.remove(id); } /** * 移除點選事件 */ public void removeItemClickListener() { mListener = null; } public void removeItemLongClickListener() { mLongListener = null; } public interface OnItemLongClickListener<T> { boolean onItemLongClick(LwViewHolder holder, T item); } public interface OnItemClickListener<T> { void onItemClick(LwViewHolder holder, T item); } public interface OnChildClickListener<T> { void onChildClick(LwViewHolder holder, View child, T item); } public interface OnChildLongClickListener<T> { void onChildLongClick(LwViewHolder holder, View child, T item); } } 複製程式碼
程式碼也很簡單,提供了Click以及LongClick的監聽,並且在onCreateViewHolder()
方法中將我們剛剛封裝的 BaseViewHolder 給傳進去,然後提供兩個抽象方法:
-
getView(@NonNull LayoutInflater inflater,@NonNull ViewGroup parent)
- 需要返回Item的View例項
-
onBind(@NonNull BaseViewHolder holder, @NonNull T item)
- 在此方法內進行資料繫結
以後我們就不必為每個 Binder 都設定一套ViewHolder了,例項如下:
public class RankItemBinder extends LwItemBinder<Rank> { private final int[] RANK_IMG = { R.drawable.no_4, R.drawable.no_5, R.drawable.no_6, R.drawable.no_7, R.drawable.no_8, R.drawable.no_9, R.drawable.no_10 }; @Override protected View getView(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { return inflater.inflate(R.layout.item_rank, parent, false); } @Override protected void onBind(@NonNull BaseViewHolder holder, @NonNull Rank item) { Context context = holder.getView().getContext(); holder.setText(R.id.tv_name, item.getUserNickname()); holder.setText(R.id.tv_num, context.getString(R.string.text_caught_doll_num, item.getCaughtNum())); loadCircleImage(context,item.getUserIconUrl(),0,0,holder.getView(R.id.iv_avatar)); if (holder.getAdapterPosition() < 7) { holder.setImageResource(R.id.iv_rank, RANK_IMG[holder.getAdapterPosition()]); } } public void loadCircleImage(final Context context, String url, int placeholderRes, int errorRes, final ImageView imageView) { RequestOptions requestOptions = new RequestOptions() .circleCrop(); if (placeholderRes != 0) requestOptions.placeholder(placeholderRes); if (errorRes != 0) requestOptions.error(errorRes); Glide.with(context).load(url).apply(requestOptions).into(imageView); } } 複製程式碼
可以看到,非常的簡潔,並且可以在 Activity 或 Fragment 中新增監聽事件:
RankItemBinder binder = new RankItemBinder(); binder.setOnItemClickListener(new BaseItemBinder.OnItemClickListener<Rank>() { @Override public void onItemClick(BaseViewHolder holder, Rank item) { ToastUtils.showShort("點選了"+item.getUserNickname()); } }); 複製程式碼
如果使用 lambda 表示式,則可以更簡潔:
binder.setOnItemClickListener((holder, item) -> ToastUtils.showShort("點選了"+item.getUserNickname())); 複製程式碼
以上就是整套的封裝了,很簡單,但是也很實用,可以在日常開發中省下不少程式碼。
3. 封裝Sample
上面說了,我們還可以通過繼承這個 BaseItemBinder 來實現一個只有一個 TextView 的Sample:
public class SampleBinder extends LwItemBinder<Object> { public static final int DEFAULT_TEXT_SIZE = 15; //sp public static final int DEFAULT_HEIGHT = 50;//dp public static final int DEFAULT_PADDING_HORIZONTAL = 6; //dp public static final int DEFAULT_PADDING_VERTICAL = 4; //dp @Override protected View getView(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { Context context = parent.getContext(); DisplayMetrics metrics = context.getResources().getDisplayMetrics(); float density = metrics.density; int heightPx = dp2px(density, DEFAULT_HEIGHT); int paddingHorizontal = dp2px(density, DEFAULT_PADDING_HORIZONTAL); TextView textView = new TextView(context); textView.setTextSize(DEFAULT_TEXT_SIZE); textView.setGravity(Gravity.CENTER_VERTICAL); textView.setPadding(paddingHorizontal, 0, paddingHorizontal, 0); ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, heightPx); textView.setLayoutParams(params); custom(textView, parent); return textView; } @Override protected void onBind(@NonNull LwViewHolder holder, @NonNull Object item) { TextView textView = holder.getView(); textView.setText(item.toString()); } private int dp2px(float density, float dp) { return (int) (density * dp + 0.5f); } protected void custom(TextView textView, ViewGroup parent) { } } 複製程式碼
很簡單的一個擴充套件,根 View 就是一個TextView
,然後提供了一些屬性的設定修改,如果不滿足預設樣式還可以重寫custom(TextView textView, ViewGroup parent)
方法對TextView
進行樣式的修改,或者重寫custom(TextView textView, ViewGroup parent)
方法在進行繫結的時候進行控制元件的屬性修改等邏輯。
4. 新增Header、Footer
MultiType其實本身就支援HeaderView
、FooterView
,只要建立一個Header.class
-HeaderViewBinder
和Footer.class
-FooterViewBinder
即可,然後把new Header()
新增到items
第一個位置,把new Footer()
新增到items
最後一個位置。需要注意的是,如果使用了 Footer View,在底部插入資料的時候,需要新增到最後位置 - 1
,即倒二個位置,或者把Footer
remove 掉,再新增資料,最後再插入一個新的Footer
.
這個是作者文件裡面說的,簡單,但是繁瑣,既然我們要封裝,肯定就不能容忍這麼繁瑣的事情。
先理一下要實現的點:
- 一行程式碼新增 Header/Footer
- 源資料的更改更新與 Header/Footer 無關
接下來看看具體實現:
public class LwAdapter extends MultiTypeAdapter { //...省略部分程式碼 private HeaderExtension mHeader; private FooterExtension mFooter; /** * 新增Footer * * @param o Header item */ public LwAdapter addHeader(Object o) { createHeader(); mHeader.add(o); notifyItemRangeInserted(getHeaderSize() - 1, 1); return this; } /** * 新增Footer * * @param o Footer item */ public LwAdapter addFooter(Object o) { createFooter(); mFooter.add(o); notifyItemInserted(getItemCount() + getHeaderSize() + getFooterSize() - 1); return this; } /** * 增加Footer資料集 * * @param items Footer 的資料集 */ public LwAdapter addFooter(Items items) { createFooter(); mFooter.addAll(items); notifyItemRangeInserted(getFooterSize() - 1, items.size()); return this; } private void createHeader() { if (mHeader == null) { mHeader = new HeaderExtension(); } } private void createFooter() { if (mFooter == null) { mFooter = new FooterExtension(); } } } 複製程式碼
先看上面的實現,用addHeader(Object o)
新增 Header,新增 Footer 同理,一行程式碼就實現,但是這個addHeader(Object o)
方法裡面的邏輯是怎樣的呢,首先是呼叫了createHeader()
,即建立一個HeaderExtension
物件並把引用賦值給 mHeader,然後再呼叫mHeader.add(o)
將我們傳過來的 item 例項給新增進去,最後呼叫Adapter
的notifyItemInserted
方法重新整理一下列表就OK了。邏輯很簡單,但是這樣為什麼就可以實現了新增 Header 的功能呢,HeaderExtension
又是什麼鬼呢?
接下來看看HeaderExtension
是什麼?
public class HeaderExtension implements Extension { private Items mItems; public HeaderExtension(Items items) { this.mItems = items; } public HeaderExtension(){ this.mItems = new Items(); } @Override public Object getItem(int position) { return mItems.get(position); } @Override public boolean isInRange(int adapterSize, int adapterPos) { return adapterPos < getItemSize(); } @Override public int getItemSize() { return mItems.size(); } @Override public void add(Object o) { mItems.add(o); } @Override public void remove(Object o) { mItems.add(o); } //...省略部分程式碼 } 複製程式碼
該類實現了Extension
介面,我們呼叫add()
方法就是將傳過來的物件儲存起來而已。整個類最主要的方法就是isInRange(int adapterSize, int adapterPos)
方法,看到這個方法的實現相信你也能明白他的作用了,就是用來判斷Adapter
裡面傳過來的 position 對應的 Item 是否是 Header.接下來看一下這個方法在 Adapter 內的使用在哪裡:
#LwAdapter.java
@Override public final int getItemViewType(int position) { Object item = null; int headerSize = getHeaderSize(); int mainSize = getItems().size(); if (mHeader != null) { if (mHeader.isInRange(getItemCount(), position)) { item = mHeader.getItem(position); return indexInTypesOf(position, item); } } if (mFooter != null) { if (mFooter.isInRange(getItemCount(), position)) { int relativePos = position - headerSize - mainSize; item = mFooter.getItem(relativePos); return indexInTypesOf(relativePos, item); } } int relativePos = position - headerSize; return super.getItemViewType(relativePos); } 複製程式碼
第一次的呼叫在這裡,到這裡我們應該就恍然大悟了,原來就是根據 position 來判斷是否用於 Header/Footer ,然後再用 父類裡面的indexInTypesOf(int,Object)
來獲取對應的型別。接著在onCreateViewHolder(ViewGroup parent, int indexViewType)
會自動建立我們對應的ViewHolder
,最後在onBindViewHolder()
中再進行相應的繫結即可:
@SuppressWarnings("unchecked") @Override public final void onBindViewHolder(RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) { Object item = null; int headerSize = getHeaderSize(); int mainSize = getItems().size(); ItemViewBinder binder = getTypePool().getItemViewBinder(holder.getItemViewType()); if (mHeader != null) { if (mHeader.isInRange(getItemCount(), position)) { item = mHeader.getItem(position); } } if (mFooter != null) { if (mFooter.isInRange(getItemCount(), position)) { int relativePos = position - headerSize - mainSize; item = mFooter.getItem(relativePos); } } if (item != null) { binder.onBindViewHolder(holder, item); return; } super.onBindViewHolder(holder, position - headerSize, payloads); } 複製程式碼
onBindViewHolder
跟getItemViewType
的實現思想類似,判斷是否是 Header/Footer 拿到相應的實體類,然後進行繫結。整個流程就是這樣,當然別忘了也要在getItemCount
方法中將我們的 Header 與 Footer 的數量加進入,如:
@Override public final int getItemCount() { int extensionSize = getHeaderSize() + getFooterSize(); return super.getItemCount() + extensionSize; } 複製程式碼
這樣的封裝可以讓我們的 Header/Footer 裡面的資料集與原本的資料集分離,我們的主資料再怎麼增刪查改都不會影響到Header/Footer 的正確性。
這樣的實現目前有個比較蛋疼的點,我們呼叫ViewHolder
的getAdapterPosition()
時候會返回實際的 position,即包含了 Header 的數量,目前這點還沒解決,需要手動把該 position 減去 Header 的數量才能得到原始資料集的相對位置。
以上,就完成了本次的小封裝,趕緊去程式碼中實戰吧。