11
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ZOZOAdvent Calendar 2022

Day 12

RecyclerViewのスクロールを減速させてふわっと着地させる方法

Last updated at Posted at 2022-12-11

RecyclerViewのスクロールをふわっと着地させる方法の紹介です。
サンプルコードも掲載しますが、完全なコードはこちらにあります。list表示にGroupieを使っていますが、本記事では解説しません。
https://github.com/takarabe-hamuyatti/EasingTest

最終イメージ

ezgif.com-gif-maker (1).gif
カクカクでわかり辛いですが、それなりにふわっとしてます。

減速させるためには

dt_decel.png
目的地に到達する辺りで望んだ通りに減速させるということは、時間あたりに進ませる距離を事前に指示するということです。
一定の間隔で加速させる場合は、目的地までの残り距離で調節する必要はありませんが、減速の場合速度が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)
            }
        }
    }
}

LinearLayoutManagerstartSmoothScrollの引数で渡してあげればOKです。


解説

登場人物

名前 解説
calculateDyToMakeVisible LinearLayoutManagerに用意されている関数で、viewのインスタンスを渡してあげればそのviewが完全に見えるまでに必要なスクロール量を教えてくれる。
onTargetFound スムーズスクロール中に、目的地のインデックスが画面内に現れた時に呼ばれる関数。引数で目的地のviewを持ってきてくれる。
Action スムーズスクロールを司っているクラス。このクラスに対して距離やアニメーションを指定してスクロールをかけ直すことができる。
DecelerateInterpolator animationの変化の比率を表す Interpolator interface を継承している。減速用のInterpolator

calculateDyToMakeVisibleonTargetFoundを組み合わせる点が重要です。
onTargetFoundを利用すれば目的地のviewが画面内に入ってきたことがわかり、距離を算出できるタイミングになったことがわかります。
onTargetFoundの中で目的地のviewのインスタンスを受け取ってcalculateDyToMakeVisibleで距離を算出し、actionをupdateしてあげればふわっと着地します。

問題点

この方法はあくまで目的地のviewが見えてから減速させる方法です。もし、目的地が見える遥か前から減速を始めたいといった場合、この方法は使えません。ただそういった動きをするアプリは現状見たことないので、この方法で十分なケースがほとんどではないかとも思います。

11
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?