欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Databinding之RecyclerViewAdapter使用与封装

程序员文章站 2022-05-14 23:46:51
...

Databinding之RecyclerViewAdapter使用与封装

RecyclerViewAdapter大家都不陌生,那么在使用Databinding时,RecyclerViewAdapter该如何编写呢?

本文用一个邮箱类型列表作为案例,来讲解在使用Databinding时如何编写RecyclerViewAdapter,并且如何有效的封装RecyclerViewAdapter。

简单使用

首先编写MailType类,text和icon分别表示邮箱类型的名称以及图标。

/**
 * Created by gongw on 2018/7/11.
 */

public class MailType {
    private String text;
    private String icon;

    public MailType(String text, String icon)
    {
        this.text = text;
        this.icon = icon;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

    public String getIcon() {
        return icon;
    }

    public void setIcon(String icon) {
        this.icon = icon;
    }
}

编写MailTypeModell类,通过getDatas()提供邮箱类型数据。

/**
 * Created by gongw on 2018/7/11.
 */

public class MailTypeModel {

    private static class InstanceHolder {
        static final MailTypeModel INSTANCE = new MailTypeModel();
    }

    private MailTypeModel(){}

    public static MailTypeModel getInstance(){
        return InstanceHolder.INSTANCE;
    }

    public List<MailType> getDatas(){
        List<MailType> datas = new ArrayList<>();
        String[] types = BaseApplication.getContext().getResources().getStringArray(R.array.mail_type);

        for(String type : types){
            String[] mailType = type.split("=");
            datas.add(new MailType(mailType[0], mailType[1]));
        }

        return datas;
    }
}

这里使用一个简单的StringArray作为数据源,用“=“分割邮箱类型的名称和图标资源名称。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <array name="mail_type">
        <item>QQ邮箱=icqqmail</item>
        <item>139邮箱=ic139mail</item>
        <item>189邮箱=ic189mail</item>
        <item>阿里邮箱=ic_alimail</item>
    </array>
</resources>

编写邮箱类型的item布局。

这里使用了Databinding的一个自定义属性的功能,为ImageView设置了一个drawable的属性,功能是通过图片名称获取资源id,并为imageview设置图片。

@BindingAdapter({"drawable"})
    public static void setImage(ImageView imageView, String icon){
        int r_id = imageView.getResources().getIdentifier(icon, "drawable", imageView.getContext().getPackageName());
        imageView.setImageResource(r_id);
    }
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="mailType"
            type="com.gongw.login.model.MailType"/>
    </data>
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="60dp"
    android:orientation="horizontal"
    android:paddingStart="26dp"
    android:paddingEnd="26dp">

    <ImageView
        android:layout_width="36dp"
        android:layout_height="36dp"
        android:layout_gravity="center_vertical"
        app:drawable="@{mailType.icon}"/>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_marginStart="26dp"
        android:layout_marginLeft="26dp"
        android:gravity="center_vertical"
        android:singleLine="true"
        android:text="@{mailType.text}" />

</LinearLayout>
</layout>

编写MailTypeAdapter,可以看到使用Databinding可以有效的减少Adapter中代码的行数,在onBindViewHolder方法中,只需要一行binding.setMailType即可为item布局中的视图设置数据。

/**
 * Created by gongw on 2018/7/14.
 */

public class MailTypeAdapter extends RecyclerView.Adapter<MailTypeAdapter.MailTypeViewHolder> {
    private List<MailType> itemDatas;

    public MailTypeAdapter(List<MailType> itemDatas){
        this.itemDatas = itemDatas;
    }

    @Override
    public MailTypeAdapter.MailTypeViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ItemMailTypeBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.item_mail_type, parent, false);
        return new MailTypeViewHolder(binding);
    }

    @Override
    public void onBindViewHolder(MailTypeAdapter.MailTypeViewHolder holder, int position) {
        holder.binding.setMailType(itemDatas.get(position));
        holder.binding.executePendingBindings();
    }

    @Override
    public int getItemCount() {
        return itemDatas.size();
    }

    class MailTypeViewHolder extends RecyclerView.ViewHolder{

        ItemMailTypeBinding binding;

        public MailTypeViewHolder(ItemMailTypeBinding binding) {
            super(binding.getRoot());
            this.binding = binding;
        }

    }
}

将MailType数据通过MailTypeAdapter绑定到RecyclerView。

