tutorial studio recyclerview recycler personalizado listas dinamicas activity android android-recyclerview android-gridlayout gridlayoutmanager

android - studio - RecyclerView GridLayoutManager: ¿cómo detectar automáticamente el recuento de span?



recyclerview in activity (12)

Usando el nuevo GridLayoutManager: https://developer.android.com/reference/android/support/v7/widget/GridLayoutManager.html

Se necesita un recuento de tramo explícito, por lo que el problema ahora es: ¿cómo sabe cuántos "tramos" caben por fila? Esta es una cuadrícula, después de todo. Debe haber tantos tramos como el RecyclerView pueda caber, según el ancho medido.

Usando el antiguo GridView , simplemente establecería la propiedad "columnWidth" y detectaría automáticamente cuántas columnas caben. Esto es básicamente lo que quiero replicar para RecyclerView:

  • agregue OnLayoutChangeListener en RecyclerView
  • en esta devolución de llamada, infla un solo ''elemento de cuadrícula'' y mídelo
  • spanCount = recyclerViewWidth / singleItemWidth;

Esto parece un comportamiento bastante común, entonces, ¿hay una manera más simple que no estoy viendo?


  1. Establezca un ancho fijo mínimo de imageView (144dp x 144dp por ejemplo)
  2. Cuando crea GridLayoutManager, necesita saber cuántas columnas tendrán con un tamaño 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. Después de eso, debe cambiar el tamaño de imageView en el adaptador si tiene espacio en la columna. Puede enviar newImageViewSize y luego inisilizar el adaptador de la actividad allí calcula la pantalla y el recuento de columnas:

    @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 en ambas orientaciones. En vertical tengo 2 columnas y en horizontal - 4 columnas. El resultado: https://i.stack.imgur.com/WHvyD.jpg


Aquí están las partes relevantes de un contenedor que he estado usando para detectar automáticamente el recuento de intervalos. Lo inicializa llamando a setGridLayoutManager con una referencia R.layout.my_grid_item , y calcula cuántos de ellos pueden caber en cada fila.

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


Bueno, esto es lo que usé, bastante básico, pero hace el trabajo por mí. Este código básicamente obtiene el ancho de la pantalla en saltos y luego se divide por 300 (o el ancho que esté utilizando para el diseño de su adaptador). Por lo tanto, los teléfonos más pequeños con un ancho de inmersión de 300-500 solo muestran una columna, tabletas de 2-3 columnas, etc. Por lo que puedo ver, simple, sin complicaciones y sin inconvenientes.

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


Esta es la clase de s.maks con una corrección menor para cuando la vista del reciclador cambia de tamaño. Tal como cuando trata con la orientación, usted mismo cambia (en el manifiesto android:configChanges="orientation|screenSize|keyboardHidden" ), o por alguna otra razón, la vista del reciclador puede cambiar de tamaño sin que cambie mColumnWidth. También cambié el valor int que se necesita para ser el recurso del tamaño y permití que un constructor sin recursos establezcaColumnWidth para hacerlo usted mismo.

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


Establezca spanCount en un número grande (que es el número máximo de columna) y establezca un SpanSizeLookup personalizado en GridLayoutManager.

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

Es un poco feo, pero funciona.

Creo que un administrador como AutoSpanGridLayoutManager sería la mejor solución, pero no encontré nada de eso.

EDITAR: hay un error, en algunos dispositivos agrega espacio en blanco a la derecha


Extendí RecyclerView y anulé el método onMeasure.

Establezco un ancho de elemento (variable de miembro) lo antes posible, con un valor predeterminado de 1. Esto también se actualiza en la configuración cambiada. Esto ahora tendrá tantas filas como pueda caber en vertical, horizontal, teléfono / tableta, 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); } } }


I conclusión arriba respuestas here


La solución votada está bien, pero maneja los valores entrantes como píxeles, lo que puede hacer que se tropiece si está codificando valores para probar y asumir dp. La forma más fácil es colocar el ancho de la columna en una dimensión y leerlo al configurar el GridAutofitLayoutManager, que convertirá automáticamente dp al valor de píxel correcto:

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


Logré esto usando un observador de árbol de vista para obtener el ancho de la vista de lista de reproducción una vez renderizada y luego obtener las dimensiones fijas de mi vista de tarjeta de los recursos y luego establecer el recuento de tramo después de hacer mis cálculos. Solo es realmente aplicable si los elementos que está mostrando son de un ancho fijo. Esto me ayudó a completar automáticamente la cuadrícula independientemente del tamaño de la pantalla o la orientación.

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


Para acomodar el cambio de orientación en respuesta de las s, agregué un control sobre el cambio de ancho (ancho de getWidth (), no ancho de columna).

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


Personalmente, no me gusta subclasificar RecyclerView para esto, porque para mí parece que existe la responsabilidad de GridLayoutManager de detectar el recuento de intervalos. Entonces, después de cavar un código fuente de Android para RecyclerView y GridLayoutManager, escribí mi propia clase extendida GridLayoutManager que hace el trabajo:

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

En realidad no recuerdo por qué elegí establecer el recuento de períodos en onLayoutChildren, escribí esta clase hace algún tiempo. Pero el punto es que debemos hacerlo después de medir la vista. para que podamos obtener su altura y anchura.

EDITAR 1: Se corrigió el error en el código que causaba configurar incorrectamente el recuento de intervalos. Gracias usuario @Elyees Abouda por informar y sugerir una solution .

EDIT 2: algunas pequeñas refactorizaciones y casos de borde fijo con orientación manual de cambios de manejo. Gracias usuario @tatarize por informar y sugerir una solution .


Estoy publicando esto solo en caso de que alguien obtenga un ancho de columna extraño como en mi caso.

No puedo comentar sobre la respuesta de debido a mi baja reputación. Apliqué su solución pero obtuve un ancho de columna extraño, así que modifiqué la función checkColumnWidth de la siguiente manera:

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

Al convertir el ancho de columna dado en DP se solucionó el problema.