RecyclerView accrochage de défilement horizontal au centre

89

J'essaie de créer une vue de type carrousel ici à l'aide de RecyclerView, je veux que l'élément s'emboîte au milieu de l'écran lors du défilement, un élément à la fois. J'ai essayé d'utiliserrecyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);

mais la vue défile toujours en douceur, j'ai également essayé d'implémenter ma propre logique en utilisant un écouteur de défilement comme ceci:

recyclerView.setOnScrollListener(new OnScrollListener() {
                @Override
                public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                    super.onScrollStateChanged(recyclerView, newState);
                    Log.v("Offset ", recyclerView.getWidth() + "");
                    if (newState == 0) {
                        try {
                               recyclerView.smoothScrollToPosition(layoutManager.findLastVisibleItemPosition());
                                recyclerView.scrollBy(20,0);
                            if (layoutManager.findLastVisibleItemPosition() >= recyclerView.getAdapter().getItemCount() - 1) {
                                Beam refresh = new Beam();
                                refresh.execute(createUrl());
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }

Le balayage de droite à gauche fonctionne correctement maintenant, mais pas l'inverse, que me manque-t-il ici?

Ahmed Awad
la source

Réponses:

161

Avec LinearSnapHelper, c'est désormais très simple.

Tout ce que vous avez à faire est ceci:

SnapHelper helper = new LinearSnapHelper();
helper.attachToRecyclerView(recyclerView);

Mise à jour

Disponible depuis 25.1.0, PagerSnapHelperpeut aider à obtenir le ViewPagermême effet. Utilisez-le comme vous utiliseriez le LinearSnapHelper.

Ancienne solution de contournement:

Si vous souhaitez qu'il se comporte comme le ViewPager, essayez plutôt ceci:

LinearSnapHelper snapHelper = new LinearSnapHelper() {
    @Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
        View centerView = findSnapView(layoutManager);
        if (centerView == null) 
            return RecyclerView.NO_POSITION;

        int position = layoutManager.getPosition(centerView);
        int targetPosition = -1;
        if (layoutManager.canScrollHorizontally()) {
            if (velocityX < 0) {
                targetPosition = position - 1;
            } else {
                targetPosition = position + 1;
            }
        }

        if (layoutManager.canScrollVertically()) {
            if (velocityY < 0) {
                targetPosition = position - 1;
            } else {
                targetPosition = position + 1;
            }
        }

        final int firstItem = 0;
        final int lastItem = layoutManager.getItemCount() - 1;
        targetPosition = Math.min(lastItem, Math.max(targetPosition, firstItem));
        return targetPosition;
    }
};
snapHelper.attachToRecyclerView(recyclerView);

L'implémentation ci-dessus renvoie simplement la position à côté de l'élément actuel (centré) en fonction de la direction de la vitesse, quelle que soit l'amplitude.

La première est une solution de première partie incluse dans la bibliothèque de support version 24.2.0. Cela signifie que vous devez l'ajouter à votre module d'application build.gradleou le mettre à jour.

compile "com.android.support:recyclerview-v7:24.2.0"
tape-à-l'oeil
la source
Cela n'a pas fonctionné pour moi (mais la solution de @Albert Vila Calvo a fonctionné). L'avez-vous mis en œuvre? Sommes-nous censés en remplacer les méthodes? Je pense que la classe devrait faire tout le travail
prêt à l'emploi
Oui, d'où la réponse. Je n'ai cependant pas testé à fond. Pour être clair, je l'ai fait avec une horizontale LinearLayoutManageroù les vues étaient toutes de taille régulière. Rien d'autre que l'extrait ci-dessus n'est nécessaire.
razzledazzle
@galaxigirl excuses, j'ai compris ce que vous vouliez dire et j'ai fait quelques modifications en reconnaissant cela, veuillez faire un commentaire. Il s'agit d'une solution alternative avec le LinearSnapHelper.
razzledazzle
Cela a fonctionné parfaitement pour moi. @galaxigirl outrepasser cette méthode aide en ce que l'assistant d'accrochage linéaire s'alignera autrement sur l'élément le plus proche lorsque le défilement est en cours de stabilisation, pas exactement sur l'élément suivant / précédent. Merci beaucoup pour cela, razzledazzle
AA_PV
LinearSnapHelper était la solution initiale qui a fonctionné pour moi, mais il semble que PagerSnapHelper a donné au balayage une meilleure animation.
LeonardoSibela
76

Mise à jour Google I / O 2019

ViewPager2 est là!

Google vient d'annoncer lors de la conférence «Quoi de neuf dans Android» (alias «La keynote Android») qu'ils travaillaient sur un nouveau ViewPager basé sur RecyclerView!

À partir des diapositives:

Comme ViewPager, mais mieux

  • Migration facile depuis ViewPager
  • Basé sur RecyclerView
  • Prise en charge du mode de droite à gauche
  • Permet la pagination verticale
  • Amélioration des notifications de modification des ensembles de données

Vous pouvez consulter la dernière version ici et les notes de version ici . Il existe également un échantillon officiel .

Opinion personnelle: je pense que c'est un ajout vraiment nécessaire. J'ai récemment eu beaucoup de problèmes avec l' PagerSnapHelper oscillation gauche droite indéfiniment - voir le ticket que j'ai ouvert.


Nouvelle réponse (2016)

Vous pouvez maintenant simplement utiliser un SnapHelper .

Si vous voulez un comportement d' accrochage centré similaire à ViewPager, utilisez PagerSnapHelper :

SnapHelper snapHelper = new PagerSnapHelper();
snapHelper.attachToRecyclerView(recyclerView);

Il existe également un LinearSnapHelper . Je l'ai essayé et si vous vous lancez avec de l'énergie, il fait défiler 2 éléments avec 1 jet. Personnellement, je n'ai pas aimé, mais décidez par vous-même - l'essayer ne prend que quelques secondes.


Réponse originale (2016)

Après de nombreuses heures à essayer 3 solutions différentes trouvées ici dans SO, j'ai finalement construit une solution qui imite de très près le comportement trouvé dans un fichier ViewPager.

La solution est basée sur la solution @eDizzle , que je pense avoir suffisamment améliorée pour dire qu'elle fonctionne presque comme un ViewPager.

Important: la RecyclerViewlargeur de mes éléments est exactement la même que celle de l'écran. Je n'ai pas essayé avec d'autres tailles. Aussi je l'utilise avec un horizontal LinearLayoutManager. Je pense que vous devrez adapter le code si vous voulez un défilement vertical.

Ici vous avez le code:

public class SnappyRecyclerView extends RecyclerView {

    // Use it with a horizontal LinearLayoutManager
    // Based on https://stackoverflow.com/a/29171652/4034572

    public SnappyRecyclerView(Context context) {
        super(context);
    }

    public SnappyRecyclerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public SnappyRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public boolean fling(int velocityX, int velocityY) {

        LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();

        int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;

        // views on the screen
        int lastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition();
        View lastView = linearLayoutManager.findViewByPosition(lastVisibleItemPosition);
        int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
        View firstView = linearLayoutManager.findViewByPosition(firstVisibleItemPosition);

        // distance we need to scroll
        int leftMargin = (screenWidth - lastView.getWidth()) / 2;
        int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth();
        int leftEdge = lastView.getLeft();
        int rightEdge = firstView.getRight();
        int scrollDistanceLeft = leftEdge - leftMargin;
        int scrollDistanceRight = rightMargin - rightEdge;

        if (Math.abs(velocityX) < 1000) {
            // The fling is slow -> stay at the current page if we are less than half through,
            // or go to the next page if more than half through

            if (leftEdge > screenWidth / 2) {
                // go to next page
                smoothScrollBy(-scrollDistanceRight, 0);
            } else if (rightEdge < screenWidth / 2) {
                // go to next page
                smoothScrollBy(scrollDistanceLeft, 0);
            } else {
                // stay at current page
                if (velocityX > 0) {
                    smoothScrollBy(-scrollDistanceRight, 0);
                } else {
                    smoothScrollBy(scrollDistanceLeft, 0);
                }
            }
            return true;

        } else {
            // The fling is fast -> go to next page

            if (velocityX > 0) {
                smoothScrollBy(scrollDistanceLeft, 0);
            } else {
                smoothScrollBy(-scrollDistanceRight, 0);
            }
            return true;

        }

    }

    @Override
    public void onScrollStateChanged(int state) {
        super.onScrollStateChanged(state);

        // If you tap on the phone while the RecyclerView is scrolling it will stop in the middle.
        // This code fixes this. This code is not strictly necessary but it improves the behaviour.

        if (state == SCROLL_STATE_IDLE) {
            LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();

            int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;

            // views on the screen
            int lastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition();
            View lastView = linearLayoutManager.findViewByPosition(lastVisibleItemPosition);
            int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
            View firstView = linearLayoutManager.findViewByPosition(firstVisibleItemPosition);

            // distance we need to scroll
            int leftMargin = (screenWidth - lastView.getWidth()) / 2;
            int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth();
            int leftEdge = lastView.getLeft();
            int rightEdge = firstView.getRight();
            int scrollDistanceLeft = leftEdge - leftMargin;
            int scrollDistanceRight = rightMargin - rightEdge;

            if (leftEdge > screenWidth / 2) {
                smoothScrollBy(-scrollDistanceRight, 0);
            } else if (rightEdge < screenWidth / 2) {
                smoothScrollBy(scrollDistanceLeft, 0);
            }
        }
    }

}

Prendre plaisir!

Albert Vila Calvo
la source
Belle capture le onScrollStateChanged (); sinon, un petit bogue est présent si nous glissons lentement le RecyclerView. Merci!
Mauricio Sartori
Pour prendre en charge les marges (android: layout_marginStart = "16dp") j'ai essayé avec int screenwidth = getWidth (); et cela semble fonctionner.
eigil
AWESOOOMMEEEEEEEEE
raditya gumay
1
@galaxigirl non, je n'ai pas encore essayé LinearSnapHelper. Je viens de mettre à jour la réponse pour que les gens connaissent la solution officielle. Ma solution fonctionne sans aucun problème sur mon application.
Albert Vila Calvo
1
@AlbertVilaCalvo J'ai essayé LinearSnapHelper. LinearSnapHelper fonctionne avec un Scroller. Il défile et s'enclenche en fonction de la gravité et de la vitesse de projection. Plus la vitesse de défilement des pages augmente. Je ne le recommanderais pas pour un comportement similaire à ViewPager.
mipreamble
46

Si le but est de faire RecyclerViewmimer le comportement, ViewPageril y a une approche assez simple

RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);

LinearLayoutManager layoutManager = new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false);
SnapHelper snapHelper = new PagerSnapHelper();
recyclerView.setLayoutManager(layoutManager);
snapHelper.attachToRecyclerView(mRecyclerView);

En utilisant, PagerSnapHelpervous pouvez obtenir le comportement commeViewPager

Boonya Kitpitak
la source
4
J'utilise à la LinearSnapHelperplace de PagerSnapHelper et cela fonctionne pour moi
fanjavaid
1
Oui, il n'y a pas d'effet instantané dansLinearSnapHelper
Boonya Kitpitak
1
cela colle l'élément sur la vue permet le scrolable
Sam
@BoonyaKitpitak, j'ai essayé LinearSnapHelper, et il s'aligne sur un élément du milieu. PagerSnapHelperempêche de faire défiler facilement (au moins, une liste d'images).
CoolMind
@BoonyaKitpitak pouvez-vous me dire comment faire cela, un élément au centre est entièrement visible et les éléments latéraux sont à moitié visibles?
Rucha Bhatt Joshi
30

Vous devez utiliser findFirstVisibleItemPosition pour aller dans la direction opposée. Et pour détecter la direction dans laquelle le balayage était, vous devrez obtenir soit la vitesse de jet, soit le changement de x. J'ai abordé ce problème sous un angle légèrement différent de celui que vous avez.

Créez une nouvelle classe qui étend la classe RecyclerView, puis remplacez la méthode fling de RecyclerView comme suit:

@Override
public boolean fling(int velocityX, int velocityY) {
    LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();

//these four variables identify the views you see on screen.
    int lastVisibleView = linearLayoutManager.findLastVisibleItemPosition();
    int firstVisibleView = linearLayoutManager.findFirstVisibleItemPosition();
    View firstView = linearLayoutManager.findViewByPosition(firstVisibleView);
    View lastView = linearLayoutManager.findViewByPosition(lastVisibleView);

//these variables get the distance you need to scroll in order to center your views.
//my views have variable sizes, so I need to calculate side margins separately.     
//note the subtle difference in how right and left margins are calculated, as well as
//the resulting scroll distances.
    int leftMargin = (screenWidth - lastView.getWidth()) / 2;
    int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth();
    int leftEdge = lastView.getLeft();
    int rightEdge = firstView.getRight();
    int scrollDistanceLeft = leftEdge - leftMargin;
    int scrollDistanceRight = rightMargin - rightEdge;

//if(user swipes to the left) 
    if(velocityX > 0) smoothScrollBy(scrollDistanceLeft, 0);
    else smoothScrollBy(-scrollDistanceRight, 0);

    return true;
}
eDizzle
la source
Où screenWidth est-il défini?
ltsai
@Itsai: Oups! Bonne question. C'est une variable que j'ai définie ailleurs dans la classe. Il s'agit de la largeur de l'écran de l'appareil en pixels, et non en pixels de densité, car la méthode smoothScrollBy nécessite le nombre de pixels sur lesquels vous voulez qu'il défile.
eDizzle
3
@ltsai Vous pouvez changer pour getWidth () (recyclerview.getWidth ()) au lieu de screenWidth. screenWidth ne fonctionne pas correctement lorsque RecyclerView est plus petit que la largeur de l'écran.
VAdaihiep
J'ai essayé d'étendre RecyclerView mais j'obtiens "Erreur de gonflage de la classe RecyclerView personnalisée". Pouvez-vous m'aider? Merci
@bEtTyBarnes: Vous voudrez peut-être rechercher cela dans un autre fil de questions. C'est assez hors de portée de la question initiale ici.
eDizzle
6

Ajoutez simplement paddinget marginà recyclerViewet recyclerView item:

recyclerAfficher l'article:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/parentLayout"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_marginLeft="8dp" <!-- here -->
    android:layout_marginRight="8dp" <!-- here  -->
    android:layout_width="match_parent"
    android:layout_height="200dp">

   <!-- child views -->

</RelativeLayout>

recyclerView:

<androidx.recyclerview.widget.RecyclerView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingLeft="8dp" <!-- here -->
    android:paddingRight="8dp" <!-- here -->
    android:clipToPadding="false" <!-- important!-->
    android:scrollbars="none" />

et définissez PagerSnapHelper:

int displayWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
parentLayout.getLayoutParams().width = displayWidth - Utils.dpToPx(16) * 4;
SnapHelper snapHelper = new PagerSnapHelper();
snapHelper.attachToRecyclerView(recyclerView);

dp en px:

public static int dpToPx(int dp) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
}

résultat:

entrez la description de l'image ici

Farzad
la source
Existe-t-il un moyen d'obtenir un remplissage variable entre les éléments? comme si la première et la dernière carte n'avaient pas besoin d'être centrées pour montrer davantage les cartes suivantes / précédentes par rapport aux cartes intermédiaires étant centrées?
RamPrasadBismil
2

Ma solution:

/**
 * Horizontal linear layout manager whose smoothScrollToPosition() centers
 * on the target item
 */
class ItemLayoutManager extends LinearLayoutManager {

    private int centeredItemOffset;

    public ItemLayoutManager(Context context) {
        super(context, LinearLayoutManager.HORIZONTAL, false);
    }

    @Override
    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
        LinearSmoothScroller linearSmoothScroller = new Scroller(recyclerView.getContext());
        linearSmoothScroller.setTargetPosition(position);
        startSmoothScroll(linearSmoothScroller);
    }

    public void setCenteredItemOffset(int centeredItemOffset) {
        this.centeredItemOffset = centeredItemOffset;
    }

    /**
     * ********** Inner Classes **********
     */

    private class Scroller extends LinearSmoothScroller {

        public Scroller(Context context) {
            super(context);
        }

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

        @Override
        public int calculateDxToMakeVisible(View view, int snapPreference) {
            return super.calculateDxToMakeVisible(view, SNAP_TO_START) + centeredItemOffset;
        }
    }
}

Je passe ce gestionnaire de mise en page à RecycledView et définit le décalage requis pour centrer les éléments. Tous mes articles ont la même largeur, donc le décalage constant est correct

anagaf
la source
0

PagerSnapHelperne fonctionne pas GridLayoutManageravec spanCount> 1, donc ma solution dans cette circonstance est:

class GridPagerSnapHelper : PagerSnapHelper() {
    override fun findTargetSnapPosition(layoutManager: RecyclerView.LayoutManager?, velocityX: Int, velocityY: Int): Int {
        val forwardDirection = if (layoutManager?.canScrollHorizontally() == true) {
            velocityX > 0
        } else {
            velocityY > 0
        }
        val centerPosition = super.findTargetSnapPosition(layoutManager, velocityX, velocityY)
        return centerPosition +
            if (forwardDirection) (layoutManager as GridLayoutManager).spanCount - 1 else 0
    }
}
Wenqing MA
la source