RecyclerView GridLayoutManager: bagaimana cara mendeteksi jumlah rentang secara otomatis?

110

Menggunakan GridLayoutManager baru: https://developer.android.com/reference/android/support/v7/widget/GridLayoutManager.html

Dibutuhkan jumlah span eksplisit, jadi masalahnya sekarang menjadi: bagaimana Anda tahu berapa banyak "span" yang cocok per baris? Ini adalah grid. Harus ada rentang sebanyak yang dapat dimuat oleh RecyclerView, berdasarkan lebar yang diukur.

Menggunakan yang lama GridView, Anda hanya perlu menyetel properti "columnWidth" dan secara otomatis akan mendeteksi berapa banyak kolom yang sesuai. Ini pada dasarnya yang ingin saya tiru untuk RecyclerView:

  • tambahkan OnLayoutChangeListener di RecyclerView
  • dalam panggilan balik ini, tingkatkan satu 'item kisi' dan ukur
  • spanCount = recyclerViewWidth / singleItemWidth;

Ini sepertinya perilaku yang cukup umum, jadi adakah cara yang lebih sederhana yang tidak saya lihat?

foo64
sumber

Jawaban:

122

Personaly Saya tidak suka membuat subkelas RecyclerView untuk ini, karena bagi saya tampaknya ada tanggung jawab GridLayoutManager untuk mendeteksi jumlah rentang. Jadi setelah beberapa kode sumber android menggali untuk RecyclerView dan GridLayoutManager, saya menulis GridLayoutManager perluasan kelas saya sendiri yang melakukan pekerjaan itu:

public class GridAutofitLayoutManager extends GridLayoutManager
{
    private int columnWidth;
    private boolean isColumnWidthChanged = true;
    private int lastWidth;
    private int lastHeight;

    public GridAutofitLayoutManager(@NonNull final Context context, final int columnWidth) {
        /* Initially set spanCount to 1, will be changed automatically later. */
        super(context, 1);
        setColumnWidth(checkedColumnWidth(context, columnWidth));
    }

    public GridAutofitLayoutManager(
        @NonNull final Context context,
        final int columnWidth,
        final int orientation,
        final boolean reverseLayout) {

        /* Initially set spanCount to 1, will be changed automatically later. */
        super(context, 1, orientation, reverseLayout);
        setColumnWidth(checkedColumnWidth(context, columnWidth));
    }

    private int checkedColumnWidth(@NonNull final Context context, final int columnWidth) {
        if (columnWidth <= 0) {
            /* Set default columnWidth value (48dp here). It is better to move this constant
            to static constant on top, but we need context to convert it to dp, so can't really
            do so. */
            columnWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48,
                    context.getResources().getDisplayMetrics());
        }
        return columnWidth;
    }

    public void setColumnWidth(final int newColumnWidth) {
        if (newColumnWidth > 0 && newColumnWidth != columnWidth) {
            columnWidth = newColumnWidth;
            isColumnWidthChanged = true;
        }
    }

    @Override
    public void onLayoutChildren(@NonNull final RecyclerView.Recycler recycler, @NonNull final RecyclerView.State state) {
        final int width = getWidth();
        final int height = getHeight();
        if (columnWidth > 0 && width > 0 && height > 0 && (isColumnWidthChanged || lastWidth != width || lastHeight != height)) {
            final int totalSpace;
            if (getOrientation() == VERTICAL) {
                totalSpace = width - getPaddingRight() - getPaddingLeft();
            } else {
                totalSpace = height - getPaddingTop() - getPaddingBottom();
            }
            final int spanCount = Math.max(1, totalSpace / columnWidth);
            setSpanCount(spanCount);
            isColumnWidthChanged = false;
        }
        lastWidth = width;
        lastHeight = height;
        super.onLayoutChildren(recycler, state);
    }
}

Saya sebenarnya tidak ingat mengapa saya memilih untuk mengatur jumlah rentang di onLayoutChildren, saya menulis kelas ini beberapa waktu lalu. Tapi intinya adalah kita harus melakukannya setelah tampilan diukur. jadi kita bisa mendapatkan tinggi dan lebarnya.

EDIT 1: Perbaiki kesalahan dalam kode yang disebabkan oleh pengaturan jumlah rentang yang salah. Terima kasih pengguna @Elyees Abouda untuk melaporkan dan menyarankan solusi .

EDIT 2: Beberapa refactoring kecil dan fix edge case dengan penanganan perubahan orientasi manual. Terima kasih pengguna @tatarize untuk melaporkan dan menyarankan solusi .

s.maks
sumber
8
Ini harus menjadi jawaban diterima, itu LayoutManager'pekerjaan s untuk meletakkan anak-anak keluar dan tidak RecyclerView' s
Mewa
3
@ s.maks: Maksud saya columnWidth akan berbeda tergantung pada data yang diteruskan ke adaptor. Katakanlah jika empat huruf kata dilewatkan maka itu harus menampung empat item berturut-turut jika 10 huruf kata dilewatkan maka itu harus mampu menampung hanya 2 item dalam satu baris.
Shubham
2
Bagi saya, saat memutar perangkat, itu mengubah sedikit ukuran kisi, menyusut 20dp. Punya info tentang ini? Terima kasih.
Alexandre
3
Kadang getWidth()- kadang atau getHeight()0 sebelum tampilan dibuat, yang akan mendapatkan spanCount yang salah (1 karena totalSpace akan menjadi <= 0). Apa yang saya tambahkan adalah mengabaikan setSpanCount dalam kasus ini. ( onLayoutChildrenakan dipanggil lagi nanti)
Elyess Abouda
3
Ada kondisi tepi yang tidak tercakup. Jika Anda menyetel configChanges sedemikian rupa sehingga Anda menangani rotasi daripada membiarkannya membangun kembali seluruh aktivitas, Anda memiliki kasus ganjil bahwa lebar recyclerview berubah, sementara tidak ada yang lain yang melakukannya. Dengan lebar dan tinggi yang diubah, spancount menjadi kotor, tetapi mColumnWidth tidak berubah, jadi onLayoutChildren () membatalkan dan tidak menghitung ulang nilai sekarang kotor. Simpan tinggi dan lebar sebelumnya, dan picu jika berubah dengan cara bukan nol.
Tatarize
29

Saya menyelesaikan ini menggunakan pengamat pohon tampilan untuk mendapatkan lebar recylcerview setelah dirender dan kemudian mendapatkan dimensi tetap tampilan kartu saya dari sumber daya dan kemudian menyetel jumlah rentang setelah melakukan perhitungan saya. Ini hanya benar-benar berlaku jika item yang Anda tampilkan memiliki lebar tetap. Ini membantu saya mengisi kisi secara otomatis terlepas dari ukuran atau orientasi layar.

mRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
            new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    mRecyclerView.getViewTreeObserver().removeOnGLobalLayoutListener(this);
                    int viewWidth = mRecyclerView.getMeasuredWidth();
                    float cardViewWidth = getActivity().getResources().getDimension(R.dimen.cardview_layout_width);
                    int newSpanCount = (int) Math.floor(viewWidth / cardViewWidth);
                    mLayoutManager.setSpanCount(newSpanCount);
                    mLayoutManager.requestLayout();
                }
            });
speedynomads
sumber
2
Saya menggunakan ini dan mendapatkan ArrayIndexOutOfBoundsException( di android.support.v7.widget.GridLayoutManager.layoutChunk (GridLayoutManager.java:361) ) saat menggulir file RecyclerView.
fikr4n
Cukup tambahkan mLayoutManager.requestLayout () setelah setSpanCount () dan itu berfungsi
alvinmeimoun
2
Catatan: removeGlobalOnLayoutListener()tidak digunakan lagi di API level 16. removeOnGlobalLayoutListener()sebagai gantinya. Dokumentasi .
pez
salah ketik removeOnGLobalLayoutListener harus dihapusOnGlobalLayoutListener
Theo
17

Ini yang saya gunakan, cukup mendasar, tetapi menyelesaikan pekerjaan untuk saya. Kode ini pada dasarnya mendapatkan lebar layar dalam dips dan kemudian dibagi 300 (atau lebar apa pun yang Anda gunakan untuk tata letak adaptor Anda). Jadi ponsel yang lebih kecil dengan lebar 300-500 dip hanya menampilkan satu kolom, tablet 2-3 kolom dll. Sederhana, bebas repot dan tanpa kekurangan, sejauh yang saya bisa lihat.

