- はじめに
- recyclerview-selection とは
- 前準備
- SelectionTrackerインスタンスをつくる
- RecyclerView.Adapter#onBindViewHolderで選択状態をViewに反映する
- 採用情報
はじめに
はじめまして、この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 では、エンジニアメンバーを募集しております! 興味のある方はぜひ下記募集ページをご確認下さい!