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?

Ahmed Awad
źródło

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"
razzledazzle
źródło
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ę!

Albert Vila Calvo
źródło
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

Boonya Kitpitak
źródło
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;
}
eDizzle
źródło
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

Farzad
źródło
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

anagaf
źródło
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
    }
}
Wenqing MA
źródło