Display display = getActivity().getWindowManager().getDefaultDisplay();
DisplayMetrics outMetrics = new DisplayMetrics();
display.getMetrics(outMetrics);

float density  = getResources().getDisplayMetrics().density;
float dpWidth  = outMetrics.widthPixels / density;
int columns = Math.round(dpWidth/300);
mLayoutManager = new GridLayoutManager(getActivity(),columns);
mRecyclerView.setLayoutManager(mLayoutManager);
Tulang iga
sumber
1
Mengapa menggunakan lebar layar, bukan lebar RecyclerView? Dan hardcoding 300 adalah praktik yang buruk (harus tetap sinkron dengan tata letak xml Anda)
foo64
@ foo64 di xml Anda cukup menyetel match_parent pada item tersebut. Tapi ya, itu masih jelek;)
Philip Giuliani
15

Saya memperluas RecyclerView dan mengganti metode onMeasure.

Saya menetapkan lebar item (variabel anggota) sedini mungkin, dengan default 1. Ini juga pembaruan pada konfigurasi yang diubah. Ini sekarang akan memiliki sebanyak mungkin baris dalam potret, lanskap, ponsel / tablet dll.

@Override
protected void onMeasure(int widthSpec, int heightSpec) {
    super.onMeasure(widthSpec, heightSpec);
    int width = MeasureSpec.getSize(widthSpec);
    if(width != 0){
        int spans = width / mItemWidth;
        if(spans > 0){
            mLayoutManager.setSpanCount(spans);
        }
    }
}
andrew_s
sumber
12
+1 Chiu-ki Chan memiliki entri blog yang menguraikan pendekatan ini dan proyek contoh untuk itu juga.
CommonsWare
7

Saya memposting ini untuk berjaga-jaga jika seseorang mendapatkan lebar kolom yang aneh seperti dalam kasus saya.

Saya tidak dapat mengomentari jawaban @ s-marks karena reputasi saya yang rendah. Saya menerapkan solusi solusinya tetapi saya mendapat beberapa lebar kolom yang aneh, jadi saya memodifikasi fungsi checkColumnWidth sebagai berikut:

private int checkedColumnWidth(Context context, int columnWidth)
{
    if (columnWidth <= 0)
    {
        /* Set default columnWidth value (48dp here). It is better to move this constant
        to static constant on top, but we need context to convert it to dp, so can't really
        do so. */
        columnWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48,
                context.getResources().getDisplayMetrics());
    }

    else
    {
        columnWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, columnWidth,
                context.getResources().getDisplayMetrics());
    }
    return columnWidth;
}

Dengan mengubah lebar kolom yang diberikan menjadi DP memperbaiki masalah.

Ariq
sumber
2

Untuk mengakomodasi perubahan orientasi pada jawaban s-marks , saya menambahkan tanda centang pada perubahan lebar (lebar dari getWidth (), bukan lebar kolom).

private boolean mWidthChanged = true;
private int mWidth;


@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)
{
    int width = getWidth();
    int height = getHeight();

    if (width != mWidth) {
        mWidthChanged = true;
        mWidth = width;
    }

    if (mColumnWidthChanged && mColumnWidth > 0 && width > 0 && height > 0
            || mWidthChanged)
    {
        int totalSpace;
        if (getOrientation() == VERTICAL)
        {
            totalSpace = width - getPaddingRight() - getPaddingLeft();
        }
        else
        {
            totalSpace = height - getPaddingTop() - getPaddingBottom();
        }
        int spanCount = Math.max(1, totalSpace / mColumnWidth);
        setSpanCount(spanCount);
        mColumnWidthChanged = false;
        mWidthChanged = false;
    }
    super.onLayoutChildren(recycler, state);
}
Andreas
sumber
2

