RecyclerView - Wie kann man an einer bestimmten Position sanft zum Anfang des Elements scrollen?

125

In einer RecyclerView kann ich plötzlich zum Anfang eines ausgewählten Elements scrollen, indem ich Folgendes verwende:

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

Dadurch wird der Gegenstand jedoch abrupt in die oberste Position gebracht. Ich möchte reibungslos an die Spitze eines Elements gelangen .

Ich habe auch versucht:

recyclerView.smoothScrollToPosition(position);

Es funktioniert jedoch nicht gut, da das Objekt nicht an die oben ausgewählte Position verschoben wird. Es wird lediglich ein Bildlauf durch die Liste durchgeführt, bis das Element an der Position sichtbar ist.

Tiago
quelle

Antworten:

223

RecyclerViewist erweiterbar, so dass es nicht erforderlich ist, die Unterklasse LayoutManager(wie von droidev vorgeschlagen ) zu unterteilen, nur um das Scrollen durchzuführen.

Erstellen Sie stattdessen einfach ein SmoothScrollermit der Einstellung SNAP_TO_START:

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

Jetzt legen Sie die Position fest, zu der Sie scrollen möchten:

smoothScroller.setTargetPosition(position);

und übergeben Sie diesen SmoothScroller an den LayoutManager:

layoutManager.startSmoothScroll(smoothScroller);
Paul Woitaschek
quelle
9
Vielen Dank für diese kürzere Lösung. Nur zwei weitere Dinge, die ich bei meiner Implementierung berücksichtigen musste: Da ich eine horizontale Bildlaufansicht habe, musste ich festlegen protected int getHorizontalSnapPreference() { return LinearSmoothScroller.SNAP_TO_START; }. Außerdem musste ich die abstrakte Methode implementieren public PointF computeScrollVectorForPosition(int targetPosition) { return layoutManager.computeScrollVectorForPosition(targetPosition); }.
AustrianDude
2
RecyclerView ist möglicherweise erweiterbar, aber eine einfache Sache wie diese fühlt sich einfach faul an, wenn sie fehlt. Gute Antwort tho! Danke dir.
Michael
8
Gibt es eine Möglichkeit, es zum Starten einrasten zu lassen, jedoch mit einem Versatz von X dp?
Mark Buikema
2
@Alessio genau, dies wird RecyclerView Standard SmoothScrollToPosition Funktionalität brechen
Droidev
4
ist es möglich, seine Geschwindigkeit zu verlangsamen?
Alireza Noorali
112

Dazu müssen Sie einen benutzerdefinierten LayoutManager erstellen

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;
        }
    }
}

Verwenden Sie dies für Ihre RecyclerView und rufen SieoothScrollToPosition auf.

Beispiel:

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

Dadurch wird zum RecyclerView-Element der angegebenen Position gescrollt.

hoffe das hilft.

droidev
quelle
Ich hatte Probleme bei der Implementierung des vorgeschlagenen LayoutManager. Diese Antwort hier funktionierte viel einfacher für mich: stackoverflow.com/questions/28025425/…
Arberg
3
Nur nützliche Antwort nach stundenlangen Schmerzen, dies funktioniert wie geplant!
wblaschko
1
@droidev Ich habe Ihr Beispiel mit reibungslosem Scrollen verwendet und im Allgemeinen ist SNAP_TO_START das, was ich brauche, aber es funktioniert mit einigen Problemen für mich. Manchmal stoppt das Scrollen 1,2,3 oder 4 Positionen früher, an die ich gehe smoothScrollToPosition. Warum kann dieses Problem auftreten? Vielen Dank.
Natasha
Kannst du uns irgendwie deinen Code erreichen? Ich bin mir ziemlich sicher, dass es Ihr Codeproblem ist.
Droidev
1
Dies ist die richtige Antwort trotz der akzeptierten
Alessio
24

Dies ist eine Erweiterungsfunktion, die ich in Kotlin geschrieben habe , um sie mit der RecyclerView(basierend auf der Antwort von @Paul Woitaschek) zu verwenden:

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)
}

Verwenden Sie es so:

myRecyclerView.smoothSnapToPosition(itemPosition)
vovahost
quelle
Das funktioniert! Das Problem beim reibungslosen Scrollen ist, wenn Sie große Listen haben und das Scrollen nach unten oder oben lange, lange dauert
portfoliobuilder
@vovahost Wie kann ich die gewünschte Position zentrieren?
ysfcyln
@ysfcyln diese prüfen stackoverflow.com/a/39654328/1502079 Antwort
vovahost
12

Wir können es so versuchen

    recyclerView.getLayoutManager().smoothScrollToPosition(recyclerView,new RecyclerView.State(), recyclerView.getAdapter().getItemCount());
