RecyclerView上のアイテムを選択する方法を紹介します。
外部ライブラリー(AndroidX)で提供されるrecyclerview-selection APIを用いた方法です。
目次
recyclerview-selection APIを用いたアイテムの選択
recyclerview-selection APIを用いたアイテムの選択は次のような動作です。
長押し後にマルチセレクションモードに入り、そのままスワイプを行えば選択の範囲を広げることができます。
選択されたアイテムがある間はマルチセレクションモードを保持します。
マルチセレクションモード中はアイテムのクリックで選択の有無がトグルします。
環境設定
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が操作を追跡するクラスです。内部でアイテムの選択を判定し、その状態を管理しています。
... 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のインスタンスは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に出来ません。
選択の有無を確認
選択リスト(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 *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の変更を有効化 ... }
関連記事: