RecyclerView:アイテムの選択(recyclerview-selection API)

投稿日:  更新日:

RecyclerView上のアイテムを選択する方法を紹介します。

外部ライブラリー(AndroidX)で提供されるrecyclerview-selection APIを用いた方法です。

スポンサーリンク

recyclerview-selection APIを用いたアイテムの選択

recyclerview-selection APIを用いたアイテムの選択は次のような動作です。

recyclerview-selectionの動作

長押し後にマルチセレクションモードに入り、そのままスワイプを行えば選択の範囲を広げることができます。

選択されたアイテムがある間はマルチセレクションモードを保持します。

マルチセレクションモード中はアイテムのクリックで選択の有無がトグルします。

スポンサーリンク

環境設定

RecyclerViewはAndroid APIではなく外部ライブラリー(AndroidX)で提供されます。

recyclerview-selection APIも同様です。

recyclerview-selectionを使うためにライブラリの依存リストへ次の一行が必要です。

dependencies {
    ....
    implementation 'androidx.recyclerview:recyclerview:1.1.0'
    implementation 'androidx.recyclerview:recyclerview-selection:1.1.0'
	...
}
スポンサーリンク

アイテムの選択の構成

SelectionTrackerで操作を追跡

recyclerview-selection APIを用いたアイテムの選択は、「RecyclerViewの内部へ選択する仕組みを組み込む」というよりも、「RecyclerViewの外部から操作(タッチスクリーンならば指の操作)を追跡し、その結果から選択を判定する」という仕組みです。

SelectionTrackerが操作を追跡するクラスです。内部でアイテムの選択を判定し、その状態を管理しています。

外部から操作を追跡するSelectionTracker

        ...
		
        val _rcySample = findViewById<RecyclerView>(R.id.rcySample)
        _rcySample.adapter = _adapter
        _rcySample.layoutManager = _manager

        tracker = SelectionTracker.Builder(    // 依存関係はRecyclerViewのDIのみ
                "ListTracker",
                _rcySample,
                StableIdKeyProvider(_rcySample),
                SampleItemDetailsLookup(_rcySample),
                StorageStrategy.createLongStorage()
        ).build()
		
		...

アイテムを選択する機能はSelectionTrackerのインスタンスを作成するだけで実現できます。

RecyclerViewとSelectionTrackerの依存関係はRecyclerViewをDI(Dependency Injection)するのみです。両者の関係は希薄で、ほぼ独立していると言えます。

故に、RecyclerViewの変更の影響をSelectionTrackerはあまり受けずに済みます。

SelectionTrackerの構成

SelectionTrackerの構成は図のようになっています。

SelectionTrackerの構成

SelectionTrackerのインスタンスはBuilderを使って作成され、その時にコンストラクタから3つのクラスを取り込みます。@NonNullのためNullによる省略は許されません。

  • ItemKeyProvider
  • ItemDetailsLookup
  • StorageStrategy
… アイテムの選択を判定
… 変換(Position ⇔ Key)
… 保存データの構築
public abstract class SelectionTracker<K> {

	...
    public static final class Builder<K> {
	    ...
        public Builder(
                @NonNull String selectionId,		// Trackerを識別するID
                @NonNull RecyclerView recyclerView, // RecyclerViewのインスタンス
                @NonNull ItemKeyProvider<K> keyProvider,
                @NonNull ItemDetailsLookup<K> detailsLookup,
                @NonNull StorageStrategy<K> storage) {
            ...
        }
		...
	}
}

選択の状態はKeyで管理

Keyとは

選択の状態はKeyで管理され、選択リスト(Selection)に保持されています。

Keyとは各々のアイテムを識別するために使われる値です。

アイテムを選択する機能を実装するにはKeyが必要で、「どの値をKeyにするか?」はとても重要です。

Keyの条件

Keyは次の条件を満たしていなければなりません。

  • アイテム毎にユニーク
  • 値が不変(アプリの動作中)
  • データタイプがLong、String、またはParcelableを実装したクラス

例えば、アイテムのポジション(データの並び順)をKeyにすることを考えてみます。

ポジションをKeyにするのはNG

アイテムの挿入があると、挿入された位置よりも後方のアイテムは、ポジションが変わってしまいます。値はユニークですが不変でないため、ポジションはKeyに出来ません。

選択の有無を確認

