美文网首页listviewAndroid知识Android开发
如何优雅地实现Adapter多布局列表

如何优雅地实现Adapter多布局列表

作者: zpayh | 来源:发表于2017-02-16 16:52 被阅读914次

    前言

    现在在实际开发中,越来越多的人选择RecyclerView来实现列表布局,而RecyclerView写多了,每次都要直接继承Adapter实现onCreateViewHolderonBindViewHoldergetItemCount这三个方法,虽然代码量不算很大,但每个XXXAdapter其实都长得差不多,这种重复性的代码,开发者是最不想写的了,所以网上就出现了很多封装Adapter的开源库。所以本篇文章也介绍自己封装的一个Adapter,帮你快速高效的添加一个列表(包括单Item列表和多Item列表)。

    预览

    先简单看一下最终效果:

    多Item列表

    而Adapter的代码量极少,感受一下:

    public class MyMultiAdapter extends BaseMultiAdapter {
    
        @Override
        public void bind(BaseViewHolder holder, int layoutRes) {
    
        }
    }
    

    嗯,没错,你只需要实现bind方法就可以了,而bind方法是用来设置View的一些一次性设置的,例如开启响应点击事件,长按事件等。所以上面我就什么都没写。

    总体思路

    • 实现一个通用的Adapter模版,避免写Adapter中大量的重复代码,抽象出几个接口。
    • 通过让数据类实现IMultiItem接口,把部分Adapter中的代码转移到具体的数据类中,而不用在Adapter去判断数据类型和ViewType。这样很容易添加新的Item(ViewType)类型,减少耦合,Adapter不用去感知IMultiItem的具体类型。
    • 高内聚,低耦合,方便扩展。
    • 封装ViewHolder,将对View的常用操作都加上去。

    实现

    我们先看一下BaseViewHolderBaseViewHolder封装了我们一些常用的操作,例如获取子View,设置item的点击事件,设置item的子View响应点击事件等。获取子View我用了Object[]数组进行缓存,没有用SparseArray来缓存View,主要是我之前看了Agera的源码,所以才用这种方式来缓存的,这里按下不表,下面是BaseAdapter的部分代码:

    public class BaseViewHolder extends RecyclerView.ViewHolder {
    
        private Object[] mIdsAndViews = new Object[0];
    
        /**
         * 设置响应点击事件,如果设置了clickable为true的话,在{@link BaseAdapter#setOnItemClickListener(OnItemClickListener)}
         * 中会得到响应事件的回调,详情参考{@link BaseAdapter#setOnItemClickListener(OnItemClickListener)}
         * @param id 响应点击事件的View Id
         * @param clickable true响应点击事件,false不响应点击事件
         */
        public BaseViewHolder setClickable(@IdRes int id, boolean clickable){
            View view = find(id);
            if (view != null){
                if (clickable){
                    view.setOnClickListener(mOnClickListener);
                }else{
                    view.setOnClickListener(null);
                }
            }
            return this;
        }
    
        /**
         * 根据当前id查找对应的View控件
         * @param viewId View id
         * @param <T> 子View的具体类型
         * @return 返回当前id对应的子View控件,如果没有,则返回null
         */
        @CheckResult
        public <T extends View> T find(@IdRes int viewId){
            int indexToAdd = -1;
            for (int i = 0; i < mIdsAndViews.length; i+=2) {
                Integer id = (Integer) mIdsAndViews[i];
                if (id != null && id == viewId){
                    return (T) mIdsAndViews[i+1];
                }
    
                if (id == null){
                    indexToAdd = i;
                }
            }
    
            if (indexToAdd == -1){
                indexToAdd = mIdsAndViews.length;
                mIdsAndViews = Arrays.copyOf(mIdsAndViews,
                        indexToAdd < 2 ? 2 : indexToAdd * 2);
            }
    
            mIdsAndViews[indexToAdd] = viewId;
            mIdsAndViews[indexToAdd+1] = itemView.findViewById(viewId);
            return (T) mIdsAndViews[indexToAdd+1];
        }
    }
    

    接下来我们来看一下BaseMultiAdapter里面做了什么?

    public abstract class BaseMultiAdapter extends BaseAdapter<IMultiItem> {
    
        @Override
        public int getLayoutRes(int index) {
            final IMultiItem data = mData.get(index);
            return data.getLayoutRes();
        }
    
        @Override
        public void convert(BaseViewHolder holder, IMultiItem data, int index) {
            data.convert(holder);
        }
    }
    

    是不是发现这里面也很少代码,因为很大一部分代码都在BaseAdapter中实现了,
    这里我们发现了一个IMultiItem,我们看一下它俩的源代码:

    public interface IMultiItem {
    
        /**
         * 不同类型的item请使用不同的布局文件,
         * 即使它们的布局是一样的,也要copy多一份出来。
         * @return 返回item对应的布局id
         */
        @LayoutRes int getLayoutRes();
    
        /**
         * 进行数据处理,显示文本,图片等内容
         * @param holder Holder Helper
         */
        void convert(BaseViewHolder holder);
    
        /**
         * 在布局为{@link android.support.v7.widget.GridLayoutManager}时才有用处,
         * 返回当前布局所占用的SpanSize
         * @return 如果返回的SpanSize <= 0 或者 > {@link GridLayoutManager#getSpanCount()}
         *  则{@link BaseAdapter} 会在{@link BaseAdapter#onAttachedToRecyclerView(RecyclerView)}
         *  自适应为1或者{@link GridLayoutManager#getSpanCount()},详情参考{@link BaseAdapter#onAttachedToRecyclerView(RecyclerView)}
         */
        int getSpanSize();
    }
    
    
    public abstract class BaseAdapter<T> extends RecyclerView.Adapter<BaseViewHolder> {
    
        protected final List<T> mData = new ArrayList<>();
    
        private BaseViewHolder.OnItemClickListener mOnItemClickListener;
    
        @Override
        public BaseViewHolder onCreateViewHolder(ViewGroup parent, int layoutRes) {
            BaseViewHolder baseViewHolder = new BaseViewHolder(LayoutInflater.from(parent.getContext())
                        .inflate(layoutRes, parent, false));
            bindData(baseViewHolder,layoutRes);
            return baseViewHolder;
        }
    
        @Override
        public final void onBindViewHolder(BaseViewHolder holder, int position) {
            //数据布局
            final T data = mData.get(position);
            convert(holder, data, position);
        }
    
        @Override
        public final int getItemCount() {
            return mData.size();
        }
    
        @Override
        public int getItemViewType(int position) {
            return getLayoutRes(position);
        }
    
        @Override
        public void onAttachedToRecyclerView(RecyclerView recyclerView) {
            super.onAttachedToRecyclerView(recyclerView);
            RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
            if (manager == null || !(manager instanceof GridLayoutManager)) return;
            final GridLayoutManager gridLayoutManager = (GridLayoutManager) manager;
            gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    final T data = getData(position);
                    if (data != null && data instanceof IMultiItem){
                        int spanSize = ((IMultiItem)data).getSpanSize();
                        return spanSize <= 0 ? 1 :
                                spanSize > gridLayoutManager.getSpanCount()?
                                gridLayoutManager.getSpanCount():spanSize;
                    }
                    return 1;
                }
            });
        }
    
        protected void bindData(BaseViewHolder baseViewHolder, int layoutRes) {
            baseViewHolder.setOnItemClickListener(new OnItemClickListener() {
                @Override
                public void onItemClick(@NonNull View view, int adapterPosition) {
                    if (mOnItemClickListener != null){
                        mOnItemClickListener.onItemClick(view, adapterPosition);
                    }
                }
            });
           
            bind(baseViewHolder, layoutRes);
        }
        /**
         * 返回布局layout
         */
        @LayoutRes
        public abstract int getLayoutRes(int index);
    
        /**
         * 在这里设置显示
         */
        public abstract void convert(BaseViewHolder holder, T data, int index);
    
        /**
         * 开启子view的点击事件,或者其他监听
         */
        public abstract void bind(BaseViewHolder holder,int layoutRes);
    }
    

    看到这里我们就能发现了,BaseAdapter已经写了大部分的代码,就留下getLayoutRes,convert,bind给子类去实现,而它的子类BaseMultiAdapter直接把getLayoutResconvert丢给了IMultiItem去实现。
    getLayoutRes是返回item对应的布局文件id,同时它在BaseAdapter也作为ViewType来使用,所以如果是不同类型的item,不建议共用同个布局文件。
    所以,我们的数据类只要实现IMultiItem接口即可,例如上面的文本类item:

    public class Text implements IMultiItem{
        public String mText;
    
        private int mSpanSize;
    
        public Text(String text,int spanSize) {
            mText = text;
            mSpanSize = spanSize;
        }
    
        @Override
        public int getLayoutRes() {
            return R.layout.item_text;
        }
    
        @Override
        public void convert(BaseViewHolder holder) {
            holder.setText(R.id.text,mText);
        }
    
        @Override
        public int getSpanSize() {
            return mSpanSize;
        }
    }
    

    getLayoutResconvert交给IMultiItem处理的好处就是实现多布局列表变得很简单,数据各自对应自己的布局文件,自己在convert方法中显示数据。

    源码

    上面的具体全部代码在都在我的开源库里,一个封装了RecyclerView.Adapter一些常用功能的库:SherlockAdapter
    文章写得有点简单了点,更好的学习方式是阅读源码,如果您喜欢的话,给我的github加个star吧,或者能提出建议更好。

    相关文章

      网友评论

      • A_si:动力是自己给的
        zpayh:@A_si 嗯嗯,我找到工作了:joy::joy:

      本文标题:如何优雅地实现Adapter多布局列表

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