RecyclerViewのスクロールをふわっと着地させる方法の紹介です。
サンプルコードも掲載しますが、完全なコードはこちらにあります。list表示にGroupieを使っていますが、本記事では解説しません。
https://github.com/takarabe-hamuyatti/EasingTest
最終イメージ
減速させるためには
目的地に到達する辺りで望んだ通りに減速させるということは、時間あたりに進ませる距離を事前に指示するということです。
一定の間隔で加速させる場合は、目的地までの残り距離で調節する必要はありませんが、減速の場合速度が0になれば当然スクロールは止まってしまいます。
そのため上記画像のように速度を逓減させて目的地に到達するためには、減速する前 に 目的地までの距離 を図る必要があります。
RecyclerViewのつらみ
この目的地までの距離を図る点で、目的地が画面外にある場合RecycerViewだと困難な点があります。
RecyclerViewは名前の通り画面外にいったviewのことはリサイクルして、効率的な動作を可能にしています。そのため画面外の目的地までの距離を算出することはRecyclerViewだとできません。画面内に入ったviewまでのスクロール量を調べるとしても、画面に入ったタイミングを検知する方法が必要です。
どうやるか
LinearSmoothScrollerを拡張して実現します。
class EaseOutScroller(
context: Context,
private val durationOnFindTarget: Int = 550,
) : LinearSmoothScroller(context) {
override fun onTargetFound(targetView: View, state: RecyclerView.State, action: Action) {
if (durationOnFindTarget <= 0) return
val dy = calculateDyToMakeVisible(targetView, verticalSnapPreference)
action.update(0, -dy, durationOnFindTarget, DecelerateInterpolator())
}
}
こんな感じで作ったのを
class ListFragment : Fragment(R.layout.fragment_list) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapter = Adapter()
val scroller = EaseOutScroller(requireContext())
FragmentListBinding.bind(view).let { binding ->
binding.list.adapter = adapter
adapter.showList()
binding.fab.setOnClickListener {
if (scroller.isRunning) return@setOnClickListener
scroller.targetPosition = 0
(binding.list.layoutManager as? LinearLayoutManager)?.startSmoothScroll(scroller)
}
}
}
}
LinearLayoutManager
のstartSmoothScroll
の引数で渡してあげればOKです。
解説
登場人物
名前 | 解説 |
---|---|
calculateDyToMakeVisible | LinearLayoutManagerに用意されている関数で、viewのインスタンスを渡してあげればそのviewが完全に見えるまでに必要なスクロール量を教えてくれる。 |
onTargetFound | スムーズスクロール中に、目的地のインデックスが画面内に現れた時に呼ばれる関数。引数で目的地のviewを持ってきてくれる。 |
Action | スムーズスクロールを司っているクラス。このクラスに対して距離やアニメーションを指定してスクロールをかけ直すことができる。 |
DecelerateInterpolator | animationの変化の比率を表す Interpolator interface を継承している。減速用のInterpolator |
calculateDyToMakeVisible
とonTargetFound
を組み合わせる点が重要です。
onTargetFoundを利用すれば目的地のviewが画面内に入ってきたことがわかり、距離を算出できるタイミングになったことがわかります。
onTargetFoundの中で目的地のviewのインスタンスを受け取ってcalculateDyToMakeVisibleで距離を算出し、actionをupdateしてあげればふわっと着地します。
問題点
この方法はあくまで目的地のviewが見えてから減速させる方法です。もし、目的地が見える遥か前から減速を始めたいといった場合、この方法は使えません。ただそういった動きをするアプリは現状見たことないので、この方法で十分なケースがほとんどではないかとも思います。