Solusi upvoted baik-baik saja, tetapi menangani nilai yang masuk sebagai piksel, yang dapat membuat Anda tersandung jika Anda melakukan hardcoding nilai untuk pengujian dan asumsi dp. Cara termudah mungkin adalah dengan meletakkan lebar kolom dalam sebuah dimensi dan membacanya saat mengonfigurasi GridAutofitLayoutManager, yang secara otomatis akan mengubah dp menjadi nilai piksel yang benar:

new GridAutofitLayoutManager(getActivity(), (int)getActivity().getResources().getDimension(R.dimen.card_width))
toncek
sumber
ya, itu membuat saya berputar-putar. sungguh itu hanya akan menerima sumber daya itu sendiri. Maksud saya, itulah yang akan selalu kami lakukan.
Tatarize
2
  1. Tetapkan lebar tetap minimal imageView (144dp x 144dp misalnya)
  2. Saat Anda membuat GridLayoutManager, Anda perlu mengetahui berapa banyak kolom dengan ukuran minimal imageView:

    WindowManager wm = (WindowManager) this.getSystemService(Context.WINDOW_SERVICE); //Получаем размер экрана
    Display display = wm.getDefaultDisplay();
    
    Point point = new Point();
    display.getSize(point);
    int screenWidth = point.x; //Ширина экрана
    
    int photoWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 144, this.getResources().getDisplayMetrics()); //Переводим в точки
    
    int columnsCount = screenWidth/photoWidth; //Число столбцов
    
    GridLayoutManager gridLayoutManager = new GridLayoutManager(this, columnsCount);
    recyclerView.setLayoutManager(gridLayoutManager);
  3. Setelah itu Anda perlu mengubah ukuran imageView di adaptor jika Anda memiliki ruang di kolom. Anda dapat mengirim newImageViewSize lalu menonaktifkan adaptor dari aktivitas di sana Anda menghitung layar dan jumlah kolom:

    @Override //Заполнение нашей плитки
    public void onBindViewHolder(PhotoHolder holder, int position) {
       ...
       ViewGroup.LayoutParams photoParams = holder.photo.getLayoutParams(); //Параметры нашей фотографии
    
       int newImageViewSize = screenWidth/columnsCount; //Новый размер фотографии
    
       photoParams.width = newImageViewSize; //Установка нового размера
       photoParams.height = newImageViewSize;
       holder.photo.setLayoutParams(photoParams); //Установка параметров
       ...
    }

Ini berfungsi di kedua orientasi. Secara vertikal saya memiliki 2 kolom dan horizontal - 4 kolom. Hasilnya: https://i.stack.imgur.com/WHvyD.jpg

Serjant.arbuz
sumber
1

Ini adalah kelas s.maks dengan perbaikan kecil saat recyclerview itu sendiri mengubah ukuran. Seperti saat Anda menangani perubahan orientasi sendiri (dalam manifes android:configChanges="orientation|screenSize|keyboardHidden"), atau alasan lain mengapa recyclerview mungkin berubah ukuran tanpa perubahan mColumnWidth. Saya juga mengubah nilai int yang diperlukan untuk menjadi sumber ukuran dan mengizinkan konstruktor tanpa sumber daya, lalu setColumnWidth melakukannya sendiri.

public class GridAutofitLayoutManager extends GridLayoutManager {
    private Context context;
    private float mColumnWidth;

    private float currentColumnWidth = -1;
    private int currentWidth = -1;
    private int currentHeight = -1;


    public GridAutofitLayoutManager(Context context) {
        /* Initially set spanCount to 1, will be changed automatically later. */
        super(context, 1);
        this.context = context;
        setColumnWidthByResource(-1);
    }

    public GridAutofitLayoutManager(Context context, int resource) {
        this(context);
        this.context = context;
        setColumnWidthByResource(resource);
    }

    public GridAutofitLayoutManager(Context context, int resource, int orientation, boolean reverseLayout) {
        /* Initially set spanCount to 1, will be changed automatically later. */
        super(context, 1, orientation, reverseLayout);
        this.context = context;
        setColumnWidthByResource(resource);
    }

