RecyclerView.ItemDecorationの表示、非表示をスクロール状態によって切り替える回

はじめに

RecyclerView.ItemDecorationを表示するかどうか、スクロールの状態によって切り替えたかったというお話です。
僕がDroidKaigi 2019の公式アプリのissueと、それに対して僕が提出したプルリクに基づく投稿です。*1 *2

やりたかったこと

具体的なissueの内容については上のリンクを見てもらえればと思います。

ざっくり言うと、あるRecyclerView上のItemDecorationについて、RecyclerViewのコンテンツを見やすくするために、スクロール中のみItemDecorationを表示しスクロールを止めたらそれを非表示にしたかった、というお話しです。

どうやったか

RecyclerView.OnScrollListenerを設定してスクロール状態に応じaddItemDecorationremoveItemDecorationを行いました。

まずコードの例を出してそこから少し説明をしようと思います。

//rvはRecyclerViewです。
rv.addOnScrollListener(
    object : RecyclerView.OnScrollListener() {
        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
            rv.removeItemDecoration(itemDecoration)
            if (newState != RecyclerView.SCROLL_STATE_IDLE) {
                rv.addItemDecoration(itemDecoration)
            }
        }
    }
)

onScrollStateChanged内にてitemDecorationを付けたり外したりすることによってその表示、非表示を切り替えています。
具体的には、rvの状態が変化したら一旦itemDecorationを外し、その状態が止まった状態でなければもう一度addItemDecorationしています。

newStateについて

これだけだとあまりにも説明に味がないのでonScrollStateChangedの引数であるnewStateについて少し説明します。

newStateの値 どのような状態か
SCROLL_STATE_IDLE スクロールしていない状態
SCROLL_STATE_DRAGGING 外からの入力によってドラッグされている状態
SCROLL_STATE_SETTLING 外からの入力はないが動いている状態

外からの入力という点がちょっと?ですが、Android Developersの説明文によればoutside input such as user touch inputとのことなので、基本的にはユーザー操作と考えて良さそうです。

SCROLL_STATE_SETTLINGに関しては、スワイプした後の慣性で動いてるような状態でしょう。

おまけ

onScrollStateChanged内での処理は以下のようにすることも考えました。

override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
    if (newState == RecyclerView.SCROLL_STATE_IDLE) {
        rv.removeItemDecoration(itemDecoration)
    } else {
        rv.addItemDecoration(itemDecoration)
    }
}

考えたのですが、

  • onScrollStateChanged自体が一秒間に何回も呼ばれるようなものではないので、一旦removeItemDecorationしてからaddItemDecorationしてもそこまで負荷が高くはならないのではないか
  • もうすでに追加されているItemDecorationを再度addItemDecorationするのが嫌だ

といった理由から先の書き方にしました。

少し待ってから非表示にする

冒頭であげたissueでは上のコードとは異なり、「スクロールが止まってから少しの間を空けてItemDecorationを非表示にする」ことに取り組みました。

ここでも上と同じようにまずコードを出して軽く説明したいと思います。

rv.addOnScrollListener( 
    object : RecyclerView.OnScrollListener() {
        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
            val job = GlobalScope.launch(Dispatchers.Main) {
                delay(500)
                rv.removeItemDecoration(itemDecoration)
            }

            super.onScrollStateChanged(recyclerView, newState)
            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                job.start()
            } else {
                if (job.isActive) { job.cancel() }
                rv.removeItemDecoration(itemDecoration)
                rv.addItemDecoration(itemDecoration)
            }
        }
    }
)

ここではスクロールが止まってから500ミリ秒後にremoveItemDecorationをしています。

「500ミリ秒待ってremoveItemDecoration」をcoroutinesのJobにしています。
この主な理由は、待ち時間のうちに再びスクロールされたときにその操作をキャンセルしたかったというものです。 if (job.isActive) { job.cancel() }ですね。

あとはそんな変わったことはしていないと思います。

最後に

DroidKaigi楽しみです。

*1:嬉しいことにmergeしていただきました!手厚いサポートありがとうございました!

*2:あくまで僕の試行とPRに基づくものでベストプラクティスだという保証はありませんのでご了承ください。