前言
MultiType 這個專案,至今 v3.x 穩定多時,考慮得非常多,但也做得非常剋制。原則一直是 直觀、靈活、可靠、簡單純粹(其中直觀和靈活是非常看重的)。
這是 MultiType 框架作者給出的專案簡述。
作為一個 RecyclerView 的 Adapter 框架,感覺這專案的設計非常的優雅,而且可以滿足很多常用的需求,而且像作者所說,該專案非常剋制,沒有因為便利而加入一些會導致專案臃腫的功能,它只提供了資料的繫結,其他的功能我們只需要稍微加以封裝就可以實現。
為什麼要封裝
如果還沒用過這個庫的先去看看作者的文件
我們先來看看框架的原始用法:
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 的數量才能得到原始資料集的相對位置。
以上,就完成了本次的小封裝,趕緊去程式碼中實戰吧。