web-dev-qa-db-fra.com

RecyclerView défilement horizontal au centre

J'essaie de créer une vue semblable à un carrousel à l'aide de RecyclerView. Je souhaite que l'élément s'aligne au milieu de l'écran lors du défilement, élément par élément. J'ai essayé d'utiliser recyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);

mais la vue défile toujours, j'ai aussi essayé d'implémenter ma propre logique en utilisant le listener 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 bien maintenant, mais pas l'inverse, qu'est-ce qui me manque ici?

71
Ahmed Awad

Avec LinearSnapHelper , c'est maintenant très facile.

Tout ce que vous devez faire est ceci:

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

Mettre à jour

Disponible depuis 25.1.0, PagerSnapHelper peut aider à obtenir un effet similaire à ViewPager. Utilisez-le comme vous utiliseriez le LinearSnapHelper.

Ancien solution de contournement:

Si vous souhaitez qu'il s'apparente au 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 ne fait que renvoyer la position à côté de l'élément en cours (centré) en fonction de la direction de la vitesse, quelle que soit sa magnitude.

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 ajouter ceci au build.gradle ou le mettre à jour.

compile "com.Android.support:recyclerview-v7:24.2.0"
125
razzledazzle

Mise à jour de Google I/O 2019:

ViewPager2 est ici !

Google récemment annoncé lors de la conférence "Quoi de neuf dans Android" (ou "Le Android keynote")), ils travaillent sur un nouveau ViewPager basé sur RecyclerView!

À partir des diapositives:

Comme ViewPager, mais en 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 changement de jeu de données

Vous pouvez vérifier la dernière version ici et les notes de version ici .

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


Nouvelle réponse (2016):

Vous pouvez maintenant simplement utiliser un SnapHelper .

Si vous souhaitez un comportement de capture aligné sur le centre similaire à ViewPager , utilisez un PagerSnapHelper :

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

Il existe également un LinearSnapHelper . Je l'ai essayé et si vous lancez avec énergie, alors il fait défiler 2 éléments avec 1 projection. Personnellement, je n'aimais pas ça, mais je décidais de mon côté - l'essayer ne prend que quelques secondes.


Réponse originale (2016):

Après plusieurs heures d'essayer 3 solutions différentes trouvées ici dans SO), j'ai finalement mis au point une solution imitant de très près le comportement observé dans un ViewPager.

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

Important: la largeur de mes éléments RecyclerView est exactement la même que celle de l'écran. Je n'ai pas essayé avec d'autres tailles. De plus, je l'utilise avec un LinearLayoutManager horizontal. Je pense que vous aurez besoin d’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!

56
Albert Vila Calvo

Si l'objectif est de faire en sorte que le RecyclerView imite le comportement de ViewPager, il 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 PagerSnapHelper, vous pouvez obtenir le comportement suivant: ViewPager

38
Boonya Kitpitak

Vous devez utiliser findFirstVisibleItemPosition pour aller dans la direction opposée. Et pour détecter la direction dans laquelle le balayage a été effectué, vous devez connaître la vitesse de défilement ou 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 substituez 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;
}
30
eDizzle

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 disposition à RecycledView et définit le décalage requis pour centrer les éléments. Tous mes articles ont la même largeur, donc un décalage constant est correct

2
anagaf

Ajoutez simplement padding et margin à recyclerView et recyclerView item:

article recyclerView:

<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 à px:

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

résultat:

enter image description here

1
Farzad

PagerSnapHelper ne fonctionne pas avec GridLayoutManager avec spanCount> 1, ma solution est la suivante:

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
    }
}
0
Wenqing MA