RecyclerViewはListView(RecyclerViewの前身)の時に存在していたChoiceModeがありません。
同様な機能が欲しければプログラマ側で実装しなければなりません。
RecyclerView.AdapterをカスタマイズしてChoiceModeを実装してみたので紹介します。
目次
ChoiceModeの仕様
ChoiceModeの仕様は次のようにしました。
- 選択の状態はStable IDで管理
- モードは「NONE・SINGLE・MULTIPLE」の3つ
- アイテムのクリックで選択
- アイテムのポジション変更に対応
- Activityの再生成(端末回転時)に対応
SINGLE:常に1つを選択
MULTIPLE:複数を選択
※Stable IDについては「RecyclerView:アイテムの選択(recyclerview-selection API)」を参照
Choice Mode Single
常に1つのアイテムを選択するモードです。
クリックで選択します。すでに選択済みをのアイテムをクリックした場合は未選択になります。
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>
関連記事: