RecyclerView poziomy zatrzask przewijania w środku


89

Próbuję tutaj utworzyć widok podobny do karuzeli przy użyciu RecyclerView, chcę, aby element był przyciągany na środku ekranu podczas przewijania, jeden element na raz. Próbowałem użyćrecyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);

ale widok nadal płynnie się przewija, próbowałem też zaimplementować własną logikę za pomocą nasłuchiwania przewijania:

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

Przesunięcie od prawej do lewej działa teraz dobrze, ale nie na odwrót, czego tu brakuje?

Odpowiedzi:


161

Dzięki LinearSnapHelpertemu jest to teraz bardzo łatwe.

Wszystko, co musisz zrobić, to:

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

Aktualizacja

Dostępny od 25.1.0, PagerSnapHelpermoże pomóc osiągnąć ViewPagerpodobny efekt. Użyj go tak, jak używałbyś LinearSnapHelper.

Stare obejście:

Jeśli chcesz, aby zachowywał się podobnie do ViewPager, spróbuj zamiast tego:

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

Powyższa implementacja po prostu zwraca pozycję obok bieżącej pozycji (wyśrodkowaną) na podstawie kierunku prędkości, niezależnie od wielkości.

Pierwsza z nich to własne rozwiązanie zawarte w Support Library w wersji 24.2.0. Oznacza to, że musisz dodać to do modułu aplikacji build.gradlelub zaktualizować.

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

To nie zadziałało dla mnie (ale rozwiązanie autorstwa @Albert Vila Calvo zadziałało). Wdrożyłeś to? Czy mamy zastąpić metody z niego? Myślę, że klasa powinna zrobić wszystkie prace z pudełka
— galaxigirl

Tak, stąd odpowiedź. Jednak nie testowałem dokładnie. Żeby było jasne, zrobiłem to z horyzontem, LinearLayoutManagergdzie wszystkie widoki były regularne. Nie jest potrzebne nic poza powyższym fragmentem.
— razzledazzle

@galaxigirl przepraszam, zrozumiałem, co masz na myśli i dokonałem pewnych zmian, potwierdzając to, proszę o komentarz. To jest alternatywne rozwiązanie z LinearSnapHelper.
— razzledazzle

To działało w sam raz dla mnie. @galaxigirl nadpisanie tej metody pomaga w tym, że pomocnik przyciągania liniowego w przeciwnym razie przeskoczy do najbliższego elementu podczas opadania przewijania, a nie dokładnie do następnego / poprzedniego elementu. Wielkie dzięki za to, razzledazzle
— AA_PV,

LinearSnapHelper był pierwszym rozwiązaniem, które zadziałało w moim przypadku, ale wydaje się, że PagerSnapHelper zapewnił przesuwanieowi lepszą animację.
— LeonardoSibela

76

Aktualizacja Google I / O 2019

ViewPager2 jest tutaj!

Firma Google właśnie ogłosiła podczas wykładu „Co nowego w Androidzie” (znanego również jako „Myśl przewodnia Androida”), że pracuje nad nowym ViewPager opartym na RecyclerView!

Ze slajdów:

Podobnie jak ViewPager, ale lepiej

  • Łatwa migracja z ViewPager
  • Na podstawie RecyclerView
  • Obsługa trybu od prawej do lewej
  • Umożliwia stronicowanie w pionie
  • Ulepszone powiadomienia o zmianie zbioru danych

Możesz sprawdzić najnowszą wersję tutaj, a informacje o wydaniu tutaj . Istnieje również oficjalna próbka .

Osobista opinia: myślę, że to naprawdę potrzebny dodatek. Ostatnio miałem dużo problemów z PagerSnapHelper oscylacją w lewo w prawo w nieskończoność - zobacz bilet , który otworzyłem.


Nowa odpowiedź (2016)

Możesz teraz po prostu użyć SnapHelpera .

Jeśli chcesz, aby zachowanie przyciągania wyrównane do środka było podobne do ViewPager, użyj PagerSnapHelper :

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

Istnieje również LinearSnapHelper . Wypróbowałem to i jeśli rzucasz energią, przewija 2 przedmioty z 1 rzutem. Osobiście mi się to nie podobało, ale po prostu zdecyduj sam - wypróbowanie tego zajmuje tylko kilka sekund.


Oryginalna odpowiedź (2016)

Po wielu godzinach próbowania 3 różnych rozwiązań znalezionych tutaj w SO w końcu zbudowałem rozwiązanie, które bardzo dokładnie naśladuje zachowanie występujące w pliku ViewPager.

Rozwiązanie jest oparte na rozwiązaniu @eDizzle , które, jak sądzę, poprawiłem na tyle, aby powiedzieć, że działa prawie jak plik ViewPager.

