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

前言

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容