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の変更を有効化
...
}
関連記事:
