Databinding之RecyclerViewAdapter使用與封裝

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();
    }

}

運行效果
這裏寫圖片描述

通用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郵箱使用的佈局與其他郵箱是不同的

這裏寫圖片描述

通過上面的修改,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;
        }
    }

}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章