RecyclerView:アイテムの選択(ChoiceMode)

投稿日:  更新日:

RecyclerViewはListView(RecyclerViewの前身)の時に存在していたChoiceModeがありません。

同様な機能が欲しければプログラマ側で実装しなければなりません。

RecyclerView.AdapterをカスタマイズしてChoiceModeを実装してみたので紹介します。

スポンサーリンク

ChoiceModeの仕様

ChoiceModeの仕様は次のようにしました。

  • 選択の状態はStable IDで管理
  • モードは「NONE・SINGLE・MULTIPLE」の3つ
  • NONE:選択をしない
    SINGLE:常に1つを選択
    MULTIPLE:複数を選択
  • アイテムのクリックで選択
  • アイテムのポジション変更に対応
  • Activityの再生成(端末回転時)に対応

※Stable IDについては「RecyclerView:アイテムの選択(recyclerview-selection API)」を参照

Choice Mode Single

常に1つのアイテムを選択するモードです。

クリックで選択します。すでに選択済みをのアイテムをクリックした場合は未選択になります。

CHOICE_MODE_SINGLEの動作

Choice Mode Multiple

複数のアイテムを選択するモードです。

クリックで選択します。すでに選択済みをのアイテムをクリックした場合は未選択になります。

CHOICE_MODE_MULTIPLEの動作

スポンサーリンク

ChoiceModeの実装

全体

いきなりですが…、全体です。

RecyclerView.Adapterを継承したCheckableAdapterへChoiceModeを組み込んでいます。

abstract class CheckableAdapter<VH:RecyclerView.ViewHolder>
    : RecyclerView.Adapter<VH>() {

    companion object {
        const val CHOICE_MODE_NONE = 0      // 選択をしない
        const val CHOICE_MODE_SINGLE = 1    // 常に1つを選択
        const val CHOICE_MODE_MULTIPLE = 2  // 複数を選択
    }

    var choiceMode = CHOICE_MODE_SINGLE

    private var checkedItems = CheckedItems()
    private var attachedItems = mutableSetOf<RecyclerView.ViewHolder>()

    // --------------------------------------------------------------
    // Stable IDを使用
    //  ・デフォルトはポジションをIDにする
    //   (ただし、ポジションが変わるアイテムの操作は行わない前提)
    //
    init {
        setHasStableIds(true)
    }
    override fun getItemId(position: Int): Long = position.toLong()

    // --------------------------------------------------------------
    // 選択の状態を取得
    //
    fun isChecked(id: Long): Boolean = checkedItems.contains(id)
    fun getCheckedItems(): Set<Long> = checkedItems

    // --------------------------------------------------------------
    // 選択の状態の管理
    //  ・SINGLE、MULTIPLE、NONEの処理
    //  ・アイテムのクリックをトリガにして動作
    //
    fun createCheckableViewHolder(holder: VH): VH {
        holder.itemView.setOnClickListener{
            when(choiceMode) {
                CHOICE_MODE_SINGLE -> choiceSingleItem(holder)
                CHOICE_MODE_MULTIPLE -> choiceMultiItem(holder)
				CHOICE_MODE_NONE -> {}
                else -> {}
            }
        }
        return holder
    }
    private fun choiceSingleItem(holder: VH) {
        val _prevChecked = checkedItems.contains(holder.itemId)
        checkedItems.forEach() { _id ->     // 全てを未選択へ変更
            checkedItems.remove(_id)
            attachedItems.forEach {         // 表示中のアイテム⇒表示更新
                if(it.itemId == _id) notifyItemChanged(it.adapterPosition)
            }
        }
        if(! _prevChecked) {                // 状態のトグル
            checkedItems.add(holder.itemId)
            notifyItemChanged(holder.adapterPosition)
        }
    }
    private fun choiceMultiItem(holder: VH) {
        val _prevChecked = checkedItems.contains(holder.itemId)
        if(! _prevChecked)                  // 状態のトグル
            checkedItems.add(holder.itemId)
        else
            checkedItems.remove(holder.itemId)
        notifyItemChanged(holder.adapterPosition)
    }

    // --------------------------------------------------------------
    // Activityの再生成(端末の回転)時に状態を保存・復元
    //
    fun restoreInstanceState(inState: Bundle) {
        checkedItems = inState.getParcelable("CheckedItems")?: CheckedItems()
    }
    fun saveInstanceState(outState: Bundle) {
        outState.putParcelable("CheckedItems", checkedItems)
    }

    // --------------------------------------------------------------
    // 表示中のViewHolderをキャッシュ
    //
    override fun onViewAttachedToWindow(holder: VH) {
        attachedItems.add(holder)
    }
    override fun onViewDetachedFromWindow(holder: VH) {
        attachedItems.remove(holder)
    }

    // --------------------------------------------------------------
    // 選択の状態を格納
    //  ・状態はStable ID(Long型)で格納
    //  ・HashSetをBundleへ格納可能にするためParcelableを実装
    //
    private class CheckedItems(checkedItems: HashSet<Long>?)
        : HashSet<Long>(), Parcelable {
        constructor(): this(null)

        init {
            checkedItems?.let { addAll(checkedItems) }
        }

        // ----- Parcelableの定義
        override fun writeToParcel(dest: Parcel?, flags: Int) {
            dest?.writeLongArray(this.toLongArray())
        }
        override fun describeContents(): Int {
            return 0
        }

        companion object {
            @JvmField
            val CREATOR: Parcelable.Creator<CheckedItems>
            = object : Parcelable.Creator<CheckedItems> {
                override fun createFromParcel(src: Parcel): CheckedItems {
                    val _idsArray = longArrayOf().apply { src.readLongArray(this) }
                    return CheckedItems(_idsArray.toHashSet())
                }
                override fun newArray(size: Int): Array<CheckedItems?> {
                    return arrayOfNulls(size)
                }
            }
        }
    }
}

