LoginSignup
78
70

More than 5 years have passed since last update.

RecyclerViewでセクション分けをする

Posted at

お題

RecyclerViewでセクション分けをしたい。ここではStringのリストとNumberのリストを、セクション分けして表示するRecyclerViewを例に説明します。

image

愚直にやる例

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のような特定のクラスに管理の責務を押し付けて対応しましょう。

実際に動くサンプルプログラムはこちら

78
70
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
78
70