RecyclerView 通用适配器封装

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zgcqflqinhao/article/details/82988316

有很久没有写博客了,主要还是因为小 baby 的诞生,忙前忙后,跑来跑去的很少有整片整片的时间静下心来写博客,现在小楠终于回北京了,自己利用 SpringBoot 搭建博客也基本完成了,终于有了自己的个人博客。这段时间虽然没有写博客,但是平时也不是没有积累,只是零零散散的记在本子里,现在准备慢慢整理成博客记录下来。
第一篇还是来一个我觉得比较满意的,在做公司项目时,吸收了网上其他人的思想,封装了 RecyclerView 的通用适配器。

序言

RecyclerView 是用来替代 ListView 和 GridView 的控件,功能非常强大,有很强的扩展性,不过每次写适配器的时候,总会覆写那么几个相同的方法。虽然都是熟悉的配方,熟悉的味道,并没有什么难度,但是写多了也未免觉得繁琐,于是就萌生了封装 RecyclerView 通用适配器的想法,在此先感谢前辈的思想:

为RecyclerView打造通用Adapter 让RecyclerView更加好用

MultiType-Adapter 优雅的实现RecyclerVIew中的复杂布局

一、要实现的小目标

1.可以很快速的实现简单数据的 RecyclerView 的适配器,这里的简单不是指实体类的属性少,而是指适配器的数据类型只有一种。
2.可以很方便的实现一对多的数据类型,即一种数据对应多种布局,像很多聊天界面就有这样的需求。
3.可以很方便的实现多对多的数据类型,即多种数据对应多种布局。

二、思路

写程序就是理清了思路逻辑,然后再用代码实现这个逻辑就 OK 了,首先看看一般我们写一个适配器需要怎么做。

public class TestAdapter extends RecyclerView.Adapter<TestAdapter.MyViewHolder> {
    private List<String> stringList;

    public void setDatas(List<String> stringList) {
        this.stringList = stringList;
        notifyDataSetChanged();
    }

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_rv_test, parent, false);
        MyViewHolder vh = new MyViewHolder(v);
        return vh;
    }

    @Override
    public void onBindViewHolder(final MyViewHolder holder, int position) {
        holder.tvTest.setText(stringList.get(position));
    }


    @Override
    public int getItemCount() {
        return stringList == null ? 0 : stringList.size();
    }

    class MyViewHolder extends RecyclerView.ViewHolder {
        public TextView tvTest;

        public MyViewHolder(View itemView) {
            super(itemView);
            tvTest = (TextView) itemView.findViewById(R.id.tv_test);
        }
    }
}

是的,这几个方法是必不可少的,但是多写几个适配器后你会发现
onCreateViewHolder()方法 和 getItemCount() 方法基本是一样的,setDatas() 这种我们自己写的设置数据的方法也是一样的,还有复用的 ViewHolder 也差不多,只是里面定义的控件不同罢了,OK 我们就从这几个方法入手。

三、通用 BaseViewHolder

继承 RecyclerView.ViewHolder 时,它需要我们提供一个 itemView 作为每一个 item 的视图,然后在里面通过这个 itemView 的 findViewById() 方法来找到里面的子控件,用于我们设置数据,所以可以这样封装。

public class BaseViewHolder extends RecyclerView.ViewHolder {
    private Context context;
    private View itemView;
    private SparseArray<View> viewSparseArray;

    public BaseViewHolder(Context context, View itemView) {
        super(itemView);
        this.context = context;
        this.itemView = itemView;
        this.viewSparseArray = new SparseArray<View>();
    }

    public View getItemView() {
        return itemView;
    }

    public <T extends View> T findViewById(int viewId) {
        View view = viewSparseArray.get(viewId);
        if (view == null) {
            view = itemView.findViewById(viewId);
            viewSparseArray.put(viewId, view);
        }
        return (T) view;
    }
}

SparseArray 是 Android 特有的容器,相当于 key 为 int 型的 Map,控件的 id 正好是 int 型的,所以用这个容器来存放子控件,当调用 findViewById() 时,如果 SparseArray 中有则直接取出来返回,如果没有再调用 itemView 的 findViewById() 方法找到该控件返回,并存到容器中。这个通用的 BaseViewHolder 还可以继续封装,扩展很多常用的方法,稍后再说。

