RecyclerView horizontaler Bildlauf in der Mitte einrasten

84

Ich versuche hier mit RecyclerView eine karussellartige Ansicht zu erstellen. Ich möchte, dass das Element beim Scrollen einzeln in der Mitte des Bildschirms einrastet. Ich habe es versuchtrecyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);

Aber die Ansicht rollt immer noch reibungslos. Ich habe auch versucht, meine eigene Logik mit dem Scroll-Listener wie folgt zu implementieren:

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

Das Wischen von rechts nach links funktioniert jetzt einwandfrei, aber nicht umgekehrt. Was vermisse ich hier?

Ahmed Awad
quelle

Antworten:

148

Mit LinearSnapHelperist das jetzt sehr einfach.

Alles was Sie tun müssen ist Folgendes:

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

Aktualisieren

Verfügbar seit 25.1.0, PagerSnapHelperkann helfen, einen ViewPagerähnlichen Effekt zu erzielen . Verwenden Sie es so, wie Sie es verwenden würden LinearSnapHelper.

Alte Problemumgehung:

Wenn Sie möchten, dass es sich ähnlich wie das verhält ViewPager, versuchen Sie stattdessen Folgendes:

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

Die obige Implementierung gibt nur die Position neben dem aktuellen Element (zentriert) basierend auf der Richtung der Geschwindigkeit zurück, unabhängig von der Größe.

Ersteres ist eine Erstanbieterlösung, die in der Support Library Version 24.2.0 enthalten ist. Das heißt, Sie müssen dies zu Ihrem App-Modul hinzufügen build.gradleoder aktualisieren.

compile "com.android.support:recyclerview-v7:24.2.0"
razzledazzle
quelle
Dies hat bei mir nicht funktioniert (aber die Lösung von @Albert Vila Calvo hat funktioniert). Hast du es implementiert? Sollen wir Methoden daraus überschreiben? Ich würde denken, dass die Klasse die ganze Arbeit
sofort
Ja, daher die Antwort. Ich habe jedoch nicht gründlich getestet. Um klar zu sein, habe ich dies mit einer Horizontalen gemacht, LinearLayoutManagerbei der alle Ansichten normal groß waren. Nichts anderes als das obige Snippet wird benötigt.
Razzledazzle
@galaxigirl Entschuldigung, ich habe verstanden, was Sie meinten, und einige Änderungen vorgenommen, um dies zu bestätigen. Bitte kommentieren Sie. Dies ist eine alternative Lösung mit dem LinearSnapHelper.
Razzledazzle
Das hat genau richtig für mich funktioniert. @galaxigirl, das diese Methode überschreibt, hilft dabei, dass der lineare Fang-Helfer ansonsten beim nächsten Bildlauf zum nächsten Element schnappt, nicht genau zum nächsten / vorherigen Element. Vielen Dank dafür, razzledazzle
AA_PV
LinearSnapHelper war die ursprüngliche Lösung, die für mich funktioniert hat, aber es scheint, dass PagerSnapHelper dem Wischen eine bessere Animation verlieh.
LeonardoSibela
74

Google I / O 2019 Update

ViewPager2 ist da!

Google hat gerade beim Vortrag "Was ist neu in Android" (auch bekannt als "The Android Keynote") angekündigt , dass sie an einem neuen ViewPager arbeiten, der auf RecyclerView basiert!

Von den Folien:

Wie ViewPager, aber besser

  • Einfache Migration von ViewPager
  • Basierend auf RecyclerView
  • Unterstützung des Rechts-Links-Modus
  • Ermöglicht vertikales Paging
  • Verbesserte Benachrichtigungen über Datensatzänderungen

Sie können die neueste Version überprüfen hier und die Release Notes hier . Es gibt auch eine offizielle Probe .

Persönliche Meinung: Ich denke, dies ist eine wirklich notwendige Ergänzung. Ich hatte in letzter Zeit große Probleme mit dem PagerSnapHelper oszillierenden Links-Rechts auf unbestimmte Zeit - siehe das Ticket, das ich geöffnet habe.


Neue Antwort (2016)

Sie können jetzt einfach einen SnapHelper verwenden .

Wenn Sie ein zentriertes Ausrichtungsverhalten ähnlich ViewPager wünschen, verwenden Sie PagerSnapHelper :

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

Es gibt auch einen LinearSnapHelper . Ich habe es versucht und wenn du mit Energie fliegst, rollt es 2 Gegenstände mit 1 Schleuder. Persönlich hat es mir nicht gefallen, aber ich entscheide einfach selbst - es dauert nur Sekunden.


Ursprüngliche Antwort (2016)

Nachdem ich viele Stunden lang 3 verschiedene Lösungen ausprobiert habe, die hier in SO gefunden wurden, habe ich endlich eine Lösung entwickelt, die das Verhalten in a sehr genau nachahmt ViewPager.

Die Lösung basiert auf der @ eDizzle- Lösung , die ich meiner Meinung nach so weit verbessert habe, dass sie fast wie eine funktioniert ViewPager.

Wichtig: Die RecyclerViewBreite meiner Artikel entspricht genau der des Bildschirms. Ich habe es nicht mit anderen Größen versucht. Auch ich benutze es mit einer Horizontalen LinearLayoutManager. Ich denke, dass Sie den Code anpassen müssen, wenn Sie einen vertikalen Bildlauf wünschen.

