Gulir horizontal RecyclerView terkunci di tengah

89

Saya mencoba membuat tampilan seperti korsel di sini menggunakan RecyclerView, saya ingin item dipasang di tengah layar saat menggulir, satu item dalam satu waktu. Saya sudah mencoba menggunakanrecyclerView.setScrollingTouchSlop(RecyclerView.TOUCH_SLOP_PAGING);

tetapi tampilan masih bergulir dengan lancar, saya juga mencoba menerapkan logika saya sendiri menggunakan pendengar gulir seperti:

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

Gesekan dari kanan ke kiri berfungsi dengan baik sekarang, tetapi tidak sebaliknya, apa yang saya lewatkan di sini?

Ahmed Awad
sumber

Jawaban:

164

Dengan LinearSnapHelper, sekarang ini sangat mudah.

Yang perlu Anda lakukan hanyalah ini:

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

Memperbarui

Tersedia sejak 25.1.0, PagerSnapHelperdapat membantu mencapai ViewPagerefek serupa. Gunakan seperti Anda akan menggunakan LinearSnapHelper.

Solusi lama:

Jika Anda ingin agar berperilaku serupa dengan ViewPager, coba ini sebagai gantinya:

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

Implementasi di atas hanya mengembalikan posisi di sebelah item saat ini (berpusat) berdasarkan arah kecepatan, berapa pun besarnya.

Yang pertama adalah solusi pihak pertama yang disertakan dalam Support Library versi 24.2.0. Artinya Anda harus menambahkan ini ke modul aplikasi Anda build.gradleatau memperbaruinya.

compile "com.android.support:recyclerview-v7:24.2.0"
razzledazzle
sumber
Ini tidak berhasil untuk saya (tetapi solusi oleh @Albert Vila Calvo berhasil). Sudahkah Anda menerapkannya? Apakah kita harus mengganti metode darinya? Saya akan berpikir bahwa kelas harus melakukan semua pekerjaan di luar kotak
galaxigirl
Ya, itulah jawabannya. Namun saya belum menguji secara menyeluruh. Untuk memperjelas, saya melakukan ini dengan horizontal di LinearLayoutManagermana semua tampilan berukuran biasa. Tidak ada yang lain selain cuplikan di atas.
razzledazzle
@galaxigirl mohon maaf, saya telah mengerti apa yang Anda maksud dan telah membuat beberapa perubahan mengakui itu, tolong beri komentar. Ini adalah solusi alternatif dengan LinearSnapHelper.
razzledazzle
Ini bekerja dengan baik untuk saya. @galaxigirl overriding metode ini membantu dalam hal itu linear snap helper jika tidak akan snap ke item terdekat saat scroll diselesaikan, tidak persis hanya item berikutnya / sebelumnya. Terima kasih banyak untuk ini, razzledazzle
AA_PV
LinearSnapHelper adalah solusi awal yang berhasil untuk saya, tetapi tampaknya PagerSnapHelper memberikan animasi yang lebih baik untuk menggesek.
LeonardoSibela
77

Pembaruan Google I / O 2019

ViewPager2 ada di sini!

Google baru saja mengumumkan pada pembicaraan 'What's New in Android' (alias 'The Android keynote') bahwa mereka sedang mengerjakan ViewPager baru berdasarkan RecyclerView!

Dari slide:

Seperti ViewPager, tetapi lebih baik

  • Migrasi mudah dari ViewPager
  • Berdasarkan RecyclerView
  • Dukungan mode Kanan-ke-Kiri
  • Memungkinkan paging vertikal
  • Pemberitahuan perubahan set data yang ditingkatkan

Anda dapat memeriksa versi terbaru di sini dan catatan rilisnya di sini . Ada juga sampel resmi .

Pendapat pribadi: Saya pikir ini adalah tambahan yang sangat dibutuhkan. Saya baru-baru ini mengalami banyak masalah dengan PagerSnapHelper kiri kanan yang berosilasi tanpa batas waktu - lihat tiket yang telah saya buka.


Jawaban baru (2016)

Anda sekarang dapat menggunakan SnapHelper .

Jika Anda menginginkan perilaku gertakan rata tengah yang mirip dengan ViewPager, gunakan PagerSnapHelper :

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

Ada juga LinearSnapHelper . Saya sudah mencobanya dan jika Anda fling dengan energi maka itu menggulung 2 item dengan 1 lemparan. Secara pribadi saya tidak menyukainya, tetapi memutuskan sendiri - mencobanya hanya membutuhkan beberapa detik.


Jawaban asli (2016)

Setelah berjam - jam mencoba 3 solusi berbeda yang ditemukan di SO, saya akhirnya membuat solusi yang sangat mirip dengan perilaku yang ditemukan di file ViewPager.

Solusinya didasarkan pada solusi @eDizzle , yang saya yakin telah cukup saya tingkatkan untuk mengatakan bahwa itu bekerja hampir seperti ViewPager.

