RecyclerView scorrimento orizzontale a scatto al centro


89

Sto cercando di creare una vista simile a una giostra qui usando RecyclerView, voglio che l'elemento si agganci al centro dello schermo durante lo scorrimento, un elemento alla volta. Ho provato a usarerecyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);

ma la vista continua a scorrere senza problemi, ho anche provato a implementare la mia logica usando lo scroll listener in questo modo:

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

Lo scorrimento da destra a sinistra funziona bene ora, ma non viceversa, cosa mi manca qui?

Risposte:


165

Con LinearSnapHelper, ora è molto semplice.

Tutto quello che devi fare è questo:

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

Aggiornare

Disponibile dalla 25.1.0, PagerSnapHelperpuò aiutare a ottenere ViewPagerun effetto simile. Usalo come faresti con il LinearSnapHelper.

Vecchia soluzione alternativa:

Se desideri che si comporti in modo simile al ViewPager, prova questo invece:

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'implementazione di cui sopra restituisce semplicemente la posizione accanto all'elemento corrente (centrato) in base alla direzione della velocità, indipendentemente dall'entità.

La prima è una soluzione di prima parte inclusa nella Support Library versione 24.2.0. Significa che devi aggiungerlo al modulo della tua app build.gradleo aggiornarlo.

compile "com.android.support:recyclerview-v7:24.2.0"

Questo non ha funzionato per me (ma la soluzione di @Albert Vila Calvo ha funzionato). L'hai implementato? Dovremmo sovrascrivere i metodi da esso? Penso che la classe dovrebbe fare tutto il lavoro fuori dagli schemi
galaxigirl

Sì, da qui la risposta. Tuttavia non ho testato a fondo. Per essere chiari, l'ho fatto con un piano orizzontale in LinearLayoutManagercui le viste erano tutte di dimensioni regolari. È necessario nient'altro che lo snippet sopra.
razzledazzle

@galaxigirl mi scuso, ho capito cosa intendevi e ho apportato alcune modifiche riconoscendolo, per favore commenta. Questa è una soluzione alternativa con LinearSnapHelper.
Razzledazzle

Questo ha funzionato perfettamente per me. @galaxigirl l'override di questo metodo aiuta in quanto l'helper snap lineare si aggancerà altrimenti all'elemento più vicino quando lo scorrimento si sta stabilizzando, non esattamente solo l'elemento successivo / precedente. Grazie mille per questo, razzledazzle
AA_PV

LinearSnapHelper era la soluzione iniziale che ha funzionato per me, ma sembra che PagerSnapHelper abbia dato allo scorrimento un'animazione migliore.
LeonardoSibela

77

Aggiornamento di Google I / O 2019

ViewPager2 è qui!

Google ha appena annunciato al discorso "Novità di Android" (ovvero "Il keynote di Android") che stanno lavorando a un nuovo ViewPager basato su RecyclerView!

Dalle diapositive:

Come ViewPager, ma migliore

  • Facile migrazione da ViewPager
  • Basato su RecyclerView
  • Supporto della modalità da destra a sinistra
  • Consente il paging verticale
  • Notifiche di modifica del set di dati migliorate

Puoi controllare l'ultima versione qui e le note di rilascio qui . C'è anche un campione ufficiale .

Opinione personale: penso che questa sia un'aggiunta davvero necessaria. Recentemente ho avuto molti problemi con l' PagerSnapHelper oscillazione sinistra destra indefinitamente - vedi il biglietto che ho aperto.


Nuova risposta (2016)

Ora puoi semplicemente usare uno SnapHelper .

Se desideri un comportamento di snap allineato al centro simile a ViewPager, usa PagerSnapHelper :

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

C'è anche un LinearSnapHelper . L'ho provato e se lanci con energia, scorre 2 elementi con 1 avventura. Personalmente non mi è piaciuto, ma decidi da solo: provarlo richiede solo pochi secondi.


Risposta originale (2016)

Dopo molte ore di tentativi di 3 diverse soluzioni trovate qui in SO, ho finalmente creato una soluzione che imita molto da vicino il comportamento trovato in un file ViewPager.

La soluzione si basa sulla soluzione @eDizzle , che credo di aver migliorato abbastanza da dire che funziona quasi come un file ViewPager.

