基於 Multitype 開源庫封裝更好用的RecyclerView.Adapter

前言

MultiType 這個項目,至今 v3.x 穩定多時,考慮得很是多,但也作得很是剋制。原則一直是 直觀、靈活、可靠、簡單純粹(其中直觀和靈活是很是看重的)。java

這是 MultiType 框架做者給出的項目簡述。git

做爲一個 RecyclerView 的 Adapter 框架,感受這項目的設計很是的優雅,並且能夠知足不少經常使用的需求,並且像做者所說,該項目很是剋制,沒有由於便利而加入一些會致使項目臃腫的功能,它只提供了數據的綁定,其餘的功能咱們只須要稍微加以封裝就能夠實現。github

爲何要封裝

若是還沒用過這個庫的先去看看做者的文檔bash

咱們先來看看框架的原始用法:

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 和綁定數據。示例: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);
        }
    }
}
複製代碼

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 。

因此咱們的封裝就是爲了解決上面的兩個問題。ide

封裝

問題

上面說到咱們封裝就是要解決上面提到的兩個問題,讓其更好用:佈局

  1. 封裝 ViewHolder
  2. 添加點擊事件
  3. 添加 Sample Binder
  4. 添加Header、Footer

第三點是隨便添加上去的,用於只有一個 TextView 的 Item。ui

方案

1. 封裝ViewHolder

思路其實很簡單,就是建立一個 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

爲了解決咱們上面問題中的第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 其實自己就支持 HeaderViewFooterView,只要建立一個 Header.class - HeaderViewBinderFooter.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 實例給添加進去,最後調用AdapternotifyItemInserted方法刷新一下列表就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);
    }
複製代碼

onBindViewHoldergetItemViewType的實現思想相似,判斷是不是 Header/Footer 拿到相應的實體類,而後進行綁定。整個流程就是這樣,固然別忘了也要在 getItemCount方法中將咱們的 Header 與 Footer 的數量加進入,如:

@Override
public final int getItemCount() {
    int extensionSize = getHeaderSize() + getFooterSize();
    return super.getItemCount() + extensionSize;
}
複製代碼

這樣的封裝可讓咱們的 Header/Footer 裏面的數據集與本來的數據集分離,咱們的主數據再怎麼增刪查改都不會影響到Header/Footer 的正確性。

這樣的實現目前有個比較蛋疼的點,咱們調用ViewHoldergetAdapterPosition()時候會返回實際的 position,即包含了 Header 的數量,目前這點還沒解決,須要手動把該 position 減去 Header 的數量才能獲得原始數據集的相對位置。

以上,就完成了本次的小封裝,趕忙去代碼中實戰吧。

相關文章
相關標籤/搜索