Hier haben Sie den 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);
            }
        }
    }

}

Genießen!

Albert Vila Calvo
quelle
Nizza fangen die on onScrollStateChanged (); Andernfalls liegt ein kleiner Fehler vor, wenn wir die RecyclerView langsam durchwischen. Vielen Dank!
Mauricio Sartori
Um Ränder zu unterstützen (android: layout_marginStart = "16dp") habe ich versucht mit int screenwidth = getWidth (); und es scheint zu funktionieren.
Eigil
AWESOOOMMEEEEEEEEE
Raditya Gumay
1
@galaxigirl nein, ich habe LinearSnapHelper noch nicht ausprobiert. Ich habe gerade die Antwort aktualisiert, damit die Leute über die offizielle Lösung Bescheid wissen. Meine Lösung funktioniert problemlos in meiner App.
Albert Vila Calvo
1
@ AlbertVilaCalvo Ich habe LinearSnapHelper ausprobiert. LinearSnapHelper arbeitet mit einem Scroller. Es rollt und schnappt basierend auf der Schwerkraft und der Schleudergeschwindigkeit. Je höher die Geschwindigkeit, desto mehr Seiten werden gescrollt. Ich würde es nicht für ein ViewPager-ähnliches Verhalten empfehlen.
Mipreamble
45

Wenn das Ziel darin besteht, RecyclerViewdas Verhalten zu imitieren, ViewPagerist dies ein recht einfacher Ansatz

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

Durch die Verwendung PagerSnapHelperkönnen Sie das Verhalten wie erhaltenViewPager

Boonya Kitpitak
quelle
4
Ich benutze LinearSnapHelperstatt PagerSnapHelper und es funktioniert für mich
fanjavaid
1
Ja, es gibt keinen Snap-Effekt inLinearSnapHelper
Boonya Kitpitak
1
Dies klebt das Element in der Ansicht aktiviert scrolable
Sam
@BoonyaKitpitak, ich habe es versucht LinearSnapHelper, und es schnappt nach einem mittleren Gegenstand. PagerSnapHelperbehindert das einfache Scrollen (zumindest eine Liste von Bildern).
CoolMind
@BoonyaKitpitak Kannst du mir sagen, wie das geht? Ein Artikel in der Mitte ist vollständig sichtbar und die Nebenelemente sind zur Hälfte sichtbar.
Rucha Bhatt Joshi
30

Sie müssen findFirstVisibleItemPosition verwenden, um in die entgegengesetzte Richtung zu gehen. Und um festzustellen, in welche Richtung sich der Schlag befand, müssen Sie entweder die Schleudergeschwindigkeit oder die Änderung von x ermitteln. Ich habe dieses Problem aus einem etwas anderen Blickwinkel angegangen als Sie.

Erstellen Sie eine neue Klasse, die die RecyclerView-Klasse erweitert, und überschreiben Sie dann die Fling-Methode von RecyclerView wie folgt:

@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
quelle
Wo ist screenWidth definiert?
ltsai
@ Itai: Ups! Gute Frage. Es ist eine Variable, die ich an anderer Stelle in der Klasse festgelegt habe. Dies ist die Bildschirmbreite des Geräts in Pixel und nicht in Dichte-Pixeln, da für die Methode "cleanScrollBy" die Anzahl der Pixel erforderlich ist, um die das Gerät gescrollt werden soll.
eDizzle
3
@ltsai Sie können zu getWidth () (recyclerview.getWidth ()) anstelle von screenWidth wechseln. screenWidth funktioniert nicht richtig, wenn RecyclerView kleiner als die Bildschirmbreite ist.
VAdaihiep
Ich habe versucht, RecyclerView zu erweitern, erhalte jedoch die Meldung "Fehler beim Aufblasen der benutzerdefinierten RecyclerView-Klasse". Kannst du helfen? Danke
@bEtTyBarnes: Vielleicht möchten Sie in einem anderen Fragen-Feed danach suchen. Die anfängliche Frage hier ist ziemlich unangebracht.
eDizzle
6

Fügen Sie einfach hinzu paddingund marginzu recyclerViewund recyclerView item:

recyclerView-Artikel:

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

und setzen PagerSnapHelper:

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

dp bis px:

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

Ergebnis:

Geben Sie hier die Bildbeschreibung ein

Farzad
quelle
Gibt es eine Möglichkeit, eine variable Auffüllung zwischen Elementen zu erreichen? Zum Beispiel, wenn die erste und die letzte Karte nicht zentriert werden müssen, um mehr von den nächsten / vorherigen Karten im Vergleich zu den zentrierten Zwischenkarten anzuzeigen?
RamPrasadBismil
2

Meine Lösung:

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

Ich übergebe diesen Layout-Manager an RecycledView und stelle den Versatz ein, der zum Zentrieren von Elementen erforderlich ist. Alle meine Artikel haben die gleiche Breite, daher ist ein konstanter Versatz in Ordnung

Anagaf
quelle
0

PagerSnapHelperfunktioniert nicht GridLayoutManagermit spanCount> 1, daher lautet meine Lösung unter diesen Umständen:

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
quelle