RecyclerView GridLayoutManager: como detectar automaticamente a contagem de span?

110

Usando o novo GridLayoutManager: https://developer.android.com/reference/android/support/v7/widget/GridLayoutManager.html

É necessária uma contagem de span explícita, então o problema agora é: como você sabe quantos "spans" cabem por linha? Afinal, esta é uma grade. Deve haver tantos vãos quanto o RecyclerView pode caber, com base na largura medida.

Usando o antigo GridView, você apenas definiria a propriedade "columnWidth" e detectaria automaticamente quantas colunas cabem. Isso é basicamente o que eu quero replicar para o RecyclerView:

  • adicione OnLayoutChangeListener no RecyclerView
  • neste retorno de chamada, aumente um único 'item de grade' e meça
  • spanCount = recyclerViewWidth / singleItemWidth;

Este parece um comportamento bastante comum, então há uma maneira mais simples que não estou vendo?

foo64
fonte

Respostas:

122

Pessoalmente, não gosto de criar uma subclasse de RecyclerView para isso, porque, para mim, parece que é responsabilidade do GridLayoutManager detectar a contagem de amplitude. Então, depois de pesquisar o código-fonte do Android para RecyclerView e GridLayoutManager, escrevi minha própria classe GridLayoutManager estendida que faz o trabalho:

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

Na verdade, não me lembro por que escolhi definir a contagem de amplitude em onLayoutChildren, escrevi esta classe há algum tempo. Mas a questão é que precisamos fazer isso depois que a visualização for medida. para que possamos obter sua altura e largura.

EDIT 1: Corrigir erro no código causado pela configuração incorreta da contagem de amplitude. Obrigado ao usuário @Elyees Abouda por relatar e sugerir soluções .

EDIT 2: Algumas pequenas refatorações e correção de casos extremos com manipulação manual de mudanças de orientação. Obrigado ao usuário @tatarize por relatar e sugerir a solução .

s.maks
fonte
8
Esta deve ser a resposta aceita, é LayoutManager's trabalho para colocar as crianças para fora e não RecyclerView' s
mewa
3
@ s.maks: quero dizer que columnWidth difere dependendo dos dados passados ​​para o adaptador. Digamos que, se palavras de quatro letras forem passadas, ele deve acomodar quatro itens em uma linha, se palavras de 10 letras forem passadas, então ele deve ser capaz de acomodar apenas 2 itens em uma linha.
Shubham
2
Para mim, ao girar o aparelho, ele muda um pouco o tamanho da grade, diminuindo 20 dp. Tem alguma informação sobre isso? Obrigado.
Alexandre,
3
Às vezes, getWidth()ou getHeight()é 0 antes de a visualização ser criada, o que resultará em um spanCount errado (1, pois totalSpace será <= 0). O que adicionei foi ignorar setSpanCount neste caso. ( onLayoutChildrenserá chamada novamente mais tarde)
Elyess Abouda
3
Há uma condição de borda que não está coberta. Se você definir configChanges de forma que controle a rotação em vez de permitir que reconstrua toda a atividade, terá o estranho caso de que a largura da visualização de reciclagem muda, enquanto nada mais muda. Com a largura e a altura alteradas, o spancount está sujo, mas mColumnWidth não mudou, então onLayoutChildren () aborta e não recalcula os valores agora sujos. Salve a altura e a largura anteriores e acione se ela mudar de uma maneira diferente de zero.
Tatarize em
29

Eu fiz isso usando um observador de árvore de visualização para obter a largura da recylcerview uma vez renderizada e, em seguida, obter as dimensões fixas de minha visualização de cartão a partir dos recursos e definir a contagem de amplitude após fazer meus cálculos. Só é realmente aplicável se os itens que você está exibindo tiverem uma largura fixa. Isso me ajudou a preencher automaticamente a grade, independentemente do tamanho ou orientação da tela.

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
fonte
2
Eu usei isso e obtive ArrayIndexOutOfBoundsException( em android.support.v7.widget.GridLayoutManager.layoutChunk (GridLayoutManager.java:361) ) ao rolar o RecyclerView.
fikr4n 01 de
Basta adicionar mLayoutManager.requestLayout () após setSpanCount () e funcionará
alvinmeimoun
2
Nota: removeGlobalOnLayoutListener()está obsoleto na API de nível 16. use em seu removeOnGlobalLayoutListener()lugar. Documentação .
pez
typo removeOnGLobalLayoutListener deve ser removeOnGlobalLayoutListener
Theo
17

