基于 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 的数量才能获得原始数据集的相对位置。

以上,就完成了本次的小封装,赶忙去代码中实战吧。

相关文章
相关标签/搜索