お題
RecyclerViewでセクション分けをしたい。ここではStringのリストとNumberのリストを、セクション分けして表示するRecyclerViewを例に説明します。
愚直にやる例
RecyclerView.Adapter#getItemViewType(int position)でセクションと表示する要素とで異なる値を返すようにするとできます。
愚直に実装すると次のようになります。
public class SimpleAndHonestAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
static final int UNKNOWN = 0;
static final int STRING_HEADER = 1;
static final int STRING_DATA = 2;
static final int NUMBER_HEADER = 3;
static final int NUMBER_DATA = 4;
List<String> mStrings;
List<Number> mNumbers;
public SimpleAndHonestAdapter(List<String> strings, List<Number> Numbers) {
mStrings = strings;
mNumbers = Numbers;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
RecyclerView.ViewHolder holder;
switch (viewType) {
case STRING_HEADER: {
holder = /* Stringのヘッダー用のViewHolderを作る */;
}
case STRING_DATA: {
holder = /* Stringのデータ用のViewHolderを作る */;
}
case NUMBER_HEADER: {
holder = /* Numberのヘッダー用のViewHolderを作る */;
}
case NUMBER_DATA: {
holder = /* Numberのデータ用のViewHolderを作る */;
}
case UNKNOWN:
default: { // ありえないエラーケースなので適当なのを返しておく
holder = new RecyclerView.ViewHolder(new View(parent.getContext())) {
};
}
}
return holder;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
int viewType = getItemViewType(position);
switch (viewType) {
case STRING_DATA: {
String data = mStrings.get(position - 1);
/* ここでholderにdataの内容を設定する */
}
case NUMBER_DATA: {
Number data = mNumbers.get(position - 2 - mStrings.size());
/* ここでholderにdataの内容を設定する */
}
}
}
@Override
public int getItemViewType(int position) {
if (position == 0) {
return STRING_HEADER;
} else if (position < mStrings.size() + 1) {
return STRING_DATA;
} else if (position == mStrings.size() + 1) {
return NUMBER_HEADER;
} else {
return NUMBER_DATA;
}
}
@Override
public int getItemCount() {
return mStrings.size() + mNumbers.size() + 2;
}
}
onCreateViewHolder / onBindViewHolder / getItemViewType / getItemCount メソッドそれぞれがインデックスを考慮するようにしなければならず、改修するたびに組み換えが必要で物凄い大変です。セクションの数が増えたら増えた分だけ雪だるま式に大変になります。
MergeRecyclerAdapterを使えばこの大変さから解放されます。
MergeRecyclerAdapterを使う例
前準備として次のような、それぞれのデータを単独で表示するアダプターがあるとします。
- StringAdapter : Stringのリスト用のアダプター
- NumberAdapter : Numberのリスト用のアダプター
- SingleViewAdapter : 単一のViewを表示できるアダプター(ヘッダー用)
MergeRecyclerAdapterを使えば専用のアダプターを作ること無く、既存のアダプターを流用して次のようにシンプルに書くことができます。
// find views
mRecyclerView = (RecyclerView) findViewById(R.id.recycler);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
{ // prepare adapters
mMergeRecyclerAdapter = new MergeRecyclerAdapter<>(this);
{ // create strings header adapter
SingleViewAdapter stringsHeaderAdapter = new SingleViewAdapter(this, R.layout.view_header_string);
mMergeRecyclerAdapter.addAdapter(stringsHeaderAdapter);
}
{ // create strings adapter
List<String> items = /* Stringのリストを準備する */;
StringAdapter stringsAdapter = new StringAdapter(this, items, mListenerRelay);
mMergeRecyclerAdapter.addAdapter(stringsAdapter);
}
{ // create numbers header adapter
SingleViewAdapter numbersHeaderAdapter = new SingleViewAdapter(this, R.layout.view_header_number);
mMergeRecyclerAdapter.addAdapter(numbersHeaderAdapter);
}
{ // create numbers adapter
List<Number> items = /* Numberのリストを準備する */;
NumberAdapter numbersAdapter = new NumberAdapter(this, items, mListenerRelay);
mMergeRecyclerAdapter.addAdapter(numbersAdapter);
}
mRecyclerView.setAdapter(mergeRecyclerAdapter);
}
それぞれのアダプターを追加していくだけで煩わしいインデックスの問題を解決してくれます。
一つ課題として、クリックなどのイベントをハンドリングするときにn番目の項目が何であるかを判定する必要がでてきます。これについては対応策が用意されており、MergeRecyclerAdapter#getAdapterOffsetForItem(int position)というメソッドを使うと解決されます。
public LocalAdapter getAdapterOffsetForItem(final int position) {
/* 省略 */
}
戻り値のLocalAdapterは次のようなメンバ変数を持っています。
public class LocalAdapter extends RecyclerView.AdapterDataObserver {
public final T mAdapter;
public int mLocalPosition = 0;
/* 省略 */
}
これらを使うと次のようなView.OnClickListenerを作ることで、クリック時のイベントをハンドリングすることができます。
public void onClick(View view) {
ViewHolder holder = (ViewHolder) view.getTag(); // 予めsetTagしておくこと
LocalAdapter la = mMergeRecyclerAdapter.getAdapterOffsetForItem(holder.getAdapterPosition());
if (la.mAdapter instanceof StringAdapter) {
String data = ((StringAdapter)la.mAdapter).getItemAt(la.mLocalPosition);
/* Stringの場合の処理をする */
} else if (la.mAdapter instanceof NumberAdapter) {
Number data = ((NumberAdapter)la.mAdapter).getItemAt(la.mLocalPosition);
/* Numberの場合の処理をする */
}
}
まとめ
RecyclerView.Adapterは最低限の機能しか提供してくれないので、セクション分けをしようとするとインデックスの取り扱いがかなり大変です。そしてインデックスの管理を少しでも間違えるとズレたりArrayIndexOutOfBoundsExceptionでクラッシュしたりでかなり危険です。その手の危険なことはMergeRecyclerAdapterのような特定のクラスに管理の責務を押し付けて対応しましょう。