Importante: la RecyclerViewlarghezza dei miei articoli è esattamente la stessa dello schermo. Non ho provato con altre dimensioni. Anche io lo uso con un orizzontale LinearLayoutManager. Penso che dovrai adattare il codice se vuoi lo scorrimento verticale.

Ecco il codice:

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

}

Godere!


Bella cattura onScrollStateChanged (); in caso contrario, è presente un piccolo bug se si scorre lentamente RecyclerView. Grazie!
Mauricio Sartori

Per supportare i margini (android: layout_marginStart = "16dp") ho provato con int screenwidth = getWidth (); e sembra funzionare.
eigil

AWESOOOMMEEEEEEEEE
raditya gumay

1
@galaxigirl no, non ho ancora provato LinearSnapHelper. Ho appena aggiornato la risposta in modo che le persone conoscano la soluzione ufficiale. La mia soluzione funziona senza problemi sulla mia app.
Albert Vila Calvo

1
@AlbertVilaCalvo Ho provato LinearSnapHelper. LinearSnapHelper funziona con uno scroller. Scorre e scatta in base alla gravità e alla velocità di lancio. Maggiore è la velocità di scorrimento di più pagine. Non lo consiglierei per un comportamento simile a ViewPager.
mipreamble

47

Se l'obiettivo è quello di RecyclerViewimitare il comportamento di ViewPagerlì è un approccio abbastanza facile

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

Usando PagerSnapHelperpuoi ottenere il comportamento comeViewPager


4
Sto usando LinearSnapHelperinvece di PagerSnapHelper e funziona per me
fanjavaid

1
Sì, non c'è effetto snap inLinearSnapHelper
Boonya Kitpitak

1
questo attacca l'elemento sulla visualizzazione abilita scrolable
Sam

@ BoonyaKitpitak, ho provato LinearSnapHelpere si aggancia a un elemento centrale. PagerSnapHelperimpedisce di scorrere facilmente (almeno, un elenco di immagini).
CoolMind

@ BoonyaKitpitak puoi dirmi come fare, un elemento in cente completamente visibile e gli elementi laterali sono metà visibili?
Rucha Bhatt Joshi

30

Devi usare findFirstVisibleItemPosition per andare nella direzione opposta. E per rilevare la direzione in cui si trovava lo swipe, dovrai ottenere la velocità di lancio o il cambiamento in x. Ho affrontato questo problema da un'angolazione leggermente diversa dalla tua.

Crea una nuova classe che estende la classe RecyclerView e quindi sovrascrive il metodo fling di RecyclerView in questo modo:

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

Dove viene definita ScreenWidth?
ltsai

@ Itsai: Oops! Buona domanda. È una variabile che ho impostato altrove nella classe. È la larghezza dello schermo del dispositivo in pixel, non i pixel di densità, poiché il metodo smoothScrollBy richiede il numero di pixel che si desidera che scorra.
eDizzle

3
@ltsai Puoi cambiare in getWidth () (recyclerview.getWidth ()) invece di screenWidth. screenWidth non funziona correttamente quando RecyclerView è più piccolo della larghezza dello schermo.
VAdaihiep

Ho provato a estendere RecyclerView ma ricevo "Errore durante il gonfiaggio della classe RecyclerView personalizzata". Puoi aiutare? Grazie

@bEtTyBarnes: potresti cercarlo in un altro feed di domande. È piuttosto fuori portata con la domanda iniziale qui.
eDizzle

6

Basta aggiungere paddinge margina recyclerViewe recyclerView item:

recyclerVisualizza articolo:

<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" />

e impostare PagerSnapHelper:

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

dp a px:

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

risultato:

inserisci qui la descrizione dell'immagine


C'è un modo per ottenere un'imbottitura variabile tra gli elementi? come se la prima e l'ultima carta non dovessero essere centrate per mostrare più carte successive / precedenti rispetto alle carte intermedie che sono centrate?
RamPrasadBismil

2

La mia soluzione:

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

Passo questo layout manager a RecycledView e imposto l'offset richiesto per centrare gli elementi. Tutti i miei articoli hanno la stessa larghezza, quindi un offset costante è ok


0

PagerSnapHelpernon funziona con GridLayoutManagerspanCount> 1, quindi la mia soluzione in questa circostanza è:

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
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.