RecyclerView GridLayoutManager: Aralık sayımı nasıl otomatik olarak tespit edilir?


110

Yeni GridLayoutManager'ı kullanma: https://developer.android.com/reference/android/support/v7/widget/GridLayoutManager.html

Açık bir aralık sayımı gerektirdiğinden şimdi sorun şu hale gelir: Satır başına kaç "aralık" sığacağını nasıl bilebilirsiniz? Sonuçta bu bir ızgara. Ölçülen genişliğe bağlı olarak RecyclerView'ın sığabileceği kadar açıklık olmalıdır.

GridViewEskisini kullanarak, "columnWidth" özelliğini ayarlarsınız ve bu, kaç tane sütunun sığdığını otomatik olarak algılar. Bu temelde RecyclerView için çoğaltmak istediğim şey:

  • OnLayoutChangeListener ekleyin RecyclerView
  • bu geri aramada, tek bir 'ızgara öğesini' şişirin ve ölçün
  • spanCount = recyclerViewWidth / singleItemWidth;

Bu oldukça yaygın bir davranış gibi görünüyor, bu yüzden görmediğim daha basit bir yol var mı?

Yanıtlar:


122

Kişisel olarak RecyclerView'ı bunun için alt sınıflara ayırmayı sevmiyorum, çünkü benim için GridLayoutManager'ın aralık sayısını tespit etme sorumluluğu var gibi görünüyor. RecyclerView ve GridLayoutManager için bazı android kaynak kodu kazımından sonra, işi yapan kendi sınıf genişletilmiş GridLayoutManager yazdım:

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

OnLayoutChildren'da neden yayılma sayısını ayarlamayı seçtiğimi gerçekten hatırlamıyorum, bu dersi bir süre önce yazmıştım. Ama mesele şu ki, görüş ölçüldükten sonra bunu yapmamız gerekiyor. böylece yüksekliğini ve genişliğini alabiliriz.

DÜZENLEME 1: Aralık sayısının yanlış ayarlanmasına neden olan koddaki hatayı düzeltin. @Elyees Abouda kullanıcısına rapor bildirdiği ve çözüm önerdiği için teşekkürler .

DÜZENLEME 2: Bazı küçük yeniden düzenleme ve manuel yönlendirme değişiklikleri işleme ile kenar durumu düzeltme. Teşekkür kullanıcı @tatarize raporlama ve öneren bir çözüm .


8
Bu kabul cevabı olmalı, bu kadar LayoutManagerDışarısı çocukları bırakmaya iş değil RecyclerView'ın
mewa

3
@ s.maks: Yani, columnWidth bağdaştırıcıya aktarılan verilere bağlı olarak değişir. Diyelim ki dört harfli kelime geçilirse, o zaman 10 harfli kelime geçilirse arka arkaya dört öğeyi barındırması gerekir, o zaman arka arkaya sadece 2 öğeyi barındırabilmelidir.
Shubham

2
Benim için, cihazı döndürürken, ızgaranın boyutunu biraz değiştiriyor, 20dp küçülüyor. Bununla ilgili herhangi bir bilginiz var mı? Teşekkürler.
Alexandre

3
Bazen görünüm oluşturulmadan önce getWidth()veya getHeight()0'dır, bu da yanlış bir spanCount alacaktır (totalSpace <= 0 olacağından 1). Eklediğim şey bu durumda setSpanCount'u görmezden geliyor. ( onLayoutChildrendaha sonra tekrar çağrılacak)
Elyess Abouda

3
Kapsanmayan bir uç durum var. ConfigChanges'ı, tüm etkinliği yeniden oluşturmasına izin vermek yerine dönüşü yönetecek şekilde ayarlarsanız, geri dönüşüm görünümünün genişliğinin değişmesine karşın başka hiçbir şeyin değişmediği garip bir duruma sahip olursunuz. Değişen genişlik ve yükseklik ile yayılma sayısı kirlidir, ancak mColumnWidth değişmemiştir, bu nedenle onLayoutChildren () iptal edilir ve artık kirli değerleri yeniden hesaplamaz. Önceki yüksekliği ve genişliği kaydedin ve sıfır olmayan bir şekilde değişirse tetikleyin.
Tatarize

29

Bunu, bir kez oluşturulduktan sonra yeniden görüntülemenin genişliğini elde etmek ve ardından kaynaklardan kart görünümümün sabit boyutlarını almak ve ardından hesaplamalarımı yaptıktan sonra aralık sayısını ayarlamak için bir görünüm ağacı gözlemcisi kullanarak başardım. Yalnızca gösterdiğiniz öğeler sabit bir genişliğe sahipse gerçekten uygulanabilir. Bu, ekran boyutundan veya yönünden bağımsız olarak ızgarayı otomatik olarak doldurmama yardımcı oldu.

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

2
Bunu kullandım ve got ArrayIndexOutOfBoundsException( android.support.v7.widget.GridLayoutManager.layoutChunk (GridLayoutManager.java:361) de kaydırma olduğunda) RecyclerView.
fikr4n

SetSpanCount () sonrasına mLayoutManager.requestLayout () ekleyin ve işe yarıyor
alvinmeimoun

2
Not: removeGlobalOnLayoutListener()API düzeyi 16'da kullanımdan kaldırılmıştır removeOnGlobalLayoutListener(). Bunun yerine kullanın. Belgeler .
pez

