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

1,673 阅读5分钟
原文链接: www.jianshu.com

前言

现在在实际开发中,越来越多的人选择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吧,或者能提出建议更好。