選択リスト(Selection)から選択の有無を確認したい場合はKeyを使って次のように行います。

        tracker = SelectionTracker.Builder(
                "ListTracker",
                _rcySample,
                ...
        ).build()
		
		...
		val state = tracker.isSelected(KEY)		// KEY:確認したいアイテムのKey
		                    // true:選択あり、false:選択なし

Keyのデータタイプによる違い

Keyに許されるデータタイプは「Long」、「String」、「Parcelableを実装したクラス」の3つです。

既成のクラスを使う

どのデータタイプを使用するかで、ItemKeyProvider/ItemDetailsLookup/StorageStrategyの実装が変わってきます。

データタイプによって既成のクラスがAPI内に準備されています。

Keyのデータタイプ抽象クラス(*1)
ItemKeyProvider
抽象クラス(*2)
ItemDetailsLookup
StorageStrategy(*3)
Long
※Stable ID使用
StableIdKeyProvider実装が必要LongStorageStrategy
Long実装が必要同上同上
String同上同上StringStorageStrategy
Parcelableを
実装したクラス
同上同上ParcelableStorageStrategy
青字:既成クラス(APIに入っている)
*1:抽象メソッド
    public abstract @Nullable K getKey(int position);
    public abstract int getPosition(@NonNull K key);
*2:抽象メソッド
    public abstract @Nullable ItemDetails getItemDetails(@NonNull MotionEvent e);
*3:ファクトリメソッドがある
    StorageStrategy#createLongStorage( )
    StorageStrategy#createStringStorage( )
    StorageStrategy#createParcelableStorage(Class<K>type)

Long型はStable IDが利用可能

KeyをLong型にする場合はRecyclerViewが持つStable IDという機能が使えます。

Stable IDとはアイテム毎に付けるID(Long型)のことです。RecycylerViewがアイテムの管理で使います。

Stable IDとKeyは満たすべき条件が同じなので、Stable IDをそのままKeyとして扱えます。

ただし、Stable IDはオプションの機能なので、有効化(利用の宣言、Adapter#getItemIdメソッドの実装)が必要になります。

class ListAdapter(var items: MutableList<String>)
    : RecyclerView.Adapter<ListAdapter.ListViewHolder>() {

    init {
        setHasStableIds(true)            // Stable ID利用の宣言
    }

    ...
	
    override fun getItemCount(): Int {
        return items.size
    }

    override fun getItemId(position: Int): Long {
        return items[position].hashCode().toLong() // IDを返す
    }   // ↑ ハッシュ値は完全なユニークにならないので注意!
	
	...
}

サンプルは「エレメントがアイテム毎にユニークな固定値である」という前提で、エレメントのハッシュ値をStable IDにしています。

スポンサーリンク

アイテムの選択の実装例

SelectionTrackerの作成(Stable IDを使ったKey)

Stable ID(Long型)を使ったKeyの場合です。Stable IDは有効化済みとします。

ItemKeyProviderとStorageStrategyは既成のクラスが存在しています。実装が必要なのはItemDetailsLookupです。

        val _rcySample = findViewById<RecyclerView>(R.id.rcySample)

        tracker = SelectionTracker.Builder(
                "ListTracker",
                _rcySample,
                StableIdKeyProvider(_rcySample),
                SampleItemDetailsLookup(_rcySample),
                StorageStrategy.createLongStorage()
        ).build()
class SampleItemDetailsLookup(private val recyclerView: RecyclerView)
    : ItemDetailsLookup<Long>() {

    override fun getItemDetails(e: MotionEvent): ItemDetails<Long>? {
        val _view = recyclerView.findChildViewUnder(e.x, e.y)
        return _view?.let {
            val _holder = recyclerView.getChildViewHolder(it)
            object : ItemDetailsLookup.ItemDetails<Long>() {
                override fun getPosition(): Int = _holder.adapterPosition
                override fun getSelectionKey(): Long = _holder.itemId
            }   // ↑ Stable IDはLong型
        }
    }
}

SelectionTrackerの作成(Long/String/Parcelable*型のKey)

代表してString型を使ったKeyの場合です。エレメントの1つをKeyにしました。

KeyはアイテムのViewのTagに格納しています。

class ListAdapter(var items: MutableList<String>)
    : RecyclerView.Adapter<ListAdapter.ListViewHolder>() {

    ...

    override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
        holder.itemView.tag = items[position] // エレメントをKeyにする、Tagへ格納
        holder.mesg.text = items[position]    // 要素とデータを紐づける
    }
	
	...
}

