RecyclerView - Comment faire défiler en douceur vers le haut de l'élément sur une certaine position?

125

Sur un RecyclerView, je suis capable de faire défiler soudainement vers le haut d'un élément sélectionné en utilisant:

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

Cependant, cela déplace brusquement l'élément vers la position supérieure. Je souhaite passer en haut d'un élément en douceur .

J'ai aussi essayé:

recyclerView.smoothScrollToPosition(position);

mais cela ne fonctionne pas bien car il ne déplace pas l'élément vers la position sélectionnée vers le haut. Il fait simplement défiler la liste jusqu'à ce que l'élément sur la position soit visible.

Tiago
la source

Réponses:

225

RecyclerViewest conçu pour être extensible, il n'est donc pas nécessaire de sous- classer le LayoutManager(comme le suggère droidev ) juste pour effectuer le défilement.

Au lieu de cela, créez simplement un SmoothScrolleravec la préférence SNAP_TO_START:

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

Vous définissez maintenant la position à laquelle vous souhaitez faire défiler:

smoothScroller.setTargetPosition(position);

et passez ce SmoothScroller au LayoutManager:

layoutManager.startSmoothScroll(smoothScroller);
Paul Woitaschek
la source
9
Merci pour cette solution plus courte. Juste deux autres choses que je devais prendre en compte dans mon implémentation: Comme j'ai une vue à défilement horizontal, je devais définir protected int getHorizontalSnapPreference() { return LinearSmoothScroller.SNAP_TO_START; }. De plus, j'ai dû implémenter la méthode abstraite public PointF computeScrollVectorForPosition(int targetPosition) { return layoutManager.computeScrollVectorForPosition(targetPosition); }.
AustrianDude
2
RecyclerView peut être conçu pour être extensible, mais une chose simple comme celle-ci semble juste paresseuse de manquer. Bonne réponse tho! Je vous remercie.
Michael
8
Existe-t-il un moyen de le laisser casser pour commencer, mais avec un décalage de X dp?
Mark Buikema
5
est-il possible de ralentir sa vitesse?
Alireza Noorali
3
Se demandant également si la vitesse à laquelle le défilement se produit peut être modifiée (plus lente) @AlirezaNoorali
vikzilla
112

pour cela, vous devez créer un LayoutManager personnalisé

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

utilisez ceci pour votre RecyclerView et appelez smoothScrollToPosition.

exemple :

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

cela fera défiler vers le haut de l'élément RecyclerView de la position spécifiée.

J'espère que cela t'aides.

droidev
la source
J'ai eu du mal à implémenter le LayoutManager suggéré. Cette réponse ici a fonctionné beaucoup plus facilement pour moi: stackoverflow.com/questions/28025425/…
arberg
3
Seule réponse utile après des heures de douleur, cela fonctionne comme prévu!
wblaschko
1
@droidev J'ai utilisé votre exemple avec un défilement fluide et généralement SNAP_TO_START est ce dont j'ai besoin, mais cela fonctionne avec certains problèmes pour moi. Parfois, le défilement s'arrête 1, 2, 3 ou 4 positions plus tôt que je passe smoothScrollToPosition. Pourquoi ce problème peut-il se produire? Merci.
Natasha
pouvez-vous nous communiquer votre code d'une manière ou d'une autre? Je suis presque sûr que c'est votre problème de code.
droidev
1
C'est la bonne réponse malgré la réponse acceptée
Alessio
26

C'est une fonction d'extension que j'ai écrite dans Kotlin à utiliser avec le RecyclerView(basé sur la réponse @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)
}

Utilisez-le comme ceci:

myRecyclerView.smoothSnapToPosition(itemPosition)
vovahost
la source
Cela marche! Le problème avec le défilement fluide est que vous avez de grandes listes et que le défilement vers le bas ou le haut prend beaucoup de temps
portfoliobuilder
@vovahost comment puis-je centrer la position souhaitée?
ysfcyln le
@ysfcyln Vérifiez cette réponse stackoverflow.com/a/39654328/1502079
vovahost le
12

On peut essayer comme ça

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