Ważne: RecyclerViewszerokość moich przedmiotów jest dokładnie taka sama jak na ekranie. Nie próbowałem z innymi rozmiarami. Używam go również z poziomym LinearLayoutManager. Myślę, że będziesz musiał dostosować kod, jeśli chcesz przewijać w pionie.

Tutaj masz kod:

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

}

Cieszyć się!


Miło złapać onScrollStateChanged (); w przeciwnym razie mały błąd jest obecny, jeśli powoli przesuniemy RecyclerView. Dzięki!
— Mauricio Sartori

Aby obsługiwać marginesy (android: layout_marginStart = "16dp") Próbowałem z int screenwidth = getWidth (); i wydaje się, że działa.
— eigil

AWESOOOMMEEEEEEEEE
— raditya gumay

1
@galaxigirl nie, jeszcze nie wypróbowałem LinearSnapHelper. Właśnie zaktualizowałem odpowiedź, aby ludzie wiedzieli o oficjalnym rozwiązaniu. Moje rozwiązanie działa bez problemu w mojej aplikacji.
— Albert Vila Calvo

1
@AlbertVilaCalvo Próbowałem LinearSnapHelper. LinearSnapHelper współpracuje ze Scrollerem. Przewija się i zaskakuje w oparciu o grawitację i prędkość lotu. Większa prędkość przewijania większej liczby stron. Nie polecałbym tego dla zachowania typu ViewPager.
— mipreamble

46

Jeśli celem jest RecyclerViewnaśladowanie zachowania, ViewPagerto podejście jest dość łatwe

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

Używając PagerSnapHelper, możesz uzyskać takie zachowanie jakViewPager


4
Używam LinearSnapHelperzamiast PagerSnapHelper i to działa dla mnie
— fanjavaid

1
Tak, nie ma efektu przyciąganiaLinearSnapHelper
— Boonya Kitpitak

1
to przykleja element do widoku włącza przewijanie
— Sam

@BoonyaKitpitak, próbowałem LinearSnapHelperi przeskakuje do środkowego elementu. PagerSnapHelperutrudnia łatwe przewijanie (przynajmniej listy obrazów).
— CoolMind

@BoonyaKitpitak Czy możesz mi powiedzieć, jak to zrobić, jeden element w centrum jest w pełni widoczny, a elementy boczne są widoczne w połowie?
— Rucha Bhatt Joshi

30

Aby iść w przeciwnym kierunku, musisz użyć funkcji findFirstVisibleItemPosition. Aby wykryć, w którym kierunku był zamach, musisz uzyskać prędkość lotu lub zmianę w x. Podszedłem do tego problemu z nieco innego punktu widzenia niż ty.

Utwórz nową klasę, która rozszerza klasę RecyclerView, a następnie przesłoń metodę fling RecyclerView w następujący sposób:

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

Gdzie zdefiniowano screenWidth?
— ltsai

@Itsai: Ups! Dobre pytanie. Jest to zmienna, którą ustawiłem w innym miejscu w klasie. Jest to szerokość ekranu urządzenia w pikselach, a nie piksele gęstości, ponieważ metoda smoothScrollBy wymaga liczby pikseli, o którą ma się przewijać.
— eDizzle

3
@ltsai Możesz zmienić getWidth () (recyclinglerview.getWidth ()) zamiast screenWidth. screenWidth nie działa poprawnie, gdy szerokość RecyclerView jest mniejsza niż szerokość ekranu.
— VAdaihiep

Próbowałem rozszerzyć RecyclerView, ale otrzymuję komunikat „Błąd inflacji niestandardowej klasy RecyclerView”. Możesz pomóc? Dzięki

@bEtTyBarnes: Możesz poszukać tego w innym kanale pytań. To jest poza zakresem pierwszego pytania tutaj.
— eDizzle

6

Po prostu dodaj paddingi margindo recyclerViewi recyclerView item:

recyklerZobacz element:

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

recyklerView:

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

i ustaw PagerSnapHelper:

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

dp do px:

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

wynik:

wprowadź opis obrazu tutaj


Czy istnieje sposób na osiągnięcie zmiennego wypełnienia między elementami? na przykład jeśli pierwsza i ostatnia karta nie musi być wyśrodkowana, aby pokazać więcej kart następnej / poprzedniej w porównaniu z wyśrodkowanymi kartami pośrednimi?
— RamPrasadBismil

2

Moje rozwiązanie:

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

Przekazuję tego menedżera układu do RecycledView i ustawiam przesunięcie wymagane do wyśrodkowania elementów. Wszystkie moje przedmioty mają tę samą szerokość, więc stałe przesunięcie jest w porządku


0

PagerSnapHelpernie działa z GridLayoutManagerspanCount> 1, więc moim rozwiązaniem w tej sytuacji jest:

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
    }
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.