StorageStrategyは既成のクラスが存在しています。実装が必要なのはItemDetailsLookupとItemKeyProviderです。

        val _rcySample = findViewById<RecyclerView>(R.id.rcySample)

        tracker = SelectionTracker.Builder(
                "ListTracker",
                _rcySample,
                StringKeyProvider(_rcySample),
                SampleItemDetailsLookup(_rcySample),
                StorageStrategy.createStringStorage()
        ).build()
class SampleItemDetailsLookup(private val recyclerView: RecyclerView)
    : ItemDetailsLookup<String>() {

    override fun getItemDetails(e: MotionEvent): ItemDetails<String>? {
        val _view = recyclerView.findChildViewUnder(e.x, e.y)
        return _view?.let {
            val _holder = recyclerView.getChildViewHolder(it)
            object : ItemDetailsLookup.ItemDetails<String>() {
                override fun getPosition(): Int = _holder.adapterPosition
                override fun getSelectionKey(): String? = _holder.itemView.tag as String
            }   // ↑ TagはObject型(Any型)なのでString型へキャスト
        }
    }
}
class StringKeyProvider(private val recyclerView: RecyclerView)
    : ItemKeyProvider<String>(SCOPE_CACHED) {

    // ---- ここから ---- StableIdKeyProviderから流用(posAndKeyの部分のみ変更) ----
    private val posAndKey = SparseArray<String>()  // key->pos value->key

    init {
        recyclerView.addOnChildAttachStateChangeListener(
                object : RecyclerView.OnChildAttachStateChangeListener {
                    override fun onChildViewAttachedToWindow(view: View) {
                        onAttached(view)
                    }
                    override fun onChildViewDetachedFromWindow(view: View) {
                        onDetached(view)
                    }
                }
        )
    }
    fun onAttached(view: View) {
        val _holder = recyclerView.findContainingViewHolder(view)
        _holder?.let {
            val _pos = it.adapterPosition
            val _key = it.itemView.tag as String
            if(_pos != RecyclerView.NO_POSITION && _key != null) {
                posAndKey.put(_pos, _key)
            }
        }
    }
    fun onDetached(view: View) {
        val _holder = recyclerView.findContainingViewHolder(view)
        _holder?.let {
            val _pos = it.adapterPosition
            val _key = it.itemView.tag as String
            if(_pos != RecyclerView.NO_POSITION && _key != null) {
                posAndKey.delete(_pos)
            }
        }
    }
    //  ---- ここまで ---- StableIdKeyProviderから流用(posAndKeyの部分のみ変更) ----

    override fun getKey(position: Int): String? { // Position ⇒ Key 変換
        return posAndKey.get(position, null)
    }

    override fun getPosition(key: String): Int {  // Key ⇒ Position 変換
        return posAndKey.indexOfValue(key)  // Valueが存在しない時、
                                            // -1(RecyclerView.NO_POSITIONと同じ)が変える
    }
}

positionToKeyとkeyToPositionを作成する部分はStableIdKeyProviderからの流用です。

両者はPositionとKeyの関係をキャッシュしています。

キャッシュの中からPositionまたはKeyを見つけ出すことで、検索する範囲を絞り込む働きがあると思われます。

スポンサーリンク

アイテムの選択の状態を保存・復元(必要であれば)

端末の回転を行うとActivityが再起動されます。

再起動時にSelectionTrackerのインスタンスが再作成され、アイテムの選択の状態はクリアされてしまいます。

これを回避するために、Activityの終了で状態を保存し、起動で復元する方法がライフサイクルの中に用意されています。

    lateinit var tracker: SelectionTracker<Long>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
		
        val _rcySample = findViewById<RecyclerView>(R.id.rcySample)
        _rcySample.adapter = _adapter
        _rcySample.layoutManager = _manager

        tracker = SelectionTracker.Builder(
                "ListTracker",
                _rcySample,
                ...
        ).build()

        savedInstanceState?.let {                        // 復元
            tracker.onRestoreInstanceState(it)
        }
		
        ...
    }

    override fun onSaveInstanceState(outState: Bundle) { // 保存
        super.onSaveInstanceState(outState)
        tracker.onSaveInstanceState(outState)
    }
