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の正体のようです。
LinearLayoutManager
やGridLayoutManager
ではこの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
を使っている
ようです。
少し唐突感を和らげられるよう、LinearInterpolator
をAccelerateInterpolator
に変えてみます。
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
にちょっと変更入れるだけで結構動きを変えられますが、dx
とdy
は簡単に変更しないほうが良さそう。
今回の例だと、一回の計算でたどり着く距離の時に指定した位置にスクロールが止まらなくなってしまう。
サンプルコードはこちら https://github.com/chibatching/long-distance-smooth-scroll-sample