RecyclerView - Jak płynnie przewijać do góry elementu na określonej pozycji?

126

W RecyclerView mogę nagle przewinąć do góry wybranego elementu, używając:

((LinearLayoutManager) recyclerView.getLayoutManager()).scrollToPositionWithOffset(position, 0);

Jednak powoduje to gwałtowne przeniesienie elementu na najwyższą pozycję. Chcę płynnie przejść na górę elementu .

Próbowałem też:

recyclerView.smoothScrollToPosition(position);

ale nie działa dobrze, ponieważ nie przenosi elementu na wybraną pozycję na górę. Po prostu przewija listę, aż pozycja na pozycji jest widoczna.

Tiago
źródło

Odpowiedzi:

225

RecyclerViewjest zaprojektowany jako rozszerzalny, więc nie ma potrzeby tworzenia podklasy LayoutManager(jak sugerował droidev ) tylko po to, by wykonać przewijanie.

Zamiast tego po prostu utwórz SmoothScrollerz preferencją SNAP_TO_START:

RecyclerView.SmoothScroller smoothScroller = new LinearSmoothScroller(context) {
  @Override protected int getVerticalSnapPreference() {
    return LinearSmoothScroller.SNAP_TO_START;
  }
};

Teraz ustawiasz pozycję, do której chcesz przewijać:

smoothScroller.setTargetPosition(position);

i przekaż ten SmoothScroller do LayoutManager:

layoutManager.startSmoothScroll(smoothScroller);
Paul Woitaschek
źródło
9
Dzięki za to krótsze rozwiązanie. Jeszcze tylko dwie rzeczy, które musiałem wziąć pod uwagę w mojej implementacji: Ponieważ mam widok przewijania w poziomie, musiałem ustawić protected int getHorizontalSnapPreference() { return LinearSmoothScroller.SNAP_TO_START; }. Ponadto musiałem wdrożyć metodę abstrakcyjną public PointF computeScrollVectorForPosition(int targetPosition) { return layoutManager.computeScrollVectorForPosition(targetPosition); }.
AustrianDude
2
RecyclerView może być zaprojektowany jako rozszerzalny, ale taka prosta rzecz wydaje się leniwa, gdy jej brakuje. Dobra odpowiedź! Dziękuję Ci.
Michael,
8
Czy jest jakikolwiek sposób, aby zacząć przyciągać, ale z przesunięciem X dp?
Mark Buikema
5
czy można spowolnić jego prędkość?
Alireza Noorali
3
Zastanawiam się również, czy można zmodyfikować (wolniej) szybkość, z jaką występuje przewijanie. @AlirezaNoorali
vikzilla
112

w tym celu musisz utworzyć niestandardowy LayoutManager

public class LinearLayoutManagerWithSmoothScroller extends LinearLayoutManager {

    public LinearLayoutManagerWithSmoothScroller(Context context) {
        super(context, VERTICAL, false);
    }

    public LinearLayoutManagerWithSmoothScroller(Context context, int orientation, boolean reverseLayout) {
        super(context, orientation, reverseLayout);
    }

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

    private class TopSnappedSmoothScroller extends LinearSmoothScroller {
        public TopSnappedSmoothScroller(Context context) {
            super(context);

        }

        @Override
        public PointF computeScrollVectorForPosition(int targetPosition) {
            return LinearLayoutManagerWithSmoothScroller.this
                    .computeScrollVectorForPosition(targetPosition);
        }

        @Override
        protected int getVerticalSnapPreference() {
            return SNAP_TO_START;
        }
    }
}

użyj tego dla swojego RecyclerView i wywołaj smoothScrollToPosition.

przykład:

 recyclerView.setLayoutManager(new LinearLayoutManagerWithSmoothScroller(context));
 recyclerView.smoothScrollToPosition(position);

spowoduje to przewinięcie do góry elementu RecyclerView o określonej pozycji.

mam nadzieję że to pomoże.

droidev
źródło
Miałem problem z wdrożeniem sugerowanego LayoutManagera. Ta odpowiedź była dla mnie znacznie łatwiejsza: stackoverflow.com/questions/28025425/ ...
arberg
3
Tylko przydatna odpowiedź po godzinach bólu, to działa zgodnie z przeznaczeniem!
wblaschko
1
@droidev Użyłem twojego przykładu z płynnym przewijaniem i ogólnie SNAP_TO_START jest tym, czego potrzebuję, ale działa z niektórymi problemami dla mnie. Czasami przewijanie zatrzymuje się 1, 2, 3 lub 4 pozycje wcześniej niż przechodzę smoothScrollToPosition. Dlaczego ten problem może wystąpić? Dzięki.
Natasha
czy możesz jakoś dotrzeć do nas ze swoim kodem? Jestem prawie pewien, że to twój problem z kodem.
droidev
1
To jest poprawna odpowiedź, pomimo zaakceptowanej
Alessio
26

To jest funkcja rozszerzenia, którą napisałem w Kotlinie do użycia z RecyclerView(na podstawie odpowiedzi @Paul Woitaschek):

fun RecyclerView.smoothSnapToPosition(position: Int, snapMode: Int = LinearSmoothScroller.SNAP_TO_START) {
  val smoothScroller = object : LinearSmoothScroller(this.context) {
    override fun getVerticalSnapPreference(): Int = snapMode
    override fun getHorizontalSnapPreference(): Int = snapMode
  }
  smoothScroller.targetPosition = position
  layoutManager?.startSmoothScroll(smoothScroller)
}

Użyj tego w ten sposób:

myRecyclerView.smoothSnapToPosition(itemPosition)
vovaost
źródło
To działa! Problem z płynnym przewijaniem polega na tym, że masz duże listy, a przewijanie do dołu lub do góry zajmuje dużo czasu
portfoliobuilder
@vovaost jak mogę wyśrodkować żądaną pozycję?
ysfcyln
@ysfcyln Sprawdź ten stackoverflow.com/a/39654328/1502079 odpowiedź
vovaost
12

Możemy spróbować w ten sposób

    recyclerView.getLayoutManager().smoothScrollToPosition(recyclerView,new RecyclerView.State(), recyclerView.getAdapter().getItemCount());
Supto
źródło
5

Zastąp funkcję CalcDyToMakeVisible / calcDxToMakeVisible w LinearSmoothScroller, aby zaimplementować przesunięcie pozycji Y / X

override fun calculateDyToMakeVisible(view: View, snapPreference: Int): Int {
    return super.calculateDyToMakeVisible(view, snapPreference) - ConvertUtils.dp2px(10f)
}
user2526517
źródło
To jest to, czego szukałem. Dzięki za udostępnienie tego w celu wdrożenia przesunięcia pozycji x / y :)
Shan Xeeshi
3

Najłatwiejszy sposób przewijania a RecyclerViewjest następujący:

// Define the Index we wish to scroll to.
final int lIndex = 0;
// Assign the RecyclerView's LayoutManager.
this.getRecyclerView().setLayoutManager(this.getLinearLayoutManager());
// Scroll the RecyclerView to the Index.
this.getLinearLayoutManager().smoothScrollToPosition(this.getRecyclerView(), new RecyclerView.State(), lIndex);
Mapsy
źródło
2
Nie spowoduje to przewinięcia do pozycji, jeśli ta pozycja jest już widoczna. Przewija najmniejszą ilość potrzebną do wyświetlenia pozycji. Dlatego potrzebujemy tego z określonym przesunięciem.
TatiOverflow
3

Użyłem w ten sposób:

recyclerView.getLayoutManager().smoothScrollToPosition(recyclerView, new RecyclerView.State(), 5);
Criss
źródło
2

Dzięki @droidev za rozwiązanie. Jeśli ktoś szuka rozwiązania Kotlin, skieruj to:

    class LinearLayoutManagerWithSmoothScroller: LinearLayoutManager {
    constructor(context: Context) : this(context, VERTICAL,false)
    constructor(context: Context, orientation: Int, reverseValue: Boolean) : super(context, orientation, reverseValue)

    override fun smoothScrollToPosition(recyclerView: RecyclerView?, state: RecyclerView.State?, position: Int) {
        super.smoothScrollToPosition(recyclerView, state, position)
        val smoothScroller = TopSnappedSmoothScroller(recyclerView?.context)
        smoothScroller.targetPosition = position
        startSmoothScroll(smoothScroller)
    }

    private class TopSnappedSmoothScroller(context: Context?) : LinearSmoothScroller(context){
        var mContext = context
        override fun computeScrollVectorForPosition(targetPosition: Int): PointF? {
            return LinearLayoutManagerWithSmoothScroller(mContext as Context)
                    .computeScrollVectorForPosition(targetPosition)
        }

        override fun getVerticalSnapPreference(): Int {
            return SNAP_TO_START
        }


    }

}
PANKAJ KUMAR MAKWANA
źródło
1
Dzięki @pankaj, szukałem rozwiązania w Kotlinie.
Akshay Kumar Zarówno
1
  1. Rozszerz klasę „LinearLayout” i przesłoń niezbędne funkcje
  2. Utwórz instancję powyższej klasy w swoim fragmencie lub działaniu
  3. Wywołaj „reclerView.smoothScrollToPosition (targetPosition)

CustomLinearLayout.kt:

class CustomLayoutManager(private val context: Context, layoutDirection: Int):
  LinearLayoutManager(context, layoutDirection, false) {

    companion object {
      // This determines how smooth the scrolling will be
      private
      const val MILLISECONDS_PER_INCH = 300f
    }

    override fun smoothScrollToPosition(recyclerView: RecyclerView, state: RecyclerView.State, position: Int) {

      val smoothScroller: LinearSmoothScroller = object: LinearSmoothScroller(context) {

        fun dp2px(dpValue: Float): Int {
          val scale = context.resources.displayMetrics.density
          return (dpValue * scale + 0.5f).toInt()
        }

        // change this and the return super type to "calculateDyToMakeVisible" if the layout direction is set to VERTICAL
        override fun calculateDxToMakeVisible(view: View ? , snapPreference : Int): Int {
          return super.calculateDxToMakeVisible(view, SNAP_TO_END) - dp2px(50f)
        }

        //This controls the direction in which smoothScroll looks for your view
        override fun computeScrollVectorForPosition(targetPosition: Int): PointF ? {
          return this @CustomLayoutManager.computeScrollVectorForPosition(targetPosition)
        }

        //This returns the milliseconds it takes to scroll one pixel.
        override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics): Float {
          return MILLISECONDS_PER_INCH / displayMetrics.densityDpi
        }
      }
      smoothScroller.targetPosition = position
      startSmoothScroll(smoothScroller)
    }
  }

Uwaga: Powyższy przykład jest ustawiony na kierunek HORIZONTAL, możesz przejść VERTICAL / HORIZONTAL podczas inicjalizacji.

Jeśli ustawisz kierunek na PIONOWY , powinieneś zmienić „ wyliczenieDxToMakeVisible ” na „ wyliczenieDyToMakeVisible ” (pamiętaj również o zwracanej wartości wywołania supertype)

Activity / Fragment.kt :

...
smoothScrollerLayoutManager = CustomLayoutManager(context, LinearLayoutManager.HORIZONTAL)
recyclerView.layoutManager = smoothScrollerLayoutManager
.
.
.
fun onClick() {
  // targetPosition passed from the adapter to activity/fragment
  recyclerView.smoothScrollToPosition(targetPosition)
}
Ali Akbar
źródło
0

Prawdopodobnie podejście @droidev jest poprawne, ale chcę po prostu opublikować coś nieco innego, co zasadniczo wykonuje tę samą pracę i nie wymaga rozszerzenia LayoutManager.

UWAGA - to zadziała dobrze, jeśli Twój element (ten, który chcesz przewinąć na górze listy) jest widoczny na ekranie i chcesz po prostu automatycznie przewinąć go na górę. Jest to przydatne, gdy ostatnia pozycja na liście ma jakąś akcję, która dodaje nowe pozycje na tej samej liście i chcesz skupić użytkownika na nowo dodanych pozycjach:

