RecyclerView でリストを作成する

RecyclerView でリストを作成する   Android Jetpack の一部

大規模なデータセット(または頻繁に変更されるデータ)に基づく要素のスクロール リストをアプリで表示する必要がある場合、このページで説明するように RecyclerView を使用する必要があります。

ヒント: まず、Android Studio で [File] > [New] > [Fragment] > [Fragment (List)] をクリックして、テンプレート コードを生成します。次に、フラグメントをアクティビティ レイアウトに追加します。

図 1. RecyclerView を使用したリスト

図 2. CardView を使用したリスト

カードを含むリストを作成する場合は、図 2 に示すように、CardView ウィジェットも使用します(カードベースのレイアウトを作成するを参照)。

RecyclerView のサンプルコードについては、RecyclerView のサンプルアプリ(Java または Kotlin)をご覧ください。

RecyclerView の概要

RecyclerView ウィジェットは ListView を拡張して柔軟性をもたせた改良版です。

RecyclerView モデルでは、複数のコンポーネントが連携してデータを表示します。ユーザー インターフェースのコンテナ全体が RecyclerView オブジェクトで、このオブジェクトをレイアウトに追加します。RecyclerView 自体の表示には、指定したレイアウト マネージャーから提供されるビューを使用します。レイアウト マネージャーは、標準のもの(LinearLayoutManagerGridLayoutManager など)を使用することも、独自のものを実装することも可能です。

リスト内のビューは、ビューホルダー オブジェクトで表されます。これらのオブジェクトは、RecyclerView.ViewHolder を拡張することによって定義するクラスのインスタンスです。各ビューホルダーは、ビューを含むアイテムを 1 つ表示します。たとえば、リストに音楽コレクションを表示する場合、各ビューホルダーが 1 枚のアルバムを表すことができます。RecyclerView は、動的コンテンツの画面上の部分を表示するのに必要な数のビューホルダーと、追加のいくつかのビューホルダーのみを作成します。ユーザーがリストをスクロールすると、RecyclerView が画面外のビューを取得して、画面上にこれからスクロールされるデータにバインドし直します。

ビューホルダー オブジェクトはアダプターによって管理されます(アダプターは RecyclerView.Adapter を拡張して作成します)。アダプターはビューホルダーを必要に応じて作成します。また、ビューホルダーのデータへのバインドも行います。バインドは、ビューホルダーをある位置に割り当てて、アダプターの onBindViewHolder() メソッドを呼び出すことによって行います。このメソッドはビューホルダーの位置を利用し、リストの位置に基づいて表示する内容を決定します。

この RecyclerView モデルはさまざまな最適化処理を行います。そのため、デベロッパーが以下のことを行う必要はありません。

  • リストを初めて作成するときに、リストの両側にビューホルダーをいくつか作成してバインドします。たとえば、ビューがリストの位置 0~9 を表示している場合、RecyclerView はそれらのビューホルダーを作成してバインドします。また、位置 10 のビューホルダーを作成してバインドすることもあります。こうすることで、ユーザーがリストをスクロールしたときに次に表示する要素を準備できます。
  • ユーザーがリストをスクロールすると、RecyclerView が必要に応じて新しいビューホルダーを作成します。画面外にスクロールされたビューホルダーも保存されるため、それらを再利用できます。ユーザーがスクロールの方向を切り替えても、画面外にスクロールされたビューホルダーをすぐに呼び戻すことができます。一方、ユーザーが同じ方向にスクロールし続けると、最も長い間画面外にあったビューホルダーを新しいデータにバインドし直すことができます。ビューホルダーを作成したり、そのビューをインフレートしたりする必要はありません。代わりにアプリが、バインドされた新しいアイテムと一致するようにビューのコンテンツを更新します。
  • 表示されているアイテムが変更された場合、適切な RecyclerView.Adapter.notify…() メソッドを呼び出すことによってアダプターに通知できます。その後、アダプターの組み込みコードが、影響を受けるアイテムだけをバインドし直します。

サポート ライブラリを追加する

RecyclerView ウィジェットにアクセスするには、次のように v7 サポート ライブラリをプロジェクトに追加する必要があります。

  1. アプリ モジュールの build.gradle ファイルを開きます。
  2. サポート ライブラリを dependencies セクションに追加します。
        dependencies {
            implementation 'com.android.support:recyclerview-v7:28.0.0'
        }
        

RecyclerView をレイアウトに追加する

