LoginSignup
11
2

More than 5 years have passed since last update.

RecyclerViewのsmoothScrollToPositionの停止位置を修正する

Posted at

はじめに

RecyclerViewでsmoothScrollToPositionを使った際、中途半端な位置でしか停止してくれなかったので、調べたときのメモです。

前提

LinearLayoutManagerを使用
スクロール方向は縦

やりたかったこと

スクロールの停止位置は、必ず一番上にしたい

デフォルトの挙動

スクロールする方向によって、上になったり下になったり…

内部実装の調査

smoothScrollToPosition

よく使われるLinearLayoutManagerの中の実装はこんな感じです。

LinearLayoutManager.java
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
   LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext());
   // scrollerにスクロールのtargetPositionを設定
   linearSmoothScroller.setTargetPosition(position);
   // scrollerを使ってスクロール実行
   startSmoothScroll(linearSmoothScroller);
}

LinearSmoothScrollerSmoothScrollerを継承しています。

SmoothScroller

SmoothScrollerはabstractクラスで、その中にonTargetFoundというmethodがあります。

SmoothScroller.java
public abstract static class SmoothScroller {
   ...()
   /**
    * Called when the target position is laid out. This is the last callback SmoothScroller
    * will receive and it should update the provided {@link Action} to define the scroll
    * details towards the target view.
    * @param targetView    The view element which render the target position.
    * @param state         Transient state of RecyclerView
    * @param action        Action instance that you should update to define final scroll action
    *                      towards the targetView
    */
   protected abstract void onTargetFound(View targetView, State state, Action action);
   ...()
}

SmoothScrollerはいくつかのcallbackがあるのですが、このcallbackは一番最後に通知されるもので、targetのViewがレイアウト内に入ってきたときにcallされます。ここで何の処理をするのかというと、引数で渡ってきたactionに対してupdateを行って位置の微調整を行いなさい、というような趣旨のコメントが書いてあります。
それでは、具体的にLinearSmoothScrollerではどのような実装になっているか確認します。

LinearSmoothScroller

LinearSmoothScroller.java
@Override
protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
    final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
    final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
    final int distance = (int) Math.sqrt(dx * dx + dy * dy);
    final int time = calculateTimeForDeceleration(distance);
    if (time > 0) {
        action.update(-dx, -dy, time, mDecelerateInterpolator);
    }
}

必要なx,y方向のスクロール量を計算し、action.updateを実行しています。計算するcalculateDx/DyToMakeVisibleの実装の中身は泥臭いのでコメントだけ見ると、このようになっています。

LinearSmoothScroller.java
/**
 * Calculates the vertical scroll amount necessary to make the given view fully visible
 * inside the RecyclerView.
 *
 * @param view           The view which we want to make fully visible
 * @param snapPreference The edge which the view should snap to when entering the visible
 *                       area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
 *                       {@link #SNAP_TO_ANY}.
 * @return The vertical scroll amount necessary to make the view visible with the given
 * snap preference.
 */
public int calculateDyToMakeVisible(View view, int snapPreference) {

第一引数のview全体が見えるようになるまでに必要なスクロール量を計算します、とあります。第二引数はviewを最終的に表示させたい位置を示し、以下の3つから選択します。

  • SNAP_TO_START
    • 左または上に寄せる
  • SNAP_TO_END
    • 右または下に寄せる
  • SNAP_ANY
    • 表示領域内であればどこでもOK

第二引数を決定するgetVerticalSnapPreferenceを見るとこのようになっています。

LinearSmoothScroller.java
protected int getVerticalSnapPreference() {
    return mTargetVector == null || mTargetVector.y == 0 ? SNAP_TO_ANY :
            mTargetVector.y > 0 ? SNAP_TO_END : SNAP_TO_START;
}

スクロースする方向によって、SNAP_TO_STARTSNAP_TO_ENDを切り替えています。つまり、今回の目的で言えば、このsnapPreferenceを修正(override)してあげれば実現できることになります。

結論

説明文は長くなってしまいましたが、実際に実装すべきコードはデフォルトのLinearSmoothScrollerを少しだけ拡張するだけで充分ということがわかりました。

CustomLayoutManager.kt
class CustomLayoutManager(context: Context) : LinearLayoutManager(context) {
    override fun smoothScrollToPosition(recyclerView: RecyclerView, state: RecyclerView.State?, position: Int) {
        val linearSmoothScroller = object : LinearSmoothScroller(recyclerView.context) {
            override fun getVerticalSnapPreference(): Int = if (reverseLayout) SNAP_TO_END else SNAP_TO_START
        }
        linearSmoothScroller.targetPosition = position
        startSmoothScroll(linearSmoothScroller)
    }
}

ちなみにこの例ではreverseLayoutだけ考慮していますが、本来はスクロール方向(vertical/horizontal)も考慮した実装にするのが汎用的だと思います。

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