はじめに
athornz氏がGistにMergeRecyclerAdapter.javaというのをあげていたので使ってみたけど、便利だったので紹介します。
なお僕が使っているバージョンは少し手を入れています。
MergeRecyclerAdapter.java(Cattaka ver)
MergeRecyclerAdapterの用途
1つのリストに複数の属性のデータを表示したいとき、1つのアダプターのデータクラスをObjectなどにして、インスタンスの型に合わせてRecyclerView.Adapter#getItemViewType(int)の戻り値を切り替えすなど、少々面倒なことをする必要がありました。この場合、表示するデータクラスの組み合わせが変わるたびにアダプターを作り変えることになり、結構なコストが掛かってしまいます。
MergeRecyclerAdapterを使えば、それぞれのデータクラス用のアダプターを個別に作り、それらを連結するということができるので、それらの煩わしさを解消することができます。また他のメリットとしてヘッダーやフッターを付けるのに使うこともできます。デメリットとしてはクリックイベントのハンドリングが少しコツがいるようになります。
ヘッダーとフッターをつける
ヘッダー用、データ用、フッター用の3つのアダプターを作り、MergeRecyclerAdapterを用いて繋げます。
ここではヘッダーとフッターには単独のレイアウトを表示するSingleViewAdapterを使っています。
RecyclerViewHeaderExampleActivity.java より
{ // prepare adapters
mMergeRecyclerAdapter = new MergeRecyclerAdapter<>(this);
{ // create header adapter
mHeaderAdapter = new SingleViewAdapter(this, R.layout.view_header);
mHeaderAdapter.setOnItemClickListener(this);
mHeaderAdapter.setOnItemLongClickListener(this);
mMergeRecyclerAdapter.addAdapter(mHeaderAdapter);
}
{ // create items adapter
List<String> items = new ArrayList<>();
for (int i = 0; i < 5; i++) {
items.add("item " + i);
}
mItemsAdapter = new SimpleStringAdapter(this, items);
mItemsAdapter.setOnItemClickListener(this);
mItemsAdapter.setOnItemLongClickListener(this);
mMergeRecyclerAdapter.addAdapter(mItemsAdapter);
}
{ // create footer adapter
mFooterAdapter = new SingleViewAdapter(this, R.layout.view_footer);
mFooterAdapter.setOnItemClickListener(this);
mFooterAdapter.setOnItemLongClickListener(this);
mMergeRecyclerAdapter.addAdapter(mFooterAdapter);
}
{
mRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
mRecyclerView.setAdapter(mMergeRecyclerAdapter);
}
}
複数のデータ型を表示する
次の例はStringのデータとNumberのデータを1つのRecyclerViewに表示しています。
Stringのデータを表示するためにSimpleStringAdapterを、Numberのデータを表示するためにSimpleNumberAdapterを使っています。それぞれのヘッダーにはSingleViewAdapterを使っています。
MultiAdapterExampleActivity.java より
{ // prepare adapters
mMergeRecyclerAdapter = new MergeRecyclerAdapter<>(this);
{ // create strings header adapter
mStringsHeaderAdapter = new SingleViewAdapter(this, R.layout.view_header_string);
mStringsHeaderAdapter.setOnItemClickListener(this);
mStringsHeaderAdapter.setOnItemLongClickListener(this);
mMergeRecyclerAdapter.addAdapter(mStringsHeaderAdapter);
}
{ // create strings adapter
List<String> items = new ArrayList<>();
for (int i = 0; i < 3; i++) {
items.add("item " + i);
}
mStringsAdapter = new SimpleStringAdapter(this, items);
mStringsAdapter.setOnItemClickListener(this);
mStringsAdapter.setOnItemLongClickListener(this);
mMergeRecyclerAdapter.addAdapter(mStringsAdapter);
}
{ // create numbers header adapter
mNumbersHeaderAdapter = new SingleViewAdapter(this, R.layout.view_header_number);
mNumbersHeaderAdapter.setOnItemClickListener(this);
mNumbersHeaderAdapter.setOnItemLongClickListener(this);
mMergeRecyclerAdapter.addAdapter(mNumbersHeaderAdapter);
}
{ // create numbers adapter
List<Number> items = new ArrayList<>();
for (int i = 0; i < 3; i++) {
items.add(i);
}
mNumbersAdapter = new SimpleNumberAdapter(this, items);
mNumbersAdapter.setOnItemClickListener(this);
mNumbersAdapter.setOnItemLongClickListener(this);
mMergeRecyclerAdapter.addAdapter(mNumbersAdapter);
}
{
mRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
mRecyclerView.setAdapter(mMergeRecyclerAdapter);
}
}
クリックイベントのハンドリング
MergeRecyclerAdapterは性質上、複数のアダプターが同居することになるので「何番目の要素がクリックされたときのイベント」をハンドリングするときに、どのアダプターの要素なのか識別することが必要です。その仕組みはMergeRecyclerAdapter#getAdapterOffsetForItem(int)として提供されています。このメソッドは戻り値としてLocalAdapterを返します。LocalAdapterからはオリジナルのアダプターへの参照とそのアダプター内のポジション(インデックス)が取得できます。
僕がクリックイベントをハンドリングするときは自作のCustomRecyclerAdapter.javaと組み合わせて、以下のようなコードでハンドリングしています(CustomRecyclerAdapter.javaについては後述の余談で解説します)。
MultiAdapterExampleActivity.java より
@Override
public void onItemClick(RecyclerView parent, CustomRecyclerAdapter adapter, int position,
int id, RecyclerView.ViewHolder vh) {
if (parent.getId() == R.id.recycler) {
MergeRecyclerAdapter.LocalAdapter la = mMergeRecyclerAdapter.getAdapterOffsetForItem(position);
if (la.mAdapter == mStringsHeaderAdapter) {
Toast.makeText(this, "Strings Header is clicked.", Toast.LENGTH_SHORT).show();
} else if (la.mAdapter == mStringsAdapter) {
String item = mStringsAdapter.getItemAt(la.mLocalPosition);
Toast.makeText(this, item + " is clicked.", Toast.LENGTH_SHORT).show();
} else if (la.mAdapter == mNumbersHeaderAdapter) {
Toast.makeText(this, "Numbers Header is clicked.", Toast.LENGTH_SHORT).show();
} else if (la.mAdapter == mNumbersAdapter) {
Number item = mNumbersAdapter.getItemAt(la.mLocalPosition);
Toast.makeText(this, item + " is clicked.", Toast.LENGTH_SHORT).show();
} else if (la.mAdapter == mFooterAdapter) {
Toast.makeText(this, "Footer is clicked.", Toast.LENGTH_SHORT).show();
}
}
}
余談
ヘッダーやフッターにわざわざSingleViewAdapterを使う件
僕がヘッダーやフッターを作るときは、わざわざレイアウトXMLを作らなくてはいけないSingleViewAdapterを使っています。正直なところ面倒です。文字列を渡したら表示するだけのアダプターを作っておいて、それを使えばいちいちレイアウトXMLを作らなくても済みます。
でもデザイナさんに外観の調整をお願いしてると、外観を個別に調整したいという声が出てきます。そういった声に対応できるように意図してヘッダーやフッターのレイアウトXMLを分けています。
CustomRecyclerAdapter.javaを噛ませてる件
RecyclerViewには作りの都合上、ListView(というかAdapterView)のOnItemClickListenerやOnItemLongClickListenerがありません。だからといって個別にListener用のinterfaceを定義して、そのイベントの引き回しを実装していると面倒くさいです。その辺りの兼ね合いからCustomRecyclerAdapterを使うようにしています。
ただ正直使うときに若干クセがあり、onCreateViewHolderの実装に注意する必要があるので、万人にお薦めできるものでは無いです。onCreateViewHolder内でクリックイベントをハンドリングしたいViewに対して、setTag/setOnClickListenerを呼んであげないといけないです。