public class MailTypeFragment extends BaseFragment {

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
       FragmentMailTypeBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_mail_type, container, false);

        RecyclerView recyclerView = binding.recyclerView;
        recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext(), LinearLayoutManager.VERTICAL, false));

        MailTypeAdapter adapter = new MailTypeAdapter(MailTypeModel.getInstance().getDatas());
        recyclerView.setAdapter(adapter);
        return binding.getRoot();
    }

}

运行效果
Databinding之RecyclerViewAdapter使用与封装

通用Adapter封装

上面我们已经成功用Databinding实现了RecyclerviewAdapter的使用,但上面的写法存在几个问题:

  1. 上面的写法把Item的布局和item的类型写死在了Adapter中,如果我们想再实现一个使用其他的item布局展示其他数据的列表,上面的Adapter就无法使用。
  2. 无法使用多布局,项目中经常存在一个列表中使用多种布局的场景,遇到这种场景,上面的Adapter也无法使用。
  3. 没有留出设置各类事件回调的方法,比如设置item的点击事件等等。

鉴于上面的问题,这里我们尝试编写一个适用于任何场景的Adapter类。

首先,来解决问题1。

为了能适用于不同的数据,我们不能将数据类型写死在Adapter中,需要用泛型替代具体的数据类型,其次,item的布局也不能写死,需要从构造方法中注入。

在封装过程中会遇到这样一个问题,由于Databinding是与布局绑定的,布局的不固定会导致binding在设置数据时无法用具体的binding.setMailType这样的写法,只能通过binding.setVariable的方式,而这个方法需要提供一个BR id,所以BR id也需要从构造方法中注入。

下面是具体的实现

/**
 * Created by gongw on 2018/7/13.
 */

public class SimpleAdapter<T> extends RecyclerView.Adapter<SimpleAdapter.BaseViewHolder> {

    private List<T> itemDatas;
    private int layoutId;
    private int brId;

    public SimpleAdapter(List<T> itemDatas, int layoutId, int brId){
        this.itemDatas = itemDatas;
        this.layoutId = layoutId;
        this.brId = brId;
    }

    @Override
    public SimpleAdapter.BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), layoutId, parent, false);
        return new BaseViewHolder(binding);
    }

    @Override
    public void onBindViewHolder(SimpleAdapter.BaseViewHolder holder, int position) {
        holder.binding.setVariable(brId, itemDatas.get(position));
        holder.binding.executePendingBindings();
    }

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

    class BaseViewHolder extends RecyclerView.ViewHolder {
        ViewDataBinding binding;

        BaseViewHolder(ViewDataBinding binding) {
            super(binding.getRoot());
            this.binding = binding;
        }
    }

}

通过上面的修改,SimpleAdapter已经可以适用于不同的数据类型和item布局的情况了,成功解决问题1;

现在再来看问题2,如何使Adapter适配多布局的场景。

这里假定一个这样的场景,当邮箱类型的是QQ邮箱时,需要用第二种item布局,将邮箱的图标和文字位置互换。

我们都知道,要适配多种布局需要实现RecyclerViewAdapter的getItemViewType方法,通过不同的viewType来对应不同的item布局。这里我们可以取个巧,因为viewType和item布局都是int类型,所以可以在getItemViewType方法中直接返回item布局id,而具体返回哪个item布局id则交由外部实现。

这里定义一个getItemLayout的方法,默认返回构造方法传入的item布局。外部可以通过重写这个方法来修改返回item布局的逻辑,以此应对多布局的场景。

public class SimpleAdapter<T> extends RecyclerView.Adapter<SimpleAdapter.BaseViewHolder> {

    private List<T> itemDatas;
    private int defaultLayout;
    private int brId;

    public SimpleAdapter(List<T> itemDatas, int defaultLayout, int brId){
        this.itemDatas = itemDatas;
        this.defaultLayout = defaultLayout;
        this.brId = brId;
    }

    public int getItemLayout(T itemData){
        return defaultLayout;
    }

    @Override
    public int getItemViewType(int position) {
        return getItemLayout(itemDatas.get(position));
    }

    @Override
    public SimpleAdapter.BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), viewType, parent, false);
        return new BaseViewHolder(binding);
    }

    @Override
    public void onBindViewHolder(SimpleAdapter.BaseViewHolder holder, int position) {
        holder.binding.setVariable(brId, itemDatas.get(position));
        holder.binding.executePendingBindings();
    }

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

    class BaseViewHolder extends RecyclerView.ViewHolder {
        ViewDataBinding binding;

        BaseViewHolder(ViewDataBinding binding) {
            super(binding.getRoot());
            this.binding = binding;
        }
    }

}