Penting: RecyclerViewlebar item saya persis sama dengan layar. Saya belum mencoba dengan ukuran lain. Saya juga menggunakannya dengan horizontal LinearLayoutManager. Saya pikir Anda perlu menyesuaikan kode jika Anda ingin scroll vertikal.

Di sini Anda memiliki kodenya:

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

}

Nikmati!

Albert Vila Calvo
sumber
Bagus menangkap onScrollStateChanged (); jika tidak, bug kecil akan muncul jika kita menggesek RecyclerView secara perlahan. Terima kasih!
Mauricio Sartori
Untuk mendukung margin (android: layout_marginStart = "16dp") saya mencoba dengan int screenwidth = getWidth (); dan tampaknya berhasil.
eigil
AWESOOOMMEEEEEEEEE
raditya gumay
1
@galaxigirl tidak, saya belum mencoba LinearSnapHelper. Saya baru saja memperbarui jawabannya sehingga orang tahu tentang solusi resminya. Solusi saya berfungsi tanpa masalah di aplikasi saya.
Albert Vila Calvo
1
@AlbertVilaCalvo Saya mencoba LinearSnapHelper. LinearSnapHelper bekerja dengan Scroller. Ini menggulung dan membentak berdasarkan gravitasi dan kecepatan lemparan. Semakin tinggi kecepatan lebih banyak halaman yang di-scroll. Saya tidak akan merekomendasikannya untuk perilaku seperti ViewPager.
mipreamble
47

Jika tujuannya adalah untuk RecyclerViewmeniru perilaku, ViewPagerada pendekatan yang cukup mudah

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

Dengan menggunakan PagerSnapHelperAnda bisa mendapatkan perilaku sepertiViewPager

Boonya Kitpitak
sumber
4
Saya menggunakan LinearSnapHelperalih-alih PagerSnapHelper dan berfungsi untuk saya
fanjavaid
1
Ya, tidak ada efek jentikan diLinearSnapHelper
Boonya Kitpitak
1
ini menempel item pada tampilan memungkinkan scrolable
Sam
@BoonyaKitpitak, saya mencoba LinearSnapHelper, dan itu masuk ke item tengah. PagerSnapHelperpenghalang untuk menggulir dengan mudah (setidaknya, daftar gambar).
CoolMind
@BoonyaKitpitak Bisakah Anda memberi tahu saya bagaimana melakukan ini, satu item dalam cente terlihat sepenuhnya dan item samping setengah terlihat?
Rucha Bhatt Joshi
30

Anda perlu menggunakan findFirstVisibleItemPosition untuk pergi ke arah yang berlawanan. Dan untuk mendeteksi ke arah mana gesekan itu masuk, Anda harus mendapatkan kecepatan lemparan atau perubahan x. Saya mendekati masalah ini dari sudut yang sedikit berbeda dari yang Anda miliki.

Buat kelas baru yang memperluas kelas RecyclerView lalu ganti metode melempar RecyclerView seperti ini:

@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
sumber
Di mana screenWidth ditentukan?
ltsai
@Itsai: Ups! Pertanyaan bagus. Ini adalah variabel yang saya tetapkan di tempat lain di kelas. Ini adalah lebar layar perangkat dalam piksel, bukan piksel kepadatan, karena metode smoothScrollBy memerlukan jumlah piksel yang Anda inginkan untuk digulir.
eDizzle
3
@ltsai Anda dapat mengubah ke getWidth () (recyclerview.getWidth ()) sebagai ganti screenWidth. screenWidth tidak berfungsi dengan benar saat RecyclerView lebih kecil dari lebar layar.
VAdaihiep
Saya mencoba untuk memperluas RecyclerView tetapi saya mendapatkan "Kesalahan menggembungkan RecyclerView kustom kelas". Bisakah kamu menolong? Terima kasih
@bEtTyBarnes: Anda mungkin ingin mencarinya di feed pertanyaan lain. Ini cukup di luar ruang lingkup dengan pertanyaan awal di sini.
eDizzle
6

Cukup tambahkan paddingdan marginke recyclerViewdan recyclerView item:

recyclerView item:

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

dan setel PagerSnapHelper:

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

dp ke px:

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

hasil:

masukkan deskripsi gambar di sini

Farzad
sumber
Apakah ada cara untuk mencapai bantalan variabel antar item? seperti jika kartu pertama dan terakhir tidak perlu di tengah untuk menampilkan lebih banyak kartu berikutnya / sebelumnya vs kartu perantara yang dipusatkan?
RamPrasadBismil
2

Solusi saya:

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

Saya meneruskan manajer tata letak ini ke RecycledView dan mengatur offset yang diperlukan ke item tengah. Semua item saya memiliki lebar yang sama sehingga offset konstan tidak masalah

anagaf
sumber
0

PagerSnapHelpertidak berfungsi dengan GridLayoutManagerspanCount> 1, jadi solusi saya dalam keadaan ini adalah:

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
sumber