これで、RecyclerView をレイアウト ファイルに追加できるようになりました。たとえば次のレイアウトでは、レイアウト全体の唯一のビューとして RecyclerView を使用しています。

    <?xml version="1.0" encoding="utf-8"?>
    <!-- A RecyclerView with some commonly used attributes -->
    <android.support.v7.widget.RecyclerView
        android:id="@+id/my_recycler_view"
        android:scrollbars="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    

RecyclerView ウィジェットをレイアウトに追加したら、オブジェクトへのハンドルを取得してレイアウト マネージャーに関連付け、表示するデータのアダプターをアタッチします。

Kotlin

    class MyActivity : Activity() {
        private lateinit var recyclerView: RecyclerView
        private lateinit var viewAdapter: RecyclerView.Adapter<*>
        private lateinit var viewManager: RecyclerView.LayoutManager

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.my_activity)

            viewManager = LinearLayoutManager(this)
            viewAdapter = MyAdapter(myDataset)

            recyclerView = findViewById<RecyclerView>(R.id.my_recycler_view).apply {
                // use this setting to improve performance if you know that changes
                // in content do not change the layout size of the RecyclerView
                setHasFixedSize(true)

                // use a linear layout manager
                layoutManager = viewManager

                // specify an viewAdapter (see also next example)
                adapter = viewAdapter

            }
        }
        // ...
    }
    

Java

    public class MyActivity extends Activity {
        private RecyclerView recyclerView;
        private RecyclerView.Adapter mAdapter;
        private RecyclerView.LayoutManager layoutManager;

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.my_activity);
            recyclerView = (RecyclerView) findViewById(R.id.my_recycler_view);

            // use this setting to improve performance if you know that changes
            // in content do not change the layout size of the RecyclerView
            recyclerView.setHasFixedSize(true);

            // use a linear layout manager
            layoutManager = new LinearLayoutManager(this);
            recyclerView.setLayoutManager(layoutManager);

            // specify an adapter (see also next example)
            mAdapter = new MyAdapter(myDataset);
            recyclerView.setAdapter(mAdapter);
        }
        // ...
    }
    

リスト アダプターを追加する

すべてのデータをリストにフィードするには、RecyclerView.Adapter クラスを拡張する必要があります。このオブジェクトはアイテムのビューを作成し、元のアイテムが表示できなくなると、一部のビューのコンテンツを新しいデータアイテムで置き換えます。

次のコード例は、TextView ウィジェットを使用して表示される文字列配列から成るデータセットのシンプルな実装を示しています。

Kotlin

    class MyAdapter(private val myDataset: Array<String>) :
            RecyclerView.Adapter<MyAdapter.MyViewHolder>() {

        // Provide a reference to the views for each data item
        // Complex data items may need more than one view per item, and
        // you provide access to all the views for a data item in a view holder.
        // Each data item is just a string in this case that is shown in a TextView.
        class MyViewHolder(val textView: TextView) : RecyclerView.ViewHolder(textView)

        // Create new views (invoked by the layout manager)
        override fun onCreateViewHolder(parent: ViewGroup,
                                        viewType: Int): MyAdapter.MyViewHolder {
            // create a new view
            val textView = LayoutInflater.from(parent.context)
                    .inflate(R.layout.my_text_view, parent, false) as TextView
            // set the view's size, margins, paddings and layout parameters
            ...
            return MyViewHolder(textView)
        }

        // Replace the contents of a view (invoked by the layout manager)
        override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
            // - get element from your dataset at this position
            // - replace the contents of the view with that element
            holder.textView.text = myDataset[position]
        }

        // Return the size of your dataset (invoked by the layout manager)
        override fun getItemCount() = myDataset.size
    }
    

Java

    public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
        private String[] mDataset;

        // Provide a reference to the views for each data item
        // Complex data items may need more than one view per item, and
        // you provide access to all the views for a data item in a view holder
        public static class MyViewHolder extends RecyclerView.ViewHolder {
            // each data item is just a string in this case
            public TextView textView;
            public MyViewHolder(TextView v) {
                super(v);
                textView = v;
            }
        }

        // Provide a suitable constructor (depends on the kind of dataset)
        public MyAdapter(String[] myDataset) {
            mDataset = myDataset;
        }

        // Create new views (invoked by the layout manager)
        @Override
        public MyAdapter.MyViewHolder onCreateViewHolder(ViewGroup parent,
                                                       int viewType) {
            // create a new view
            TextView v = (TextView) LayoutInflater.from(parent.getContext())
                    .inflate(R.layout.my_text_view, parent, false);
            ...
            MyViewHolder vh = new MyViewHolder(v);
            return vh;
        }

        // Replace the contents of a view (invoked by the layout manager)
        @Override
        public void onBindViewHolder(MyViewHolder holder, int position) {
            // - get element from your dataset at this position
            // - replace the contents of the view with that element
            holder.textView.setText(mDataset[position]);

        }

        // Return the size of your dataset (invoked by the layout manager)
        @Override
        public int getItemCount() {
            return mDataset.length;
        }
    }
    

