はじめまして、この3月に入社したAndroidエンジニアの kgmyshin です。
Androidより、2018年3月初旬に Support Library 28.0.0 alpha がリリースされましたね。 ReleaseNoteを見てみると、新しいAPIとしてrecyclerview-selectionが追加されました。
今回は、早速その新しいAPIを触ってみた所感として、Android開発者向けに「どういうものなのか」「どう使えば良いのか」を紹介していきたいと思います。
recyclerview-selection とは
まずはこちらを見てください。
RecyclerViewでこの挙動を自力で実装しようとすると少し大変です。 タップで複数選択する機能であれば難しくないですが、ドラッグ中に選択状態にしたり、選択中のみオートスクロールも実装したりとなると少し苦労します。
この〈複数選択をドラッグでやりつつオートスクロールなども〉を良い感じにやってくれるのが recyclerview-selection です。
使い方
すごく大まかには、2ステップで実装できます。
-
SelectionTrackerインスタンスをつくる -
RecyclerView.Adapter#onBindViewHolderで選択状態をViewに反映する
早速実装してみましょう。
今回は、下記のBook クラスのリストを一覧表示した RecyclerView に対して複数選択機能を実装していきます。
data class Book(
val id: Long,
val title: String,
val subTitle: String
)
選択状態中に getSelection を呼び出した時に、どのリストを返却してほしいか。 選択中の Book そのものを返してほしいのか、 選択中の Book の id を返すのかで実装が少し変わってきます。
今回は後者の方法で実装していきます。前者の方法での実装例はgithubにあげてありますので、興味あるの方はそちらをご覧ください。
前準備
まずはstableId に book.idを使うようにします。
stableId とは RecyclerView に設定する各アイテムのIDのことです。自分で有効にしない限りは NO_ID が設定されています。これを有効にして適切に設定してあげることで RecyclerView のパフォーマンスに効くことがあります。
下記のように RecyclerView.Adapter#setHasStableIds で trueをセットし、getItemId をオーバーライドして book.id を返却するようにすれば前準備の完了です。
class BookAdapter(
context: Context,
private val bookList: List<Book>
) : RecyclerView.Adapter<BookViewHolder>() {
:
init {
setHasStableIds(true)
}
:
override fun getItemId(position: Int): Long = bookList[position].id
:
}
SelectionTrackerインスタンスをつくる
次に SelectionTracker インスタンスを作りましょう。 Builder を用いて下記のように作ります。
selectionTracker = SelectionTracker.Builder<Long>(
"my-selection-id",
binding.recyclerView,
StableIdKeyProvider(binding.recyclerView),
BookIdDetailsLookup(binding.recyclerView),
StorageStrategy.createLongStorage()
).build()
Builderのコンストラクタの各引数についての説明は下記です。
| 第n引数 | 例 | 説明 |
|---|---|---|
| 第1引数 | "my-selection-id" | 使用する activty や fragment でユニークになるように指定します。 onRestoreInstanceState などで Bundle から取得する keyとして使ってるようです |
| 第2引数 | recyclerView | 該当の RecyclerView を指定してください。 |
| 第3引数 | StableIdKeyProvider(binding.recyclerView) | IdKeyProvider を設定します。IdKeyProvider は item (選択対象) と key (選択時に保持するもの) の対応関係を解決するためのものです。 |
| 第4引数 | BookIdDetailsLookup(binding.recyclerView) | MotionEventを元に今どこの item (選択対象) の上にいるのかを検索する ItemDetailsLookup を実装したものを指定します。 |
| 第5引数 | StorageStrategy.createLongStorage() | savedState に何を保存するのかという情報を持った StorageStrategy インスタンスを設定します。 |
今回は選択時に保持するものとして stableId ( book.id ) を使用するので、IdKeyProvider には標準で用意されている StableIdKeyProvider を指定します。
自作する必要があるのは ItemDetailsLookup だけで、こちらは ItemDetailsLookupのSampleを参考に BookIdDetailsLookup は下記のように実装しました。
class BookIdDetailsLookup(
private val recyclerView: RecyclerView
) : ItemDetailsLookup<Long>() {
override fun getItemDetails(e: MotionEvent): ItemDetails<Long>? = recyclerView.findChildViewUnder(
e.x,
e.y
)?.let {
(recyclerView.getChildViewHolder(it) as? BookViewHolder)?.getItemIdDetails()
}
}
RecyclerView.Adapter#onBindViewHolderで選択状態をViewに反映する
ここまで実装をしたものを動かしてみました。
見てのとおり(しっかり複数選択機能自体は動いているものの)どれが選択されているのかがまったくわかりません。
View への選択状態かどうかの反映は自分で実装していきましょう。
recyclerview-selection では選択状態になった時、最終的には RecyclerView.Adapter#onBindViewHolder が呼ばれるので、ここで背景色の変更をします。
実際には selector を作ってあげて、選択されているか否かを元に View#setActivated を呼び出します。 ( setSelected ではなく setActivated を呼ぶ理由は こちら) を参照ください。 )
選択されているか否かはSelectionTracker#isSelectedでわかるので、下記のようにして Adapter を作ります。
class BookAdapter(
context: Context,
private val sectionTracker: SelectionTracker<Long>,
private val bookList: List<Book>
) : RecyclerView.Adapter<BookViewHolder>() {
:
override fun onBindViewHolder(
holder: BookViewHolder,
position: Int
) {
val item = bookList[position]
holder.bind(
sectionTracker.isSelected(item.id), // setActivated は holder.bindの中で
position,
bookList[position]
)
}
:
}
ただ、このコードをそのまま動かすと クラッシュします 。
なぜかというと SelectionTracker の生成の時にセットする RecyclerView に Adapter がないと IllegalArgumentException が投げられるようになっているからです。
( Adapter の生成には SectionTrackerがいるが、 SectionTrackerの生成には逆にAdapterが必要になってしまっているからです。)
public abstract class SelectionTracker<K> {
:
public static final class Builder<K> {
:
public Builder(
@NonNull String selectionId,
@NonNull RecyclerView recyclerView,
@NonNull ItemKeyProvider<K> keyProvider,
@NonNull ItemDetailsLookup<K> detailsLookup,
@NonNull StorageStrategy<K> storage) {
:
mAdapter = recyclerView.getAdapter();
:
checkArgument(mAdapter != null); // ← throw IllegalArgumentException
:
}
:
}
:
}
そのためSectionTrackerは下記のように Adapter作成後に外部から設定する必要があります。
class BookAdapter(
context: Context,
private val bookList: List<Book>
) : RecyclerView.Adapter<BookViewHolder>() {
:
var sectionTracker: SelectionTracker<Long>? = null // Adapterを作成後に、SectionTrackerを作成してそのあとにadapterにセットする
:
override fun onBindViewHolder(
holder: BookViewHolder,
position: Int
) {
val item = bookList[position]
holder.bind(
sectionTracker?.isSelected(item.id) ?: false, // setActivated は holder.bindの中で
position,
bookList[position]
)
}
:
}
依存関係が複雑でスッキリした実装とは言いにくいのですが(一応SampleであげているコードはSectionTrackerを直接メンバーに持つのではなくインタフェースを噛ませてますが、正直あまり納得のいく実装はできていません)、これで完成です!
所感
まだalpha版なのでバグをちらほら見かけます。ただし、冒頭にも書きましたが〈複数選択をドラッグでやりつつオートスクロール〉する処理を実装するのはひと苦労なので、そういったケースを実装する場合は新APIを使ったほうが良いなと感じました。
サンプルコードは下記に置いております。ご興味ある方はご覧ください。
- kgmyshin/recyclerview-selection-sample Book.id(stableID)をSelectionにしたもの
- kgmyshin/recyclerview-selection-sample BookをそのままSelectionにしたもの
採用情報
現在、DMM.com Groupでは、アプリ開発のエンジニアメンバーを募集しております! 興味のある方はぜひ下記募集ページをご確認下さい! dmm-corp.com