    public void setColumnWidthByResource(int resource) {
        if (resource >= 0) {
            mColumnWidth = context.getResources().getDimension(resource);
        } else {
            /* Set default columnWidth value (48dp here). It is better to move this constant
            to static constant on top, but we need context to convert it to dp, so can't really
            do so. */
            mColumnWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48,
                    context.getResources().getDisplayMetrics());
        }
    }

    public void setColumnWidth(float newColumnWidth) {
        mColumnWidth = newColumnWidth;
    }

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        recalculateSpanCount();
        super.onLayoutChildren(recycler, state);
    }

    public void recalculateSpanCount() {
        int width = getWidth();
        if (width <= 0) return;
        int height = getHeight();
        if (height <= 0) return;
        if (mColumnWidth <= 0) return;
        if ((width != currentWidth) || (height != currentHeight) || (mColumnWidth != currentColumnWidth)) {
            int totalSpace;
            if (getOrientation() == VERTICAL) {
                totalSpace = width - getPaddingRight() - getPaddingLeft();
            } else {
                totalSpace = height - getPaddingTop() - getPaddingBottom();
            }
            int spanCount = (int) Math.max(1, Math.floor(totalSpace / mColumnWidth));
            setSpanCount(spanCount);
            currentColumnWidth = mColumnWidth;
            currentWidth = width;
            currentHeight = height;
        }
    }
}
Tatarize
sumber
-1

Setel spanCount ke angka besar (yang merupakan jumlah kolom maksimal) dan setel SpanSizeLookup kustom ke GridLayoutManager.

mLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
    @Override
    public int getSpanSize(int i) {
        return SPAN_COUNT / (int) (mRecyclerView.getMeasuredWidth()/ CELL_SIZE_IN_PX);
    }
});

Agak jelek, tapi berhasil.

Saya pikir manajer seperti AutoSpanGridLayoutManager akan menjadi solusi terbaik, tetapi saya tidak menemukan yang seperti itu.

EDIT: Ada bug, di beberapa perangkat itu menambahkan ruang kosong di sebelah kanan

alvinmeimoun.dll
sumber
Ruang di sebelah kanan bukanlah bug. jika hitungan span adalah 5 dan getSpanSizemengembalikan 3 akan ada spasi karena Anda tidak mengisi span.
Nicolas Tyler
-6

Inilah bagian relevan dari pembungkus yang telah saya gunakan untuk mendeteksi otomatis jumlah rentang. Anda memulainyasetGridLayoutManager dengan memanggil dengan referensi R.layout.my_grid_item , dan mengetahui berapa banyak dari mereka yang dapat muat di setiap baris.

public class AutoSpanRecyclerView extends RecyclerView {
    private int     m_gridMinSpans;
    private int     m_gridItemLayoutId;
    private LayoutRequester m_layoutRequester = new LayoutRequester();

    public void setGridLayoutManager( int orientation, int itemLayoutId, int minSpans ) {
        GridLayoutManager layoutManager = new GridLayoutManager( getContext(), 2, orientation, false );
        m_gridItemLayoutId = itemLayoutId;
        m_gridMinSpans = minSpans;

        setLayoutManager( layoutManager );
    }

    @Override
    protected void onLayout( boolean changed, int left, int top, int right, int bottom ) {
        super.onLayout( changed, left, top, right, bottom );

        if( changed ) {
            LayoutManager layoutManager = getLayoutManager();
            if( layoutManager instanceof GridLayoutManager ) {
                final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
                LayoutInflater inflater = LayoutInflater.from( getContext() );
                View item = inflater.inflate( m_gridItemLayoutId, this, false );
                int measureSpec = View.MeasureSpec.makeMeasureSpec( 0, View.MeasureSpec.UNSPECIFIED );
                item.measure( measureSpec, measureSpec );
                int itemWidth = item.getMeasuredWidth();
                int recyclerViewWidth = getMeasuredWidth();
                int spanCount = Math.max( m_gridMinSpans, recyclerViewWidth / itemWidth );

                gridLayoutManager.setSpanCount( spanCount );

                // if you call requestLayout() right here, you'll get ArrayIndexOutOfBoundsException when scrolling
                post( m_layoutRequester );
            }
        }
    }

    private class LayoutRequester implements Runnable {
        @Override
        public void run() {
            requestLayout();
        }
    }
}
foo64
sumber
1
Mengapa tidak memilih jawaban yang diterima. harus menjelaskan mengapa downvote
androidXP