レイアウト マネージャーはアダプターの onCreateViewHolder() メソッドを呼び出します。このメソッドでは、RecyclerView.ViewHolder を作成して、そのコンテンツを表示するために使用するビューを設定する必要があります。ViewHolder の型は、Adapter クラスのシグネチャで宣言した型と一致する必要があります。通常は、XML レイアウト ファイルをインフレートすることによってビューを設定します。ビューホルダーはまだ特定のデータに割り当てられていないため、このメソッドは実際には、ビューのコンテンツを設定しません。

次に、レイアウト マネージャーがビューホルダーをそのデータにバインドします。これは、アダプターの onBindViewHolder() メソッドを呼び出して、RecyclerView 内のビューホルダーの位置を渡すことによって行います。onBindViewHolder() メソッドでは、適切なデータを取得し、そのデータを使用してビューホルダーのレイアウトを埋める必要があります。たとえば、RecyclerView で名前のリストを表示する場合、このメソッドはリスト内で適切な名前を見つけて、ビューホルダーの TextView ウィジェットに入力することができます。

リストを更新する必要がある場合は、RecyclerView.Adapter オブジェクトで通知メソッド(notifyItemChanged() など)を呼び出します。次に、レイアウト マネージャーが影響を受けるビューホルダーをすべてバインドし直して、データを更新できるようにします。

ヒント: ListAdapter クラスは、リストの変更時に更新する必要があるアイテムを特定するのに役立ちます。

RecyclerView をカスタマイズする

特定のニーズに合わせて RecyclerView オブジェクトをカスタマイズすることができます。標準のクラスには、ほとんどのデベロッパーが必要とする機能がすべて用意されています。そのため、多くの場合に必要なカスタマイズは、各ビューホルダーのビューを設計し、それらのビューを適切なデータで更新するコードを記述することだけになります。ただし、アプリに特定の要件がある場合は、さまざまな方法で標準の動作を変更できます。次のセクションで、その他の一般的なカスタマイズについて説明します。

レイアウトの変更

RecyclerView は、レイアウト マネージャーを使用して個々のアイテムを画面上に配置します。また、ユーザーに表示されなくなったアイテムビューを再利用するタイミングを判断します。ビューを再利用(リサイクル)するために、レイアウト マネージャーがアダプターに対し、ビューのコンテンツをデータセットの別の要素で置き換えるよう求めることがあります。この方法でビューをリサイクルすると、不要なビューが作成されず、負荷の高い findViewById() による検索も行われないため、パフォーマンスが向上します。Android サポート ライブラリには 3 つの標準レイアウト マネージャーが含まれており、それぞれに各種のカスタマイズ オプションが用意されています。

  • LinearLayoutManager は 1 次元のリスト内にアイテムを配置します。RecyclerViewLinearLayoutManager とともに使用すると、以前の ListView レイアウトと同じような機能が得られます。
  • GridLayoutManager は 2 次元のグリッド(チェスのボード上の正方形に似ています)内にアイテムを配置します。RecyclerViewGridLayoutManager とともに使用すると、以前の GridView レイアウトと同じような機能が得られます。
  • StaggeredGridLayoutManager は、各列がその前の列と少しずれている 2 次元のグリッド(アメリカ国旗の星に似ています)内にアイテムを配置します。

これらのレイアウト マネージャーの中にニーズを満たすものがない場合は、RecyclerView.LayoutManager 抽象クラスを拡張することによって独自のレイアウト マネージャーを作成できます。

アイテム アニメーションを追加する

RecyclerView はアイテムが変更されるたびに、アニメーターを使用してその外観を変更します。このアニメーターは、RecyclerView.ItemAnimator 抽象クラスを拡張するオブジェクトです。デフォルトでは、RecyclerViewDefaultItemAnimator を使用してアニメーションを表示します。カスタムのアニメーションを表示したい場合は、RecyclerView.ItemAnimator を拡張することによって独自のアニメーター オブジェクトを定義できます。

リストアイテムを選択できるようにする