Remplacer la fonction CalculateDyToMakeVisible / CalculateDxToMakeVisible dans LinearSmoothScroller pour implémenter la position de décalage Y / X

override fun calculateDyToMakeVisible(view: View, snapPreference: Int): Int {
    return super.calculateDyToMakeVisible(view, snapPreference) - ConvertUtils.dp2px(10f)
}
user2526517
la source
C'est ce que je cherchais. Merci de partager ceci pour la mise en œuvre de la position de décalage de x / y :)
Shan Xeeshi
3

Le moyen le plus simple que j'ai trouvé pour faire défiler a RecyclerViewest le suivant:

// 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
la source
2
Cela ne défilera pas jusqu'à une position si cette position est déjà visible. Il fait défiler le minimum nécessaire pour rendre la position visible. Ainsi pourquoi nous avons besoin de celui avec un décalage spécifié.
TatiOverflow
3

J'ai utilisé comme ça:

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

Merci, @droidev pour la solution. Si quelqu'un cherche une solution Kotlin, référez-vous à ceci:

    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
la source
1
Merci @pankaj, cherchait une solution à Kotlin.
Akshay Kumar Both
1
  1. Étendre la classe "LinearLayout" et remplacer les fonctions nécessaires
  2. Créez une instance de la classe ci-dessus dans votre fragment ou activité
  3. Appelez "recyclerView.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)
    }
  }

Remarque: l' exemple ci-dessus est défini sur la direction HORIZONTALE, vous pouvez passer VERTICAL / HORIZONTAL lors de l'initialisation.

Si vous définissez la direction sur VERTICAL, vous devez modifier le paramètre " CalculateDxToMakeVisible " en " CalculateDyToMakeVisible " ( faites également attention à la valeur de retour d'appel du supertype)

Activité / 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
la source
0

L'approche @droidev est probablement la bonne, mais je veux juste publier quelque chose d'un peu différent, qui fait essentiellement le même travail et ne nécessite pas l'extension du LayoutManager.

REMARQUE ici - cela fonctionnera bien si votre élément (celui que vous souhaitez faire défiler en haut de la liste) est visible à l'écran et que vous souhaitez simplement le faire défiler automatiquement vers le haut. Cela est utile lorsque le dernier élément de votre liste comporte une action, qui ajoute de nouveaux éléments dans la même liste et que vous souhaitez concentrer l'utilisateur sur les nouveaux éléments ajoutés:

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

L'idée est simple: 1. Nous devons obtenir la coordonnée supérieure de l'élément recyclerview; 2. Nous devons obtenir la coordonnée supérieure de l'élément de vue que nous voulons faire défiler vers le haut; 3. À la fin du décalage calculé, nous devons faire

recyclerView.scrollBy(0, offset);

200 n'est qu'un exemple de valeur entière codée en dur que vous pouvez utiliser si l'élément de visualisation n'existe pas, car cela est également possible.

Stoycho Andreev
la source
0

Je souhaite aborder plus en détail la question de la durée du défilement , qui, si vous choisissez une réponse antérieure, variera en fait considérablement (et de manière inacceptable) en fonction de la quantité de défilement nécessaire pour atteindre la position cible à partir de la position actuelle.

Pour obtenir une durée de défilement uniforme, la vitesse (pixels par milliseconde) doit tenir compte de la taille de chaque élément individuel - et lorsque les éléments sont de dimension non standard, un tout nouveau niveau de complexité est ajouté.

C'est peut-être pourquoi les développeurs de RecyclerView ont déployé le panier trop dur pour cet aspect vital du défilement fluide.

En supposant que vous vouliez une durée de défilement semi-uniforme et que votre liste contienne des éléments semi-uniformes , vous aurez besoin de quelque chose comme ça.

/** 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: Je maudis le jour où j'ai commencé à convertir sans discernement ListView en RecyclerView .

Mauvais perdant
la source
-1

Vous pouvez inverser votre liste list.reverse()et enfin appelerRecylerView.scrollToPosition(0)

    list.reverse()
    layout = LinearLayoutManager(this,LinearLayoutManager.VERTICAL,true)  
    RecylerView.scrollToPosition(0)
Thanh Vu
la source