40
28

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 5 years have passed since last update.

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

Last updated at Posted at 2017-03-19

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

40
28
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
40
28

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?