四、通用 BaseAdapter

public abstract class RcvBaseAdapter<T> extends RecyclerView.Adapter<BaseViewHolder> {

    private Context context;
    private int layoutId;
    private List<T> dataList = new ArrayList<>();

    public RcvBaseAdapter(Context context, int layoutId) {
        this.context = context;
        this.layoutId = layoutId;
    }

    @Override
    public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(context).inflate(layoutId, parent, false);
        return new BaseViewHolder(context, itemView);
    }

    @Override
    public void onBindViewHolder(final BaseViewHolder holder, int position) {
        bindViewHolder(holder, dataList.get(position), position);
    }

    @Override
    public int getItemCount() {
        return dataList == null ? 0 : dataList.size();
    }

    public abstract void bindViewHolder(BaseViewHolder holder, T itemData, int position);

    public Context getContext() {
        return context;
    }

    public List<T> getDataList() {
        return dataList;
    }

    public void setDataList(List<T> dataList) {
        this.dataList = dataList;
        notifyDataSetChanged();
    }
}

现在我们写适配器的时候只需要继承这个通用适配器,实现 bindViewHolder() 方法绑定我们自己的数据即可:

public class TestAdapter extends RcvBaseAdapter<String> {

    public TestAdapter(Context context) {
        super(context, R.layout.item_rv_test);
    }

    @Override
    public void bindViewHolder(BaseViewHolder holder, String itemData, int position) {
        TextView tvTest = holder.findViewById(R.id.tv_test);
        tvTest.setText(itemData);
    }
}

效果:

扫描二维码关注公众号,回复: 3883252 查看本文章

比起以前的适配器可是简化了太多。

五、扩展

在上面我们在对 TextView 设置文本时需要先找到这个控件再调用 TextView 的 setText() 方法,还有点小麻烦,作为程序员当然是能省则省,所以通用 BaseViewHolder 还可以扩展,增加一些常用的设置文本、设置图片、设置背景的方法,也可以为通用适配器 BaseAdapter 设置 Item 的点击事件的监听器,这些功能比较简单,就不贴代码了,如果需要可以看看文末的 Demo 地址中去查看完整的 BaseViewHolder 代码,这里主要看看比较麻烦的单数据对多布局,多数据对多布局。

六、多数据对多布局

多数据对多布局比较简单,先说这个,首先我们知道 Adapter 有一个 getItemViewType(int position) 方法,传入 Item 的 position 返回一个 int 值,默认的是所有的都会返回 0,也就是说所有的 Item 都是同一类型。Adapter 中创建 Holder 的方法 onCreateViewHolder(ViewGroup parent,int viewType) 返回一个 ViewHolder,没错,这里面的参数 viewType 就是上面那个方法返回的 int 值,那就是说我们可以在 onCreateViewHolder() 方法中根据不同的 viewType 来返回不同的 ViewHolder,从而实现不同的布局,也就是说重写 getItemViewType(int position) 方法。为什么说多对多比较简单,因为多对多就是一种实体类对应一个布局,不同的实体类的 hashCode 肯定不一样,Class 也不一样,我们就可以将 hashCode 作为 viewType 返回,在创建数据和绑定数据时根据 hashCode 来决定取哪个布局。用代码来说事:

public class RcvMultipleBaseAdapter extends RcvBaseAdapter {
    private SparseArray<BaseItemView> itemViewSparseArray;

    public RcvMultipleBaseAdapter(Context context) {
        super(context, 0);
        itemViewSparseArray = new SparseArray<>();
    }