スポンサーリンク

アイテムの選択の状態を表示へ反映(必要であれば)

SelectionTrackerはアイテムの選択の状態を管理・保持するだけです。

アイテムが選択されたことを知らせるため、表示を変えるような事はしません。

上記のことを行いたければ、プログラム中にその動作を組み込まなければなりません。

幸いなことに、アイテムが選択されるとAdapter#onBindViewHolderが呼ばれるので、ここで表示を変えることが可能です。

次のサンプルは、Viewのstate_activatedを選択の有無で変化させ、StateListDrawableを使ってバックグラウンドを変化させる例です。

StateListDrawableの詳細はリンク先を参照してください。

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@color/Activated" android:state_activated="true" />
    <item android:drawable="@color/Unactivated" android:state_activated="false" />
</selector>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="38dp"
    android:layout_margin="1dp"
    android:background="@drawable/selector_item">

    <TextView
        android:id="@+id/txtMesg"
        ... />

</androidx.constraintlayout.widget.ConstraintLayout>
class ListAdapter(var items: MutableList<String>)
    : RecyclerView.Adapter<ListAdapter.ListViewHolder>() {

    var tracker: SelectionTracker<Long>? = null

    ...
    override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
        holder.mesg.text = items[position]  // 要素とデータを紐づける
        tracker?.let {   // 選択の有無でstate_activatedを変える
            holder.view.isActivated = it.isSelected(getItemId(position))
        }
    }
    ...
}
    lateinit var tracker: SelectionTracker<Long>

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

        ...
        val _adapter = ListAdapter(_items)
        val _manager = LinearLayoutManager(this)

        val _rcySample = findViewById<RecyclerView>(R.id.rcySample)
        _rcySample.adapter = _adapter
        _rcySample.layoutManager = _manager

        tracker = SelectionTracker.Builder(
                "ListTracker",
                _rcySample,
                StableIdKeyProvider(_rcySample),
                SampleItemDetailsLookup(_rcySample),
                StorageStrategy.createLongStorage()
        ).build()

        _adapter.tracker = tracker // Adapterへ注入、state_activatedの変更を有効化

        ...
    }
スポンサーリンク

関連記事:

RecyclerViewは子Viewを並べて表示するコンテナタイプ(ConstraintLayoutと同じ)のViewです。 複数のデータをスクリーン上に一覧表示したい時、例えば電話帳のような「氏名+住所+電話番号」の一覧を表示する場合などに最適です。 アプリを開発していると一覧表示したいデータが多いことに気付きます。 なのでRecyclerViewはとても重要で重宝するViewです。 しかし、思い通りの表示を行わせるためのテクニックが多すぎて、使いこなしが難しいです。 今まで調べたテクニックを忘れないように、整理して書き残そうと思います。 今回は基本の「RecyclerViewの実装」です。 ...
RecyclerViewでアイテムのクリックイベントを取得し、処理を実行する方法を紹介します。 ...
RecyclerViewはアイテムのレイアウトをアイテム毎に変更できます。その時に使う値がViewTypeです。 ViewTypeでアイテムのレイアウトを変更する方法を紹介します。 ...
RecyclerViewは表示が変更される(アイテムの更新、スクロール)時、アイテムのViewをリサイクル(再生利用)します。 これにより余分なViewの作成が行われなくなり、メモリーの節約とパフォーマンスの向上が望めます。 リサイクルはCachedViewsとRecyclerPoolという2つのキャッシュで行われます。 このキャッシュを使ったリサイクルの動作を調べたので紹介します。 ...
RecyclerViewのリサイクル動作で使われるキャッシュは、サイズを大きくすれば多くのViewHolderが保持できます。その分、多くのメモリを消費します。 ViewHolderを多く保持できたとしても、サイクル動作で効率よく使われなければ、メモリの浪費です。 キャッシュのサイズはRecyclerViewの使われ方よって適切なサイズがあります。 そのため、RecyclerViewはキャッシュのサイズを変更できるようになっています。 ...
RecyclerViewはアイテムを一覧表示してくれます。 ただ一覧表示するだけではなく、「追加・削除・移動・切り替え」といったアイテムの表示を効率よく変更する仕組み持っています。 今回はこの仕組みを使ったアイテムの変更方法を紹介します。 ...
RecyclerViewのリサイクル動作で使われるキャッシュは、サイズを大きくすれば多くのViewHolderが保持できます。その分、多くのメモリを消費します。 ViewHolderを多く保持できたとしても、サイクル動作で効率よく使われなければ、メモリの浪費です。 キャッシュのサイズはRecyclerViewの使われ方よって適切なサイズがあります。 そのため、RecyclerViewはキャッシュのサイズを変更できるようになっています。 ...
RecyclerViewが空(アイテムが無い)の時、EmptyViewを表示する実装を行ったので紹介します。 ...
RecyclerViewはListView(RecyclerViewの前身)の時に存在していたChoiceModeがありません。 同様な機能が欲しければプログラマ側で実装しなければなりません。 RecyclerView.AdapterをカスタマイズしてChoiceModeを実装してみたので紹介します。 ...
RecyclerViewはアイテムへ装飾を付けることが出来るようになっています。 装飾とは、例えばアイテムの区切り線などです。 今回はアイテムへ装飾を付ける方法を紹介します。 ...
RecyclerViewに表示しきれなかったアイテムはスクロールを行うことで表示されるようになっています。 スクロールは「外部入力(指でスクリーン上をタッチしてスライド)によるスクロール」の他に、「プログラムによるスクロール」をすることも出来ます。 今回はこのアイテムのスクロールについてまとめてみました。 ...
RecyclerViewはアイテムをスクロールさせて隠れたアイテムを表示できます。 スクロールを止める位置は任意です。 任意であるがゆえに、止めた位置によってはアイテムの一部が欠けてしまうこともあります。 携帯端末の画面は狭いので、効率よくコンテンツの表示を行いたいとアプリ開発者は考えます。 アイテムの一部が欠けてしまうことは、効率が良いとは言えません。 このような問題を解決するために、アイテムのスナップをRecyclerViewへ追加できます。 アイテムのスナップを追加する方法を紹介します。 ...
RecyclerViewでアイテムの変更(Change/Insert/Move/Remove)を行うと、変更される様子がアニメーション化されています。 これはデフォルトでアイテムの変更アニメーションが組み込まれているためです。 デフォルトは単純なアニメーションですが、「ある」と「ない」の違いは歴然で、アニメーションのある方が高価なアプリケーションに見えます。 GUI(Graphical User Interface)が主体の携帯端末にとって、利用者に対するアプリの見せ方は重要です。高価に見えた方が使ってもらえる可能性が高くなります。 上記のことから、アプリの機能に関係なくても、ちょっとした動きをアニメーション化するメリットがあります。 2回にわたりアイテムの変更アニメーションについてまとめてみました。 アイテムの変更アニメーション(DefaultItemAnimator、デフォルト) アイテムの変更アニメーション(SimpleItemAnimatorの継承、カスタム) 今回は第1回目「ItemAnimator、デフォルト」編です。デフォルト変更アニメーションの動作について説明します。 ...
RecyclerViewでアイテムの変更(Change/Insert/Move/Remove)を行うと、変更される様子がアニメーション化されています。 これはデフォルトでアイテムの変更アニメーションが組み込まれているためです。 デフォルトは単純なアニメーションですが、「ある」と「ない」の違いは歴然で、アニメーションのある方が高価なアプリケーションに見えます。 GUI(Graphical User Interface)が主体の携帯端末にとって、利用者に対するアプリの見せ方は重要です。高価に見えた方が使ってもらえる可能性が高くなります。 上記のことから、アプリの機能に関係なくても、ちょっとした動きをアニメーション化するメリットがあります。 2回にわたりアイテムの変更アニメーションについてまとめてみました。 アイテムの変更アニメーション(DefaultItemAnimator、デフォルト) アイテムの変更アニメーション(SimpleItemAnimatorの継承、カスタム) 今回は第2回目「SimpleItemAnimatorの継承、カスタム」編です。カスタム変更アニメーションの作り方を説明 ...
RecyclerViewへアイテムが表示されるとき、アニメーションはありません。。一瞬で表示されて終わりです。 「RecyclerViewへアイテムが表示される」ことを、ここでは「アイテムの出現」と言い表すことにします。 このアイテムの出現にアニメーションを付ける方法を紹介します。 アイテムの出現をアニメーションで演出することで、RecyclerViewに表示したい内容が際立つと思います。 ...
スポンサーリンク