重写getItemLayout方法,返回Adapter应该使用的item布局,这里如果是QQ邮箱就使用R.layout.item_mail_type1,否则使用R.layout.item_mail_type作为item布局。

/**
 * Created by gongw on 2018/7/10.
 */

public class MailTypeFragment extends BaseFragment {

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        FragmentMailTypeBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_mail_type, container, false);

        RecyclerView recyclerView = binding.recyclerView;
        recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext(), LinearLayoutManager.VERTICAL, false));

        SimpleAdapter<MailType> adapter = new SimpleAdapter<MailType>(MailTypeModel.getInstance().getDatas(), R.layout.item_mail_type, BR.mailType){
            @Override
            public int getItemLayout(MailType itemData) {
                return itemData.getText().equals("QQ邮箱") ? R.layout.item_mail_type1 : R.layout.item_mail_type;
            }
        };

        recyclerView.setAdapter(adapter);
        return binding.getRoot();
    }

}

编写第二种item布局R.layout.item_mail_type1。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="mailType"
            type="com.gongw.login.model.MailType"/>
    </data>
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="@dimen/item_height_normal"
    android:orientation="horizontal"
    android:paddingStart="26dp"
    android:paddingEnd="26dp">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_marginStart="26dp"
        android:layout_marginLeft="26dp"
        android:gravity="center_vertical"
        android:singleLine="true"
        android:text="@{mailType.text}" />

     <ImageView
        android:layout_width="36dp"
        android:layout_height="36dp"
        android:layout_gravity="center_vertical"
        app:drawable="@{mailType.icon}"/>

</LinearLayout>
</layout>

运行效果,可以看到QQ邮箱使用的布局与其他邮箱是不同的

Databinding之RecyclerViewAdapter使用与封装

通过上面的修改,SimpleAdapter可以通过重写getItemLayout来应对多布局的场景了,问题2解决;

现在再来看问题3,需要提供设置item事件回调的方法。

事件回调封装的思想主要有两点:首先需要在onBindViewHolder中设置回调,因为只有在这个方法中才可以拿到具体的view和position。其次,item布局中可能有多个不同的view,每个view都应该可以设置自己的事件回调,而且事件回调的类型根据view的类型可能有很多种,如一般view都有的onclick,checkbox的onCheckedChange,srcollview的onScrollChange等等,这些类型无法用一个抽象的参数来表示。

基于以上两点,我决定将事件绑定的整套逻辑交由外部实现,adapter向外部提供事件绑定所需的资源即view、itemData和position。这里提供一个空方法addListener供外部实现,onBindViewHolder中通过调用addListener来设置事件回调。

/**
 * Created by gongw on 2018/7/13.
 */

public class SimpleAdapter<T> extends RecyclerView.Adapter<SimpleAdapter.BaseViewHolder> {

    private List<T> itemDatas;
    private int defaultLayout;
    private int brId;

    public SimpleAdapter(List<T> itemDatas, int defaultLayout, int brId){
        this.itemDatas = itemDatas;
        this.defaultLayout = defaultLayout;
        this.brId = brId;
    }

    public int getItemLayout(T itemData){
        return defaultLayout;
    }

    public void addListener(View root, T itemData, int position){}

    @Override
    public int getItemViewType(int position) {
        return getItemLayout(itemDatas.get(position));
    }

    @Override
    public SimpleAdapter.BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), viewType, parent, false);
        return new BaseViewHolder(binding);
    }

    @Override
    public void onBindViewHolder(SimpleAdapter.BaseViewHolder holder, int position) {
        holder.binding.setVariable(brId, itemDatas.get(position));
        addListener(holder.binding.getRoot(), itemDatas.get(position), position);
        holder.binding.executePendingBindings();
    }

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

    class BaseViewHolder extends RecyclerView.ViewHolder {
        ViewDataBinding binding;

        BaseViewHolder(ViewDataBinding binding) {
            super(binding.getRoot());
            this.binding = binding;
        }
    }

}

重写addListener方法,自行设置item中具体view的具体事件。

/**
 * Created by gongw on 2018/7/10.
 */