    @Override
    public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return itemViewSparseArray.get(viewType).onCreateViewHolder(parent);
    }

    @Override
    public int getItemViewType(int position) {
        for (int i = 0; i < itemViewSparseArray.size(); i++) {
            BaseItemView baseItemView = itemViewSparseArray.valueAt(i);
            if (baseItemView.isForViewType(getDataList().get(position), position)) {
                return itemViewSparseArray.keyAt(i);
            }
        }
        throw new IllegalArgumentException("No ItemView added that matches position=" + position + " in data source");
    }

    @Override
    public void bindViewHolder(BaseViewHolder holder, Object itemData, int position) {
        for (int i = 0; i < itemViewSparseArray.size(); i++) {
            BaseItemView baseItemView = itemViewSparseArray.valueAt(i);
            if (baseItemView.isForViewType(itemData, position)) {
                baseItemView.bindViewHolder(holder, itemData, position);
                return;
            }
        }
    }

    public void addItemView(BaseItemView baseItemView) {
        itemViewSparseArray.put(baseItemView.hashCode(), baseItemView);
    }

    public void removeItemView(BaseItemView baseItemView) {
        itemViewSparseArray.remove(baseItemView.hashCode());
    }
}

用一个 SparseArray 来保存不同的 Item 布局,键就是多数据类型的 hashCode 值,正好是 int 型的满足 SparseArray,值当然就是 Item 布局了。由于是不同的布局绑定数据的操作肯定不一样,所以绑定数据肯定不能也没办法在 Adapter 中进行,交给继承 BaseItemView 的每一个布局自己去绑定,我们需要做的是调用 addItemView() 方法添加一种布局,然后适配器会在 getItemViewType() 方法时遍历 SparseArray 容器里的所有布局,当有布局匹配上时,则将该布局的数据类型的 hashCode 值作为 getItemViewType() 方法的返回值返回,如果遍历完都没有匹配上则主动抛出一个异常。onCreateViewHolder() 创建 ViewHolder 时根据 viewType 获取不同的 BaseItemView,然后让 BaseItemView 自己去创建 BaseViewHolder。同样在绑定数据时也是拿到 BaseItemView 让其自己去绑定数据。如何判断是否有与当前 position 的数据匹配的布局是在 BaseItemView 中的 isForViewType() 方法中通过反射拿到泛型来判断的:

public abstract class BaseItemView<T> {
    private Context context;
    private int layoutId;

    public BaseItemView(Context context, int layoutId) {
        this.context = context;
        this.layoutId = layoutId;
    }

    public BaseViewHolder onCreateViewHolder(ViewGroup parent) {
        View itemView = LayoutInflater.from(context).inflate(layoutId, parent, false);
        return new BaseViewHolder(context, itemView);
    }

    /**
     * Description:     子类可以覆蓋此方法决定引用该子布局的时机
     * Date:2018/8/6
     *
     * @param item     该position对应的数据
     * @param position position
     * @return 是否属于子布局
     */
    public boolean isForViewType(T item, int position) {
        Type type = getClass().getGenericSuperclass();
        ParameterizedType parameterizedType = (ParameterizedType) type;
        Class clazz = (Class) parameterizedType.getActualTypeArguments()[0];
        return item.getClass() == clazz;
    }

    /**
     * Description:绑定 UI 和数据
     * Date:2018/8/6
     */
    public abstract void bindViewHolder(BaseViewHolder holder, T itemData, int position);
}

BaseItemView 在多对多时需要指定泛型,这个泛型就是判断引入不同布局的条件,通过反射拿到泛型中的真实类型,然后与传入的 item 的类型比较,如果相同则返回 true 表示匹配成功,每一种布局的创建 VeiwHolder 和绑定数据的真正操作都是在这个 BaseItemView 中做的,RcvMultipleBaseAdapter 其实相当于起了一个中间分发者的作用。看看如何用吧:

MainActivity 中设置不同类型的数据(因为是不同类型,所以数据容器就没办法指定泛型了):

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        RecyclerView rvTest = (RecyclerView) findViewById(R.id.rv_test);
        TestAdapter testAdapter = new TestAdapter(this);
        rvTest.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
        rvTest.setAdapter(testAdapter);
        testAdapter.setDataList(getList());
    }

    private List getList() {
        List list = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                String string = "我是字符串" + i;
                list.add(string);
            } else {
                list.add(i);
            }
        }
        return list;
    }
}

TestAdapter,说了它只相当于一个中间分发者的角色,所以代码很简单,只是添加不同的布局类型,真正的绑定数据操作在 ItemView 中做:

public class TestAdapter extends RcvMultipleBaseAdapter {

    public TestAdapter(Context context) {
        super(context);
        addItemView(new StringItemView(context));
        addItemView(new IntegerItemView(context));
    }
}