int recyclerViewTop = recyclerView.getTop();
int positionTop = recyclerView.findViewHolderForAdapterPosition(positionToScroll) != null ? recyclerView.findViewHolderForAdapterPosition(positionToScroll).itemView.getTop() : 200;
final int calcOffset = positionTop - recyclerViewTop; 
//then the actual scroll is gonna happen with (x offset = 0) and (y offset = calcOffset)
recyclerView.scrollBy(0, offset);

Pomysł jest prosty: 1. Musimy uzyskać górną współrzędną elementu widoku recyklingu; 2. Musimy uzyskać górną współrzędną elementu widoku, który chcemy przewinąć do góry; 3. Na koniec z obliczonym offsetem musimy zrobić

recyclerView.scrollBy(0, offset);

200 to tylko przykład zakodowanej na stałe wartości całkowitej, której możesz użyć, jeśli element podglądu nie istnieje, ponieważ jest to również możliwe.

Stoycho Andreev
źródło
0

Chcę pełniej zająć się kwestią czasu trwania przewijania , który, jeśli wybierzesz jakąkolwiek wcześniejszą odpowiedź, w rzeczywistości będzie się znacznie różnić (i niedopuszczalnie) w zależności od ilości przewijania niezbędnego do osiągnięcia pozycji docelowej z bieżącej pozycji.

Aby uzyskać jednolity czas przewijania, prędkość (w pikselach na milisekundę) musi uwzględniać rozmiar każdego pojedynczego elementu - a gdy elementy mają niestandardowe wymiary, dodaje się zupełnie nowy poziom złożoności.

Być może dlatego programiści RecyclerView wdrożyli zbyt trudny koszyk dla tego istotnego aspektu płynnego przewijania.

Zakładając, że chcesz pół-uniform czas przewijania i że lista zawiera pół-jednolite elementy a następnie trzeba będzie coś takiego.

/** Smoothly scroll to specified position allowing for interval specification. <br>
 * Note crude deceleration towards end of scroll
 * @param rv        Your RecyclerView
 * @param toPos     Position to scroll to
 * @param duration  Approximate desired duration of scroll (ms)
 * @throws IllegalArgumentException */
private static void smoothScroll(RecyclerView rv, int toPos, int duration) throws IllegalArgumentException {
    int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;     // See androidx.recyclerview.widget.LinearSmoothScroller
    int itemHeight = rv.getChildAt(0).getHeight();  // Height of first visible view! NB: ViewGroup method!
    itemHeight = itemHeight + 33;                   // Example pixel Adjustment for decoration?
    int fvPos = ((LinearLayoutManager)rv.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
    int i = Math.abs((fvPos - toPos) * itemHeight);
    if (i == 0) { i = (int) Math.abs(rv.getChildAt(0).getY()); }
    final int totalPix = i;                         // Best guess: Total number of pixels to scroll
    RecyclerView.SmoothScroller smoothScroller = new LinearSmoothScroller(rv.getContext()) {
        @Override protected int getVerticalSnapPreference() {
            return LinearSmoothScroller.SNAP_TO_START;
        }
        @Override protected int calculateTimeForScrolling(int dx) {
            int ms = (int) ( duration * dx / (float)totalPix );
            // Now double the interval for the last fling.
            if (dx < TARGET_SEEK_SCROLL_DISTANCE_PX ) { ms = ms*2; } // Crude deceleration!
            //lg(format("For dx=%d we allot %dms", dx, ms));
            return ms;
        }
    };
    //lg(format("Total pixels from = %d to %d = %d [ itemHeight=%dpix ]", fvPos, toPos, totalPix, itemHeight));
    smoothScroller.setTargetPosition(toPos);
    rv.getLayoutManager().startSmoothScroll(smoothScroller);
}

PS: Przeklinam dzień, w którym zacząłem bezkrytycznie konwertować ListView do RecyclerView .

Zły przegrany
źródło
-1

Możesz odwrócić swoją listę list.reverse()i ostatecznie zadzwonićRecylerView.scrollToPosition(0)

    list.reverse()
    layout = LinearLayoutManager(this,LinearLayoutManager.VERTICAL,true)  
    RecylerView.scrollToPosition(0)
Thanh Vu
źródło