typo removeOnGLobalLayoutListener removeOnGlobalLayoutListener
Theo

17

Eh, kullandığım şey bu, oldukça basit ama işi benim için hallediyor. Bu kod temelde ekran genişliğini dips olarak alır ve ardından 300'e (veya adaptörünüzün düzeni için kullandığınız genişliğe) böler. Yani 300-500 dip genişliğine sahip daha küçük telefonlar sadece bir sütun, tabletler 2-3 sütun vb. Görüntüler. Gördüğüm kadarıyla basit, zahmetsiz ve dezavantajları yok.

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

1
RecyclerView'ın genişliği yerine neden ekran genişliği kullanılıyor? Ve 300
kodlu kodlama

@ xml'deki foo64 öğede match_parent ayarlayabilirsiniz. Ama evet, hala çirkin;)
Philip Giuliani

15

RecyclerView'i genişlettim ve onMeasure yönteminin üzerine yazdım.

Olabildiğince erken bir öğe genişliği (üye değişkeni) belirledim, varsayılan olarak 1. Bu da yapılandırmadaki güncellemeler değişti. Bu artık dikey, yatay, telefon / tablet vb.'ye sığabilecek kadar çok satıra sahip olacak.

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

12
+1 Chiu-ki Chan'ın bu yaklaşımı özetleyen bir blog yazısı ve bunun için örnek bir projesi var .
CommonsWare

7

Bunu, benim durumumda olduğu gibi birinin garip sütun genişliği alması durumunda gönderiyorum.

Saygınlığımın düşük olması nedeniyle @ s-marks'ın cevabına yorum yapamıyorum . Çözüm çözümünü uyguladım ama biraz tuhaf sütun genişliği elde ettim, bu yüzden checkColumnWidth fonksiyonunu aşağıdaki gibi değiştirdim :

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

Verilen sütun genişliğini DP'ye dönüştürerek sorunu çözdü.


2

S işaretlerinin cevabındaki yönelim değişikliğine uyum sağlamak için , genişlik değişikliğini kontrol ettim (sütun genişliğinden değil, getWidth () 'den genişlik).

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

2

İyileştirilmiş çözüm iyidir, ancak gelen değerleri piksel olarak ele alır, bu da dp'yi test etmek ve varsaymak için değerleri kodluyorsanız sizi yukarı çekebilir. Muhtemelen en kolay yol, sütun genişliğini bir boyuta yerleştirmek ve GridAutofitLayoutManager'ı yapılandırırken okumaktır; bu, dp'yi otomatik olarak doğru piksel değerine dönüştürür:

new GridAutofitLayoutManager(getActivity(), (int)getActivity().getResources().getDimension(R.dimen.card_width))

evet, bu beni bir döngü için attı. gerçekten sadece kaynağın kendisini kabul etmek olacaktır. Demek istediğim, her zamanki gibi yapacağız.
Tatarize

2
  1. İmageView'in minimum sabit genişliğini ayarlayın (örneğin 144dp x 144dp)
  2. GridLayoutManager'ı oluşturduğunuzda, minimum imageView boyutuyla ne kadar sütun olacağını bilmeniz gerekir:

    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. Sütunda boşluk varsa bundan sonra bağdaştırıcıda imageView yeniden boyutlandırmanız gerekir. NewImageViewSize gönderebilir ve ardından bağdaştırıcıyı etkinlikten ayırarak ekran ve sütun sayısını hesaplayabilirsiniz:

    @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); //Установка параметров
       ...
    }

Her iki yönde de çalışır. Dikeyde 2 sütunum ve yatayda - 4 sütunum var. Sonuç: https://i.stack.imgur.com/WHvyD.jpg



1

Bu, recyclerview'un kendisinin boyutunu değiştirdiğinde küçük bir düzeltme içeren s.maks sınıfıdır. Örneğin, yönelim değişiklikleriyle kendiniz (manifestte android:configChanges="orientation|screenSize|keyboardHidden") baş ettiğinizde veya geri dönüşüm görünümünün mColumnWidth değişmeden boyutunu değiştirmesinin başka bir nedeni olabilir. Ayrıca, boyutun kaynağı olması için gereken int değerini değiştirdim ve kaynağı olmayan bir kurucuya izin verdim ve ardından setColumnWidth'in bunu kendin yapmasına izin verdim.

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

-1

SpanCount'u büyük bir sayıya (maksimum sütun sayısı) ayarlayın ve GridLayoutManager'a özel bir SpanSizeLookup ayarlayın.

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

Biraz çirkin ama işe yarıyor.

AutoSpanGridLayoutManager gibi bir yöneticinin en iyi çözüm olacağını düşünüyorum, ancak ben böyle bir şey bulamadım.

DÜZENLEME: Bir hata var, bazı cihazlarda sağa boş alan ekliyor


Sağdaki boşluk bir hata değildir. aralık sayısı 5 ise ve getSpanSize3 döndürürse, aralığı doldurmadığınız için bir boşluk olacaktır.
Nicolas Tyler

-6

Aralık sayısını otomatik olarak tespit etmek için kullandığım bir sarmalayıcının ilgili kısımları. setGridLayoutManagerBir R.layout.my_grid_item başvurusu ile çağırarak başlatırsınız ve her satıra bunlardan kaçının sığabileceğini belirler.

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

1
Neden kabul edilen yanıta olumsuz oy vermelisiniz. neden olumsuz oy vermeyi açıklamalı
androidXP
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.