public class MailTypeFragment extends BaseFragment {

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        FragmentMailTypeBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_mail_type, container, false);

        RecyclerView recyclerView = binding.recyclerView;
        recyclerView.setLayoutManager(new LinearLayoutManager(recyclerView.getContext(), LinearLayoutManager.VERTICAL, false));
        recyclerView.addItemDecoration(new RecyclerViewDivider(recyclerView.getContext(), LinearLayoutManager.VERTICAL));

        SimpleAdapter<MailType> adapter = new SimpleAdapter<MailType>(MailTypeModel.getInstance().getDatas(), R.layout.item_mail_type, BR.mailType){
            @Override
            public void addListener(View root, MailType itemData, final int position) {
                root.findViewById(R.id.textView).setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        Toast.makeText(getActivity(), "textView clicked!", Toast.LENGTH_SHORT).show();
                    }
                });

                root.findViewById(R.id.imageView).setOnLongClickListener(new View.OnLongClickListener() {
                    @Override
                    public boolean onLongClick(View v) {
                        Toast.makeText(getActivity(), "imageView long clicked!", Toast.LENGTH_SHORT).show();
                        return true;
                    }
                });
            }
        };

        recyclerView.setAdapter(adapter);
        return binding.getRoot();
    }

}

通过上面的修改,SimpleAdapter可以通过重写addListener方法来设置多种类型的事件回调了,问题3解决;

除了上面提到的问题,一个成熟的RecyclerViewAdapter还应该提供一些常用的方法供项目中方便使用,如item数据变化、范围变化、范围增加、范围删除时,Adapter自动刷新列表的方法。

    public void onItemDatasChanged(List<T> newItemDatas){
        this.itemDatas = newItemDatas;
        notifyDataSetChanged();
    }

    protected void onItemRangeChanged(List<T> newItemDatas, int positionStart, int itemCount)
    {
        this.itemDatas = newItemDatas;
        notifyItemRangeChanged(positionStart,itemCount);
    }

    protected void onItemRangeInserted(List<T> newItemDatas, int positionStart, int itemCount)
    {
        this.itemDatas = newItemDatas;
        notifyItemRangeInserted(positionStart,itemCount);
    }

    protected void onItemRangeRemoved(List<T> newItemDatas, int positionStart, int itemCount)
    {
        this.itemDatas = newItemDatas;
        notifyItemRangeRemoved(positionStart,itemCount);
    }

最终得出的产物如下,所有代码加起来只有80多行,却可以应对项目中的绝大多数场景重复使用,也可以说得上短小精悍了。

package com.gongw.common.adapter;

import android.databinding.DataBindingUtil;
import android.databinding.ViewDataBinding;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.List;

/**
 * Created by gongw on 2018/7/13.
 */

public class SimpleAdapter<T> extends RecyclerView.Adapter<SimpleAdapter.BaseViewHolder> {

    private List<T> itemDatas;
    private int defaultLayout;
    private int brId;

    public SimpleAdapter(List<T> itemDatas, int defaultLayout, int brId){
        this.itemDatas = itemDatas;
        this.defaultLayout = defaultLayout;
        this.brId = brId;
    }

    public int getItemLayout(T itemData){
        return defaultLayout;
    }

    public void addListener(View root, T itemData, int position){}

    public void onItemDatasChanged(List<T> newItemDatas){
        this.itemDatas = newItemDatas;
        notifyDataSetChanged();
    }

    protected void onItemRangeChanged(List<T> newItemDatas, int positionStart, int itemCount)
    {
        this.itemDatas = newItemDatas;
        notifyItemRangeChanged(positionStart,itemCount);
    }

    protected void onItemRangeInserted(List<T> newItemDatas, int positionStart, int itemCount)
    {
        this.itemDatas = newItemDatas;
        notifyItemRangeInserted(positionStart,itemCount);
    }

    protected void onItemRangeRemoved(List<T> newItemDatas, int positionStart, int itemCount)
    {
        this.itemDatas = newItemDatas;
        notifyItemRangeRemoved(positionStart,itemCount);
    }

    @Override
    public int getItemViewType(int position) {
        return getItemLayout(itemDatas.get(position));
    }

    @Override
    public SimpleAdapter.BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), viewType, parent, false);
        return new BaseViewHolder(binding);
    }

    @Override
    public void onBindViewHolder(SimpleAdapter.BaseViewHolder holder, int position) {
        holder.binding.setVariable(brId, itemDatas.get(position));
        addListener(holder.binding.getRoot(), itemDatas.get(position), position);
        holder.binding.executePendingBindings();
    }

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

    class BaseViewHolder extends RecyclerView.ViewHolder {
        ViewDataBinding binding;

        BaseViewHolder(ViewDataBinding binding) {
            super(binding.getRoot());
            this.binding = binding;
        }
    }

}