美文网首页Android技术知识Android开发Android开发经验谈
基于 Multitype 开源库封装更好用的RecyclerVi

基于 Multitype 开源库封装更好用的RecyclerVi

作者: 鸡汤程序员 | 来源:发表于2018-11-12 18:03 被阅读46次

    前言

    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 。

    所以我们的封装就是为了解决上面的两个问题。

    封装

    问题

    上面说到我们封装就是要解决上面提到的两个问题,让其更好用:

    1. 封装 ViewHolder
    2. 添加点击事件
    3. 添加 Sample Binder
    4. 添加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 其实本身就支持 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 的数量才能得到原始数据集的相对位置。

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

    相关文章

      网友评论

        本文标题:基于 Multitype 开源库封装更好用的RecyclerVi

        本文链接:https://www.haomeiwen.com/subject/yfgcfqtx.html