Supto
quelle
5

Überschreiben Sie die Funktion berechneDyToMakeVisible / berechneDxToMakeVisible in LinearSmoothScroller, um die versetzte Y / X-Position zu implementieren

override fun calculateDyToMakeVisible(view: View, snapPreference: Int): Int {
    return super.calculateDyToMakeVisible(view, snapPreference) - ConvertUtils.dp2px(10f)
}
user2526517
quelle
Das habe ich gesucht. Vielen Dank, dass Sie dies für die Implementierung der Versatzposition von x / y geteilt haben :)
Shan Xeeshi
3

Der einfachste Weg, einen Bildlauf durchzuführen, RecyclerViewist folgender:

// 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
quelle
2
Dadurch wird nicht zu einer Position gescrollt, wenn diese Position bereits sichtbar ist. Es wird die geringste Menge gescrollt, die erforderlich ist, um die Position sichtbar zu machen. Deshalb brauchen wir den mit dem angegebenen Versatz.
TatiOverflow
3

Ich benutzte so:

recyclerView.getLayoutManager().smoothScrollToPosition(recyclerView, new RecyclerView.State(), 5);
Criss
quelle
2

Danke, @droidev für die Lösung. Wenn jemand nach einer Kotlin-Lösung sucht, beziehen Sie sich auf Folgendes:

    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
quelle
1
Danke @pankaj, suchte nach einer Lösung in Kotlin.
Akshay Kumar Beide
1
  1. Erweitern Sie die Klasse "LinearLayout" und überschreiben Sie die erforderlichen Funktionen
  2. Erstellen Sie eine Instanz der oben genannten Klasse in Ihrem Fragment oder Ihrer Aktivität
  3. Rufen Sie "recyclerView.smoothScrollToPosition (targetPosition)" auf.

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)
    }
  }

Hinweis: Das obige Beispiel ist auf die Richtung HORIZONTAL eingestellt. Sie können während der Initialisierung VERTICAL / HORIZONTAL übergeben.

Wenn Sie die Richtung auf VERTICAL setzen , sollten Sie " berechneDxToMakeVisible " in " berechneDyToMakeVisible " ändern (beachten Sie auch den Rückgabewert des Supertyps).

Aktivität / 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
quelle
0

Wahrscheinlich ist der @ droidev-Ansatz der richtige, aber ich möchte nur etwas anderes veröffentlichen, das im Grunde den gleichen Job macht und keine Erweiterung des LayoutManager erfordert.

Ein HINWEIS hier - dies funktioniert gut, wenn Ihr Element (das, das Sie oben in der Liste scrollen möchten) auf dem Bildschirm angezeigt wird und Sie es nur automatisch nach oben scrollen möchten. Dies ist nützlich, wenn das letzte Element in Ihrer Liste eine Aktion enthält, mit der neue Elemente in derselben Liste hinzugefügt werden und Sie den Benutzer auf die neu hinzugefügten Elemente konzentrieren möchten:

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);

Die Idee ist einfach: 1. Wir müssen die oberste Koordinate des recyclerview-Elements ermitteln. 2. Wir müssen die obere Koordinate des Ansichtselements ermitteln, das nach oben gescrollt werden soll. 3. Am Ende mit dem berechneten Offset müssen wir tun

recyclerView.scrollBy(0, offset);

200 ist nur ein Beispiel für einen fest codierten Ganzzahlwert, den Sie verwenden können, wenn das Viewholder-Element nicht vorhanden ist, da dies ebenfalls möglich ist.

Stoycho Andreev
quelle
0

Ich möchte mehr vollständig das Problem der Adresse Scroll Dauer , die, sollten Sie eine frühere Antwort wählen, wird in der Tat variiert dramatisch (und unakzeptabel) entsprechend der erforderlichen Menge des Scrollens der Zielposition von der aktuellen Position zu erreichen.

Um eine einheitliche Bildlaufdauer zu erhalten, muss die Geschwindigkeit (Pixel pro Millisekunde) die Größe jedes einzelnen Elements berücksichtigen - und wenn die Elemente nicht standardmäßige Abmessungen haben, wird eine völlig neue Komplexität hinzugefügt.

Dies könnte der Grund sein, warum die RecyclerView- Entwickler den zu harten Korb für diesen wichtigen Aspekt des reibungslosen Bildlaufs bereitgestellt haben .

Unter der Annahme , dass Sie einen wollen halb einheitliche Scroll - Dauer, und dass Ihre Liste enthält halb einheitliche Elemente dann werden Sie so etwas wie dieses benötigen.

/** 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: Ich verfluche den Tag, an dem ich angefangen habe , ListView wahllos in RecyclerView zu konvertieren .

Schlechter Verlierer
quelle