状態はStable IDで管理

選択の状態はStable ID(Long型)をKeyにしたSetコレクションに保持されます。

よって、アイテムの選択の有無はStable IDを指定して参照します。

制御はクリックリスナーで発動

クリックすることでアイテムは選択されます。

よって、アイテムのクリックリスナーで選択の制御を行う関数(choiceSingleItem、choiceMultipleItem)を実行しています。

表示中のViewHolderをキャッシュ

アイテムの選択を行った時、アイテムの再描画を行わせるためにnotifyItemChangedを発行しています。この時ポジションが必要です。

アイテムの変更(Insert・Remove・Move)でポジションは変化します。アイテムの全データを走査してポジションを洗い出すことは可能ですが、アイテム数が多くなれば処理が重くなります。

よって、onViewAttachedToWindow・onViewDetachedFromWindowコールバックを使って、表示中のアイテムのViewHolderをattachedItemsへキャッシュしています。

ViewHolderは最新のポジション情報を持っています。

ポジションの洗い出し先をattachedItemsにすれば、走査する範囲を絞り込めます。

状態の保存・復元

Activityの再生成(端末の回転時)は、旧Activityから新ActivityへBundleを介してデータを受け渡すことができます。

Bundleへ選択の状態を保存すれば、新Activity側で復元が可能です。

しかし、選択の状態を保持しているSetコレクションはBunbleへ保管ができません。

よって、SetへParcelableインターフェイスを実装したCheckedItemsを作っています。

Parcelableを実装したクラスはBundleへ保管ができます。

スポンサーリンク

ChoiceModeの使用方法

アイテムのデータに合わせてCheckableAdapterを継承したAdapterを作成します。

Stable ID(選択の状態を参照するKey)を何にするかは任意です。デフォルトはポジションをIDにします。

onCreateViewHolderでcreateCheckableViewHolder関数を使ってリスナーの登録を行います。

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

    // ----- ビューホルダー ------------------------------------------
    class ListViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
        val mesg = view.findViewById<CheckedTextView>(R.id.txtMesg)
    }

    // ----- アダプター本体 ------------------------------------------
    override fun getItemCount(): Int {
        return items.size
    }

    override fun getItemId(position: Int): Long {
        return items[position].hashCode().toLong()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int)
            : ListViewHolder {
        Log.i("Adapter", "Create ViewHolder!! viewType = ${viewType}")
        val _view = LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false)
        val _holder = ListViewHolder(_view)
        return createCheckableViewHolder(_holder)   // チェック可能なViewHolderを返す
    }

    override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
        Log.i("Adapter", "  Bind   ViewHolder!! position = ${position}")
        holder.mesg.text = items[position]
        holder.mesg.isChecked = isChecked(getItemId(position))
    }
}

RecyclerViewの実装部分は単純にAdapterを指定するだけでよく、特別なことは必要ありません。

Activityの再生成に対応したければ、restoreInstanceState・saveInstanceStateを使います。

	...
	
    lateinit var items: MutableList<String>

    lateinit var adapter: ListAdapter
    lateinit var rcySample: RecyclerView

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

		...
        adapter = ListAdapter(items)
        val _manager = LinearLayoutManager(this)

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

        savedInstanceState?.let {							// 復元
            adapter.restoreInstanceState(it)
        }
		
		...
	}
	
	override fun onSaveInstanceState(outState: Bundle) {	// 保存
        super.onSaveInstanceState(outState)
        adapter.saveInstanceState(outState)
    }
	
	...

サンプルはエレメントのViewにCheckedTextViewを使っています。

CheckedTextViewはCheckableインターフェースが実装されていて、属性state_checkedで状態を保持できます。

アイテムのバックグラウンドの変更は、state_checkedを使ったStateListDrawableで行っています。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    ...>

    <CheckedTextView
        android:id="@+id/txtMesg"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="@drawable/check_item"
        android:textAlignment="gravity"
        android:gravity="center"
        android:text="Message"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@color/Checked" android:state_checked="true" />
    <item android:drawable="@color/Unchecked" android:state_checked="false" />
</selector>
スポンサーリンク

関連記事:

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上のアイテムを選択する方法を紹介します。 外部ライブラリー(AndroidX)で提供されるrecyclerview-selection APIを用いた方法です。 ...
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に表示したい内容が際立つと思います。 ...
RecyclerViewで「画像ファイルをグリッド表示」する方法を、まとめます。 スマートフォンのアプリを作っていると、何度も遭遇するテクニックです。 ※環境:Android Studio Ladybug | 2024.2.1 Patch 1     Kotlin 2.0.0     Compose Compilerプラグイン 2.0.0     androidx.recyclerview:recyclerview:1.1.0 ...
RecyclerViewで「グリッド表示をスムーズにスクロール」する方法について、まとめます。 画像ファイルをグリッド表示する場合は、スムーズなスクロールを行うための工夫が必要です。 その工夫について紹介します。 ※環境:Android Studio Ladybug | 2024.2.1 Patch 1     Kotlin 2.0.0     Compose Compilerプラグイン 2.0.0     androidx.recyclerview:recyclerview:1.1.0 ...
スポンサーリンク