MultiType 這個項目,至今 v3.x 穩定多時,考慮得很是多,但也作得很是剋制。原則一直是 直觀、靈活、可靠、簡單純粹(其中直觀和靈活是很是看重的)。java
這是 MultiType 框架做者給出的項目簡述。git
做爲一個 RecyclerView 的 Adapter 框架,感受這項目的設計很是的優雅,並且能夠知足不少經常使用的需求,並且像做者所說,該項目很是剋制,沒有由於便利而加入一些會致使項目臃腫的功能,它只提供了數據的綁定,其餘的功能咱們只須要稍微加以封裝就能夠實現。github
若是還沒用過這個庫的先去看看做者的文檔bash
public class Category {
@NonNull public final String text;
public Category(@NonNull String text) {
this.text = text;
}
}
複製代碼
ItemViewBinder 是個抽象類,其中 onCreateViewHolder 方法用於生產你的 item view holder, onBindViewHolder 用於綁定數據到 Views. 通常一個 ItemViewBinder 類在內存中只會有一個實例對象,MultiType 內部將複用這個 binder 對象來生產全部相關的 item views 和綁定數據。示例:app
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);
}
}
}
複製代碼
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 的用法,上手很快。框架
因此咱們的封裝就是爲了解決上面的兩個問題。ide
上面說到咱們封裝就是要解決上面提到的兩個問題,讓其更好用:佈局
第三點是隨便添加上去的,用於只有一個 TextView 的 Item。ui
思路其實很簡單,就是建立一個 BaseViewHolder 來代替咱們以前須要頻繁建立的 ViewHolder.this
廢話少說,看代碼:
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 來實現咱們的功能。代碼以下:
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)
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()));
複製代碼
以上就是整套的封裝了,很簡單,可是也很實用,能夠在平常開發中省下很多代碼。
上面說了,咱們還能夠經過繼承這個 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)
方法在進行綁定的時候進行控件的屬性修改等邏輯。
MultiType 其實自己就支持
HeaderView
、FooterView
,只要建立一個Header.class
-HeaderViewBinder
和Footer.class
-FooterViewBinder
便可,而後把new Header()
添加到items
第一個位置,把new Footer()
添加到items
最後一個位置。須要注意的是,若是使用了 Footer View,在底部插入數據的時候,須要添加到最後位置 - 1
,即倒二個位置,或者把Footer
remove 掉,再添加數據,最後再插入一個新的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 的數量才能獲得原始數據集的相對位置。
以上,就完成了本次的小封裝,趕忙去代碼中實戰吧。