Bem, isso é o que eu usei, bastante básico, mas faz o trabalho para mim. Este código basicamente obtém a largura da tela em diminuições e então divide por 300 (ou qualquer largura que você esteja usando para o layout do adaptador). Portanto, telefones menores com largura de mergulho de 300-500 exibem apenas uma coluna, tablets de 2-3 colunas etc. Simples, sem complicações e sem desvantagens, pelo que posso ver.

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);
Costelas
fonte
1
Por que usar a largura da tela em vez da largura do RecyclerView? E hardcoding 300 é uma prática ruim (ele precisa ser mantido em sincronia com seu layout xml)
foo64
@ foo64 no xml você pode apenas definir match_parent no item. Mas sim, ainda é feio;)
Philip Giuliani
15

Estendi o RecyclerView e substituí o método onMeasure.

Eu defino uma largura de item (variável de membro) o mais cedo que posso, com um padrão de 1. Isso também atualiza a configuração alterada. Agora, isso terá quantas linhas cabem em retrato, paisagem, telefone / tablet etc.

@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
fonte
12
+1 Chiu-ki Chan tem uma postagem no blog delineando essa abordagem e um projeto de amostra para ela também.
CommonsWare
7

Estou postando isso apenas no caso de alguém ficar com uma largura de coluna estranha como no meu caso.

Não posso comentar a resposta de @s-marks devido à minha baixa reputação. I aplicou sua solução solução , mas eu tenho alguns largura da coluna estranho, então eu modificado checkedColumnWidth função da seguinte forma:

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

Ao converter a largura da coluna dada em DP corrigiu o problema.

Ariq
fonte
2

Para acomodar a mudança de orientação na resposta das marcas s, adicionei uma verificação na mudança de largura (largura de getWidth (), não largura da coluna).

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

A solução upvoted é boa, mas trata os valores de entrada como pixels, o que pode atrapalhar se você estiver codificando valores para teste e assumindo dp. Provavelmente, a maneira mais fácil é colocar a largura da coluna em uma dimensão e lê-la ao configurar o GridAutofitLayoutManager, que converterá automaticamente dp para o valor correto do pixel:

new GridAutofitLayoutManager(getActivity(), (int)getActivity().getResources().getDimension(R.dimen.card_width))
Toncek
fonte
sim, isso me deixou confuso. realmente seria apenas aceitar o próprio recurso. Quer dizer, é como sempre o que faremos.
Tatarize em
2
  1. Defina a largura fixa mínima de imageView (144dp x 144dp, por exemplo)
  2. Ao criar GridLayoutManager, você precisa saber quantas colunas terão com o tamanho mínimo de 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. Depois disso, você precisa redimensionar imageView no adaptador se houver espaço na coluna. Você pode enviar newImageViewSize e, em seguida, inisilizar o adaptador da atividade, onde você calcula a contagem de tela e coluna:

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

Funciona em ambas as orientações. Na vertical tenho 2 colunas e na horizontal - 4 colunas. O resultado: https://i.stack.imgur.com/WHvyD.jpg

Serjant.arbuz
fonte
2

Concluo acima das respostas aqui

Omid Raha
fonte
1

Esta é a classe s.maks com uma pequena correção para quando o próprio recyclerview muda de tamanho. Por exemplo, quando você mesmo lida com as mudanças de orientação (no manifesto android:configChanges="orientation|screenSize|keyboardHidden"), ou por algum outro motivo, a visualização de reciclagem pode mudar de tamanho sem alterar mColumnWidth. Eu também mudei o valor int necessário para ser o recurso do tamanho e permiti que um construtor sem recurso, então, setColumnWidth fizesse isso você mesmo.

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

Defina spanCount como um número grande (que é o número máximo de colunas) e defina um SpanSizeLookup personalizado para GridLayoutManager.

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

É um pouco feio, mas funciona.

Acho que um gerenciador como AutoSpanGridLayoutManager seria a melhor solução, mas não encontrei nada parecido.

EDIT: Há um bug, em algum dispositivo adiciona espaço em branco à direita

Alvinmeimoun
fonte
O espaço à direita não é um bug. se a contagem do intervalo for 5 e getSpanSizeretornar 3, haverá um espaço porque você não está preenchendo o intervalo.
Nicolas Tyler
-6

Aqui estão as partes relevantes de um wrapper que estou usando para detectar automaticamente a contagem de amplitude. Você o inicializa chamando setGridLayoutManagercom uma referência R.layout.my_grid_item , e ele descobre quantos deles cabem em cada linha.

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
fonte
1
Por que downvote para resposta aceita. deve explicar por que
votar