RecyclerView GridLayoutManager: comment détecter automatiquement le nombre de span?


110

Utilisation du nouveau GridLayoutManager: https://developer.android.com/reference/android/support/v7/widget/GridLayoutManager.html

Il faut un nombre de travées explicite, donc le problème devient maintenant: comment savoir combien de "travées" correspondent par ligne? C'est une grille, après tout. Il doit y avoir autant de portées que le RecyclerView peut en contenir, en fonction de la largeur mesurée.

En utilisant l'ancien GridView, vous définiriez simplement la propriété "columnWidth" et il détecterait automatiquement le nombre de colonnes qui tiennent. C'est essentiellement ce que je veux répliquer pour le RecyclerView:

  • ajoutez OnLayoutChangeListener sur le RecyclerView
  • dans ce rappel, gonflez un seul `` élément de grille '' et mesurez-le
  • spanCount = recyclerViewWidth / singleItemWidth;

Cela semble être un comportement assez courant, alors y a-t-il un moyen plus simple que je ne vois pas?

Réponses:


122

Personnellement, je n'aime pas sous-classer RecyclerView pour cela, car pour moi, il semble que GridLayoutManager ait la responsabilité de détecter le nombre de plages. Donc, après quelques recherches de code source Android pour RecyclerView et GridLayoutManager, j'ai écrit ma propre classe étendue GridLayoutManager qui fait le travail:

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

Je ne me souviens pas vraiment pourquoi j'ai choisi de définir le nombre de span dans onLayoutChildren, j'ai écrit ce cours il y a quelque temps. Mais le fait est que nous devons le faire après avoir mesuré la vue. afin que nous puissions obtenir sa hauteur et sa largeur.

EDIT 1: Correction d'une erreur dans le code causée par un réglage incorrect du nombre de plages. Merci à l'utilisateur @Elyees Abouda d' avoir signalé et suggéré une solution .

EDIT 2: Quelques petits refactoring et cas de bord fixe avec gestion manuelle des changements d'orientation. Merci à l'utilisateur @tatarize d' avoir signalé et suggéré une solution .


8
Cela devrait être la réponse acceptée, c'est le LayoutManagertravail de mettre les enfants dehors et pas RecyclerViewde
mewa

3
@ s.maks: je veux dire que columnWidth différerait en fonction des données transmises à l'adaptateur. Dites que si des mots de quatre lettres sont passés, il devrait accueillir quatre éléments d'affilée si des mots de 10 lettres sont passés, il ne devrait pouvoir accueillir que 2 éléments d'affilée.
Shubham du

2
Pour moi, lors de la rotation de l'appareil, cela change un peu la taille de la grille, en réduisant de 20dp. Vous avez des informations à ce sujet? Merci.
Alexandre

3
Parfois, getWidth()ou getHeight()vaut 0 avant la création de la vue, ce qui obtiendra un spanCount incorrect (1 puisque totalSpace sera <= 0). Ce que j'ai ajouté ignore setSpanCount dans ce cas. ( onLayoutChildrensera rappelé plus tard)
Elyess Abouda

3
Il y a une condition de bord qui n'est pas couverte. Si vous définissez configChanges de telle sorte que vous gérez la rotation plutôt que de la laisser reconstruire l'activité entière, vous avez le cas étrange que la largeur de la vue de recyclage change, alors que rien d'autre ne le fait. Avec une largeur et une hauteur modifiées, le spancount est sale, mais mColumnWidth n'a pas changé, donc onLayoutChildren () abandonne et ne recalcule pas les valeurs maintenant sales. Enregistrez la hauteur et la largeur précédentes et déclenchez si elles changent de manière non nulle.
Tatarize

29

J'ai accompli cela en utilisant un observateur d'arborescence de vues pour obtenir la largeur de la vue recyl une fois rendue, puis en obtenant les dimensions fixes de ma vue de carte à partir des ressources, puis en définissant le nombre de plages après avoir fait mes calculs. Elle n'est vraiment applicable que si les éléments que vous affichez ont une largeur fixe. Cela m'a aidé à remplir automatiquement la grille indépendamment de la taille ou de l'orientation de l'écran.

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
J'ai utilisé ceci et j'ai obtenu ArrayIndexOutOfBoundsException( à android.support.v7.widget.GridLayoutManager.layoutChunk (GridLayoutManager.java:361) ) lors du défilement du fichierRecyclerView .
fikr4n

Ajoutez simplement mLayoutManager.requestLayout () après setSpanCount () et cela fonctionne
alvinmeimoun

2
Remarque: removeGlobalOnLayoutListener()est obsolète dans l'API niveau 16. utilisez à la removeOnGlobalLayoutListener()place. Documentation .
pez

typo removeOnGLobalLayoutListener doit être removeOnGlobalLayoutListener
Theo

