はじめまして、この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