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的使用,但上面的寫法存在幾個問題:
- 上面的寫法把Item的佈局和item的類型寫死在了Adapter中,如果我們想再實現一個使用其他的item佈局展示其他數據的列表,上面的Adapter就無法使用。
- 無法使用多佈局,項目中經常存在一個列表中使用多種佈局的場景,遇到這種場景,上面的Adapter也無法使用。
- 沒有留出設置各類事件回調的方法,比如設置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;
}
}
}