17

Eh bien, c'est ce que j'ai utilisé, assez basique, mais fait le travail pour moi. Ce code obtient essentiellement la largeur de l'écran en creux, puis se divise par 300 (ou quelle que soit la largeur que vous utilisez pour la disposition de votre adaptateur). Ainsi, les téléphones plus petits avec une largeur de creux de 300 à 500 n'affichent qu'une seule colonne, les tablettes 2-3 colonnes, etc. Simple, sans tracas et sans inconvénient, pour autant que je puisse voir.

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
Pourquoi utiliser la largeur de l'écran au lieu de la largeur du RecyclerView? Et le codage en dur 300 est une mauvaise pratique (il doit être synchronisé avec votre mise en page xml)
foo64

@ foo64 dans le xml, vous pouvez simplement définir match_parent sur l'élément. Mais oui, c'est toujours moche;)
Philip Giuliani

15

J'ai étendu RecyclerView et remplacé la méthode onMeasure.

J'ai défini une largeur d'élément (variable membre) aussi tôt que possible, avec une valeur par défaut de 1. Cela met également à jour la configuration modifiée. Cela aura désormais autant de lignes que possible en mode portrait, paysage, téléphone / tablette, 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);
        }
    }
}

12
+1 Chiu-ki Chan a un article de blog décrivant cette approche et un exemple de projet pour elle également.
CommonsWare

7

Je publie ceci juste au cas où quelqu'un aurait une largeur de colonne étrange comme dans mon cas.

Je ne peux pas commenter la réponse de @ s-marks en raison de ma faible réputation. J'ai appliqué sa solution de solution mais j'ai eu une largeur de colonne étrange, j'ai donc modifié la fonction checkedColumnWidth comme suit:

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

En convertissant la largeur de colonne donnée en DP, le problème a été résolu.


2

Pour accommoder le changement d'orientation sur la réponse de s-marks , j'ai ajouté une vérification sur le changement de largeur (largeur de getWidth (), pas largeur de colonne).

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

La solution avec vote positif est correcte, mais gère les valeurs entrantes sous forme de pixels, ce qui peut vous faire trébucher si vous codez en dur des valeurs pour tester et supposer dp. Le moyen le plus simple est probablement de mettre la largeur de la colonne dans une dimension et de la lire lors de la configuration de GridAutofitLayoutManager, qui convertira automatiquement dp en valeur de pixel correcte:

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

ouais, cela m'a jeté pour une boucle. en fait, ce serait simplement accepter la ressource elle-même. Je veux dire que c'est comme toujours ce que nous allons faire.
Tatarize

2
  1. Définir la largeur fixe minimale de imageView (144dp x 144dp par exemple)
  2. Lorsque vous créez GridLayoutManager, vous devez savoir combien de colonnes seront avec une taille minimale 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. Après cela, vous devez redimensionner imageView dans l'adaptateur si vous avez de l'espace dans la colonne. Vous pouvez envoyer newImageViewSize puis inisilize adapter de l'activité là, vous calculez le nombre d'écran et de colonnes:

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

Cela fonctionne dans les deux orientations. En vertical, j'ai 2 colonnes et en horizontal - 4 colonnes. Le résultat: https://i.stack.imgur.com/WHvyD.jpg



1

C'est la classe de s.maks avec un correctif mineur pour le moment où la vue de recyclage elle-même change de taille. Par exemple, lorsque vous gérez vous-même les changements d'orientation (dans le manifeste android:configChanges="orientation|screenSize|keyboardHidden"), ou pour une autre raison, la vue recyclée peut changer de taille sans que mColumnWidth ne change. J'ai également changé la valeur int qu'il faut pour être la ressource de la taille et autorisé un constructeur sans ressource puis setColumnWidth à le faire vous-même.

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

Définissez spanCount sur un grand nombre (qui est le nombre maximal de colonnes) et définissez un SpanSizeLookup personnalisé sur GridLayoutManager.

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

C'est un peu moche, mais ça marche.

Je pense qu'un gestionnaire comme AutoSpanGridLayoutManager serait la meilleure solution, mais je n'ai rien trouvé de tel.

EDIT: Il y a un bug, sur certains appareils, il ajoute un espace vide à droite


L'espace à droite n'est pas un bug. si le nombre de span est 5 et getSpanSizeretourne 3, il y aura un espace car vous ne remplissez pas le span.
Nicolas Tyler

-6

Voici les parties pertinentes d'un wrapper que j'ai utilisé pour détecter automatiquement le nombre de span. Vous l'initialisez en appelant setGridLayoutManageravec un R.layout.my_grid_item référence , et il détermine combien de ceux-ci peuvent tenir sur chaque ligne.

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
Pourquoi voter contre la réponse acceptée. devrait expliquer pourquoi
downvote
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.