Posted at

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

More than 1 year has passed since last update.


はじめに

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)も考慮した実装にするのが汎用的だと思います。