StringItemView(String 类型的数据的布局):

public class StringItemView extends BaseItemView<String> {

    public StringItemView(Context context) {
        super(context, R.layout.item_rv_test_string);
    }

    @Override
    public void bindViewHolder(BaseViewHolder holder, String itemData, int position) {
        holder.setTvText(R.id.tv_test_string, "String 的 ItemView" + itemData);
    }
}

IntegerItemView(Integer 类型的数据的布局):

public class IntegerItemView extends BaseItemView<Integer> {

    public IntegerItemView(Context context) {
        super(context, R.layout.item_rv_test_integer);
    }

    @Override
    public void bindViewHolder(BaseViewHolder holder, Integer itemData, int position) {
        holder.setTvText(R.id.tv_test_integer, "Integer 的 ItemView" + itemData);
    }
}

xml 文件就不贴了,只是简单的背景颜色不同而已,运行结果如下:

这样多数据对多布局就实现了,而且这样封装后使用起来非常方便,各布局类型之间也没有耦合,非常方便管理。

七、单数据对多布局

为什么单数据对多布局更麻烦呢,因为单数据的 hashCode 和 Class 都是一样的,所以没办法很明确的判断何时引入哪种布局,多对多时只要使用上面的适配器,不用自己写判断条件就可以自动判断布局的引入时机,单数据时就得自己判断了,所以我们需要重写 BaseItemView 的 isForViewType() 方法,下面是例子:

MessageEntity:

public class MessageEntity {
    private String message;
    private int userType;   //0 表示发送的消息,1 表示接收的消息
    //省略构造方法,get、set、toString 方法
}

MainActivity 中设置数据的方法小改一下:

    private List<MessageEntity> getList() {
        List<MessageEntity> list = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            MessageEntity messageEntity = new MessageEntity();
            messageEntity.setMessage("你好");
            messageEntity.setUserType(i % 2);
            list.add(messageEntity);
        }
        return list;
    }

MessageItemView1,重写 isForViewType() 方法,userType 为 0 时表示是发出的消息:

public class MessageItemView1 extends BaseItemView<MessageEntity> {

    public MessageItemView1(Context context) {
        super(context, R.layout.item_rv_test_message1);
    }

    @Override
    public boolean isForViewType(MessageEntity item, int position) {
        if (item.getUserType() == 0) {
            return true;
        }
        return false;
    }

    @Override
    public void bindViewHolder(BaseViewHolder holder, MessageEntity itemData, int position) {
        holder.setTvText(R.id.tv_test_message1, "发出的消息" + itemData.getMessage());
    }
}

MessageItemView2,重写 isForViewType() 方法,userType 为 1 时表示是接收的消息:

public class MessageItemView2 extends BaseItemView<MessageEntity> {

    public MessageItemView2(Context context) {
        super(context, R.layout.item_rv_test_message2);
    }

    @Override
    public boolean isForViewType(MessageEntity item, int position) {
        Log.i("daolema", "position--->" + position);
        if (item.getUserType() == 1) {
            return true;
        }
        return false;
    }

    @Override
    public void bindViewHolder(BaseViewHolder holder, MessageEntity itemData, int position) {
        holder.setTvText(R.id.tv_test_message2, "接收的消息" + itemData.getMessage());
    }
}

运行结果如下:

八、总结

所有的程序都应该是方便的、易使用的,这次的封装虽然是借鉴的前辈们的思想,但是自己看过后能够根据自己封装一遍,也算是理解了。我一直都觉得应该是这样,之前喜欢研究自定义控件的时候也是,就算那些炫酷的开源控件可以拿来就用了,但是也想自己实现一遍,毕竟理解后再用,就算出了什么 Bug 也容易找到问题,知道从何改起。这次封装 RecyclerView 适配器更不谈了,我觉得封装虽然功能上并不会有什么大的突破,但是在结构上有很大的改善,方便易用低耦合。RecyclerView 真的很强大也很好用,这段时间积累了很多 RecyclerView 的用法,慢慢记录吧。

九、github 传送门

完整的 Demo

猜你喜欢

转载自blog.csdn.net/zgcqflqinhao/article/details/82988316