recyclerview-selection ライブラリを使用すると、ユーザーがタップまたはマウス入力で RecyclerView リスト内のアイテムを選択できるようになります。選択されたアイテムの視覚的な表示に対するコントロールは維持されます。また、選択の動作を管理するポリシー(選択の対象になり得るアイテム、選択可能なアイテムの数など)に対するコントロールも維持できます。

RecyclerView インスタンスに選択サポートを追加する手順は次のとおりです。

  1. 使用する選択キーのタイプを決定し、ItemKeyProvider を作成します。

    選択されたアイテムの特定に使用できるキーのタイプには、Parcelable(および Uri などのすべてのサブクラス)、StringLong の 3 つがあります。選択キーのタイプについて詳しくは、SelectionTracker.Builder をご覧ください。

  2. ItemDetailsLookup を実装します。
  3. ItemDetailsLookup を実装すると、MotionEvent が発生した場合に、RecyclerView アイテムに関する情報に選択ライブラリからアクセスできるようになります。これは実質的に、RecyclerView.ViewHolder インスタンスによってバックアップされる(またはこのインスタンスから抽出される)、ItemDetails インスタンスのファクトリです。

  4. RecyclerView のアイテムの Views を更新し、ユーザーがそのビューを選択または選択解除したことを反映します。

    選択ライブラリには、選択されたアイテムに対するデフォルトの視覚的装飾は用意されていません。onBindViewHolder() を実装する場合は、視覚的装飾を用意する必要があります。おすすめの方法は次のとおりです。

  5. ActionMode を使用して、選択されたアイテムに対するアクションを実行するツールをユーザーに提供します。
  6. 選択が変更されたときに通知を受け取るには、SelectionTracker.SelectionObserver を登録します。アイテムが初めて選択されたときに、ActionMode を開始してユーザーにそのことを示し、選択に固有のアクションを提供します。たとえば、削除ボタンを ActionMode バーに追加したり、そのバー上の戻る矢印を選択解除に関連付けたりすることができます。何も選択されていない場合(ユーザーが選択を解除した場合)、忘れずに ActionMode を終了してください。

  7. 解釈されたセカンダリ アクションを実行します。
  8. イベント処理パイプラインの最後に、ライブラリが、ユーザーがアイテムをタップして有効にしようとしている、あるいは、ユーザーが選択したアイテムをドラッグ&ドロップしようとしていると判断することがあります。こうした解釈に対応するには、適切なリスナーを登録します。詳しくは、SelectionTracker.Builder をご覧ください。

  9. SelectionTracker.Builder を使用してすべての要素を組み立てます。
  10. 次の例は、Long 選択キーを使用して各要素を組み立てる方法を示しています。

    Kotlin

        var tracker = SelectionTracker.Builder(
            "my-selection-id",
            recyclerView,
            StableIdKeyProvider(recyclerView),
            MyDetailsLookup(recyclerView),
            StorageStrategy.createLongStorage())
                .withOnItemActivatedListener(myItemActivatedListener)
                .build()
        

    Java

        SelectionTracker tracker = new SelectionTracker.Builder<>(
                "my-selection-id",
                recyclerView,
                new StableIdKeyProvider(recyclerView),
                new MyDetailsLookup(recyclerView),
                StorageStrategy.createLongStorage())
                .withOnItemActivatedListener(myItemActivatedListener)
                .build();
        

    SelectionTracker インスタンスを作成するには、アプリで RecyclerView を初期化するために使用したのと同じ RecyclerView.AdapterSelectionTracker.Builder に指定する必要があります。そのためほとんどの場合、RecyclerView.Adapter の作成後に、SelectionTracker インスタンスが作成され次第、それを RecyclerView.Adapter に追加する必要があります。そうしないと、アイテムの選択状態を onBindViewHolder() メソッドで確認できなくなります。

  11. 選択をアクティビティのライフサイクル イベントに追加します。
  12. 選択状態をアクティビティのライフサイクル イベント間で保持するには、アプリで選択トラッカーの onSaveInstanceState() メソッドと onRestoreInstanceState() メソッドをアクティビティの onSaveInstanceState() メソッドと onRestoreInstanceState() メソッドからそれぞれ呼び出す必要があります。アプリでは、一意の選択 ID を SelectionTracker.Builder コンストラクタに提供する必要もあります。この ID は必須です。なぜなら、アクティビティやフラグメントに選択可能なリストが複数ある場合は、それらすべてを保存された状態に維持する必要があるためです。

    参考情報

    RecyclerViewSunflower デモアプリで使用されています。