Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

RecyclerViewの長距離スムーズスクロールをスムーズにする

More than 3 years have passed since last update.

TL;DR

  • RecyclerViewのsmooth scrollの動作を変更するにはSmoothScrollerを実装する
  • 全部自分で実装するのはかなり大変そうだが、既存の拡張だけでもそれなりに動作を変更できる

はじめに

タイトルがなに言ってるんだこいつは状態ですが、移動距離の長くなるポジションでRecyclerView#smoothScrollToPositionすると下のGifアニメのように、ひたすら一定の速度で移動し続ける動作になります。

Twitter公式クライアントでは、タブをタップした時にTLの先頭まで戻れますが、移動距離が長くなるようなときにはsmooth scrollせずに一瞬で先頭が表示されます。
このような動作にしてもいいのですが、あまりにも味気ないので少しスクロールを見せた後、指定したポジションに移動する動きを実現するために少し調査したのでまとめます。

RecyclerViewをsmooth scrollさせているのは誰か

LinearLayoutManager#smoothScrollToPositionの実装を見てみます。

@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
        int position) {
    LinearSmoothScroller linearSmoothScroller =
            new LinearSmoothScroller(recyclerView.getContext());
    linearSmoothScroller.setTargetPosition(position);
    startSmoothScroll(linearSmoothScroller);
}

LinearSmoothScrollerというやつのインスタンスを生成してstartSmoothScrollに渡しているだけですね。
つまり、こいつがsmoothScrollの正体のようです。
LinearLayoutManagerGridLayoutManagerではこのLinearSmoothScrollerを使っていますが、厳密にはRecyclerView#SmoothScrollerというabstract classを継承していればいいようです。

長距離移動するときの動作

実際にRecyclerViewの内部動作を追おうとすると、いつまでたってもたどり着けなくなりそうなので適当なところにデバッグポイント張って調べてみます。
すると、updateActionForInterimTargetというメソッドで移動距離と時間等を計算しているのですが、指定のポジションに到達しなかった場合には、到達するまで繰り返し呼ばれているらしいことがわかります。

なので、こいつを拡張してupdateActionForInterimTargetの2回目の呼び出しでsmoothScrollをやめて指定のポジションへ移動するようにしてみます。

Smooth scrollの動きをカスタムする

いきなりですがコードです。

class OneTimeSmoothScroller(recyclerView: RecyclerView) : LinearSmoothScroller(recyclerView.context) {

    private var isScrolled: Boolean = false

    override fun updateActionForInterimTarget(action: Action) {
        if (isScrolled) {
            // 2回目は一気に指定位置までジャンプ
            action.jumpTo(targetPosition)
        } else {
            // 1回目は通常通りにスクロール
            super.updateActionForInterimTarget(action)
            isScrolled = true
        }
    }
}

これだけです。実際に動きを見てみます。

意外と想定していた動きをしてくれましたが、スクロール速度が一定なので若干ジャンプが唐突に感じます。
そこで、もう少し変更を加えてみます。

LinearSmoothScrollerの実装を見てみるとupdateActionForInterimTargetの最後に次の記述があります。

action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO)
        , (int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO)
        , (int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator);

どうやら

  • 引数で渡されたactionに値を設定するとそれがsmooth scrollのアニメーションに反映される
  • アニメーションにLinearInterpolatorを使っている

ようです。
少し唐突感を和らげられるよう、LinearInterpolatorAccelerateInterpolatorに変えてみます。

class OneTimeSmoothScroller(recyclerView: RecyclerView) : LinearSmoothScroller(recyclerView.context) {
    private var isScrolled: Boolean = false

    override fun updateActionForInterimTarget(action: Action) {
        if (isScrolled) {
            action.jumpTo(targetPosition)
        } else {
            super.updateActionForInterimTarget(action)
            action.duration *= 2  // 動きが分かりやすいよう時間を2倍にする
            action.interpolator = AccelerateInterpolator(1.5F)
            isScrolled = true
        }
    }
}

実際の動きはこのような感じになります。

Gifアニメだとわかりづらいですが、多少唐突感は軽減された感じになりました。

etc

actionにちょっと変更入れるだけで結構動きを変えられますが、dxdyは簡単に変更しないほうが良さそう。
今回の例だと、一回の計算でたどり着く距離の時に指定した位置にスクロールが止まらなくなってしまう。

サンプルコードはこちら https://github.com/chibatching/long-distance-smooth-scroll-sample

chibatching
Androidアプリ開発
http://www.chibatching.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away