52

I have a collection of photos, and I'm using a RecyclerView to display them. I want to have the first element in my RecyclerView span 2 columns AND 2 rows: enter image description here

I know I can span 2 columns with setSpanSizeLookup:

GridLayoutManager gridLayoutManager = new GridLayoutManager(this, 3);
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
    @Override
    public int getSpanSize(int position) {
        if (position == 0) {
            return 2;
        } else {
            return 1;
        }
    }
});

but how can I also make the first item span 2 rows as well?

I have tried setting the first item's height to be different by inflating a different layout with double the height of the others, but that resulted in every item on the same row as the first item also being stretched to that height:

@Override
public ProfilePicViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View itemView;
    if (viewType == TYPE_MAIN_PHOTO) {
        itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_view_main_profile_photo, parent, false);
    } else {
        itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_view_profile_photo, parent, false);
    }
    return new ProfilePicViewHolder(itemView);
}
Adam Johns
  • 35,397
  • 25
  • 123
  • 176
  • 1
    have you found any solution? – Naveedumar Jul 21 '16 at 06:41
  • No never found a solution. – Adam Johns Jul 21 '16 at 12:47
  • @ Adam Johns, So what way did you go? GridLayout? – pvllnspk Jul 25 '16 at 13:43
  • Maybe checkout my answer for a similar question - [link](http://stackoverflow.com/questions/38569868/android-how-to-implement-grid-with-different-columns-rows-size/38571764#38571764) - as I mentioned in the answer the library can be buggy, but it can give you good hint to write your own LayoutManager – petin Jul 25 '16 at 22:31
  • One option might be to put a separate view on top of your recyclerview that covers those indexes and implement custom logic for the recyclerview to not populate those indexes. – Adam Johns Jul 25 '16 at 22:49
  • 1
    Did you try StaggeredGridLayoutManager? – nshmura Aug 19 '16 at 12:55
  • 1
    @nshmura with `StaggeredGridLayoutManager` I can only choose to set span to all columns though. stackoverflow.com/a/27491819/1438339 – Adam Johns Aug 19 '16 at 13:00
  • I have a similar layout and created separate layouts, but the `ImageView` have the same IDs. So the first is in both layouts `@+id/imageView1` and so on. Then you bundle up three images together and calculate types. – carvaq Sep 02 '16 at 08:30
  • did you found any solution? – bugfreerammohan Aug 18 '20 at 10:47

3 Answers3

39

You cannot achieve this behavior with GridLayoutManager, because it only supports spanning multiple columns.

Nick Butcher is currently implementing a custom SpannedGridLayoutManager that does exactly what you want. It allows you to span multiple rows and columns at the same time. The implementation is still WIP, but already works quite well.

SpannedGridLayoutManager.java

package io.plaidapp.ui.recyclerview;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.PointF;
import android.graphics.Rect;
import android.support.annotation.Keep;
import android.support.annotation.NonNull;
import android.support.v7.widget.LinearSmoothScroller;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;

import java.util.ArrayList;
import java.util.List;

import io.plaidapp.R;

/**
 * A {@link RecyclerView.LayoutManager} which displays a regular grid (i.e. all cells are the same
 * size) and allows simultaneous row & column spanning.
 */
public class SpannedGridLayoutManager extends RecyclerView.LayoutManager {

    private GridSpanLookup spanLookup;
    private int columns = 1;
    private float cellAspectRatio = 1f;

    private int cellHeight;
    private int[] cellBorders;
    private int firstVisiblePosition;
    private int lastVisiblePosition;
    private int firstVisibleRow;
    private int lastVisibleRow;
    private boolean forceClearOffsets;
    private SparseArray<GridCell> cells;
    private List<Integer> firstChildPositionForRow; // key == row, val == first child position
    private int totalRows;
    private final Rect itemDecorationInsets = new Rect();

    public SpannedGridLayoutManager(GridSpanLookup spanLookup, int columns, float cellAspectRatio) {
        this.spanLookup = spanLookup;
        this.columns = columns;
        this.cellAspectRatio = cellAspectRatio;
        setAutoMeasureEnabled(true);
    }

    @Keep /* XML constructor, see RecyclerView#createLayoutManager */
    public SpannedGridLayoutManager(
            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        TypedArray a = context.obtainStyledAttributes(
                attrs, R.styleable.SpannedGridLayoutManager, defStyleAttr, defStyleRes);
        columns = a.getInt(R.styleable.SpannedGridLayoutManager_spanCount, 1);
        parseAspectRatio(a.getString(R.styleable.SpannedGridLayoutManager_aspectRatio));
        // TODO use this!
        int orientation = a.getInt(
                R.styleable.SpannedGridLayoutManager_android_orientation, RecyclerView.VERTICAL);
        a.recycle();
        setAutoMeasureEnabled(true);
    }

    public interface GridSpanLookup {
        SpanInfo getSpanInfo(int position);
    }

    public void setSpanLookup(@NonNull GridSpanLookup spanLookup) {
        this.spanLookup = spanLookup;
    }

    public static class SpanInfo {
        public int columnSpan;
        public int rowSpan;

        public SpanInfo(int columnSpan, int rowSpan) {
            this.columnSpan = columnSpan;
            this.rowSpan = rowSpan;
        }

        public static final SpanInfo SINGLE_CELL = new SpanInfo(1, 1);
    }

    public static class LayoutParams extends RecyclerView.LayoutParams {

        int columnSpan;
        int rowSpan;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }

        public LayoutParams(ViewGroup.MarginLayoutParams source) {
            super(source);
        }

        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }

        public LayoutParams(RecyclerView.LayoutParams source) {
            super(source);
        }
    }

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        calculateWindowSize();
        calculateCellPositions(recycler, state);

        if (state.getItemCount() == 0) {
            detachAndScrapAttachedViews(recycler);
            firstVisibleRow = 0;
            resetVisibleItemTracking();
            return;
        }

        // TODO use orientationHelper
        int startTop = getPaddingTop();
        int scrollOffset = 0;
        if (forceClearOffsets) { // see #scrollToPosition
            startTop = -(firstVisibleRow * cellHeight);
            forceClearOffsets = false;
        } else if (getChildCount() != 0) {
            scrollOffset = getDecoratedTop(getChildAt(0));
            startTop = scrollOffset - (firstVisibleRow * cellHeight);
            resetVisibleItemTracking();
        }

        detachAndScrapAttachedViews(recycler);
        int row = firstVisibleRow;
        int availableSpace = getHeight() - scrollOffset;
        int lastItemPosition = state.getItemCount() - 1;
        while (availableSpace > 0 && lastVisiblePosition < lastItemPosition) {
            availableSpace -= layoutRow(row, startTop, recycler, state);
            row = getNextSpannedRow(row);
        }

        layoutDisappearingViews(recycler, state, startTop);
    }

    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
    }

    @Override
    public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
        return new LayoutParams(c, attrs);
    }

    @Override
    public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
        if (lp instanceof ViewGroup.MarginLayoutParams) {
            return new LayoutParams((ViewGroup.MarginLayoutParams) lp);
        } else {
            return new LayoutParams(lp);
        }
    }

    @Override
    public boolean checkLayoutParams(RecyclerView.LayoutParams lp) {
        return lp instanceof LayoutParams;
    }

    @Override
    public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {
        removeAllViews();
        reset();
    }

    @Override
    public boolean supportsPredictiveItemAnimations() {
        return true;
    }

    @Override
    public boolean canScrollVertically() {
        return true;
    }

    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state){
        if (getChildCount() == 0 || dy == 0) return 0;

        int scrolled;
        int top = getDecoratedTop(getChildAt(0));

        if (dy < 0) { // scrolling content down
            if (firstVisibleRow == 0) { // at top of content
                int scrollRange = -(getPaddingTop() - top);
                scrolled = Math.max(dy, scrollRange);
            } else {
                scrolled = dy;
            }
            if (top - scrolled >= 0) { // new top row came on screen
                int newRow = firstVisibleRow - 1;
                if (newRow >= 0) {
                    int startOffset = top - (firstVisibleRow * cellHeight);
                    layoutRow(newRow, startOffset, recycler, state);
                }
            }
            int firstPositionOfLastRow = getFirstPositionInSpannedRow(lastVisibleRow);
            int lastRowTop = getDecoratedTop(
                    getChildAt(firstPositionOfLastRow - firstVisiblePosition));
            if (lastRowTop - scrolled > getHeight()) { // last spanned row scrolled out
                recycleRow(lastVisibleRow, recycler, state);
            }
        } else { // scrolling content up
            int bottom = getDecoratedBottom(getChildAt(getChildCount() - 1));
            if (lastVisiblePosition == getItemCount() - 1) { // is at end of content
                int scrollRange = Math.max(bottom - getHeight() + getPaddingBottom(), 0);
                scrolled = Math.min(dy, scrollRange);
            } else {
                scrolled = dy;
            }
            if ((bottom - scrolled) < getHeight()) { // new row scrolled in
                int nextRow = lastVisibleRow + 1;
                if (nextRow < getSpannedRowCount()) {
                    int startOffset = top - (firstVisibleRow * cellHeight);
                    layoutRow(nextRow, startOffset, recycler, state);
                }
            }
            int lastPositionInRow = getLastPositionInSpannedRow(firstVisibleRow, state);
            int bottomOfFirstRow =
                    getDecoratedBottom(getChildAt(lastPositionInRow - firstVisiblePosition));
            if (bottomOfFirstRow - scrolled < 0) { // first spanned row scrolled out
                recycleRow(firstVisibleRow, recycler, state);
            }
        }
        offsetChildrenVertical(-scrolled);
        return scrolled;
    }

    @Override
    public void scrollToPosition(int position) {
        if (position >= getItemCount()) position = getItemCount() - 1;

        firstVisibleRow = getRowIndex(position);
        resetVisibleItemTracking();
        forceClearOffsets = true;
        removeAllViews();
        requestLayout();
    }

    @Override
    public void smoothScrollToPosition(
            RecyclerView recyclerView, RecyclerView.State state, int position) {
        if (position >= getItemCount()) position = getItemCount() - 1;

        LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) {
            @Override
            public PointF computeScrollVectorForPosition(int targetPosition) {
                final int rowOffset = getRowIndex(targetPosition) - firstVisibleRow;
                return new PointF(0, rowOffset * cellHeight);
            }
        };
        scroller.setTargetPosition(position);
        startSmoothScroll(scroller);
    }

    @Override
    public int computeVerticalScrollRange(RecyclerView.State state) {
        // TODO update this to incrementally calculate
        if (firstChildPositionForRow == null) return 0;
        return getSpannedRowCount() * cellHeight + getPaddingTop() + getPaddingBottom();
    }

    @Override
    public int computeVerticalScrollExtent(RecyclerView.State state) {
        return getHeight();
    }

    @Override
    public int computeVerticalScrollOffset(RecyclerView.State state) {
        if (getChildCount() == 0) return 0;
        return getPaddingTop() + (firstVisibleRow * cellHeight) - getDecoratedTop(getChildAt(0));
    }

    @Override
    public View findViewByPosition(int position) {
        if (position < firstVisiblePosition || position > lastVisiblePosition) return null;
        return getChildAt(position - firstVisiblePosition);
    }

    public int getFirstVisibleItemPosition() {
        return firstVisiblePosition;
    }

    private static class GridCell {
        final int row;
        final int rowSpan;
        final int column;
        final int columnSpan;

        GridCell(int row, int rowSpan, int column, int columnSpan) {
            this.row = row;
            this.rowSpan = rowSpan;
            this.column = column;
            this.columnSpan = columnSpan;
        }
    }

    /**
     * This is the main layout algorithm, iterates over all items and places them into [column, row]
     * cell positions. Stores this layout info for use later on. Also records the adapter position
     * that each row starts at.
     * <p>
     * Note that if a row is spanned, then the row start position is recorded as the first cell of
     * the row that the spanned cell starts in. This is to ensure that we have sufficient contiguous
     * views to layout/draw a spanned row.
     */
    private void calculateCellPositions(RecyclerView.Recycler recycler, RecyclerView.State state) {
        final int itemCount = state.getItemCount();
        cells = new SparseArray<>(itemCount);
        firstChildPositionForRow = new ArrayList<>();
        int row = 0;
        int column = 0;
        recordSpannedRowStartPosition(row, column);
        int[] rowHWM = new int[columns]; // row high water mark (per column)
        for (int position = 0; position < itemCount; position++) {

            SpanInfo spanInfo;
            int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(position);
            if (adapterPosition !=  RecyclerView.NO_POSITION) {
                spanInfo = spanLookup.getSpanInfo(adapterPosition);
            } else {
                // item removed from adapter, retrieve its previous span info
                // as we can't get from the lookup (adapter)
                spanInfo = getSpanInfoFromAttachedView(position);
            }

            if (spanInfo.columnSpan > columns) {
                spanInfo.columnSpan = columns; // or should we throw?
            }

            // check horizontal space at current position else start a new row
            // note that this may leave gaps in the grid; we don't backtrack to try and fit
            // subsequent cells into gaps. We place the responsibility on the adapter to provide
            // continuous data i.e. that would not span column boundaries to avoid gaps.
            if (column + spanInfo.columnSpan > columns) {
                row++;
                recordSpannedRowStartPosition(row, position);
                column = 0;
            }

            // check if this cell is already filled (by previous spanning cell)
            while (rowHWM[column] > row) {
                column++;
                if (column + spanInfo.columnSpan > columns) {
                    row++;
                    recordSpannedRowStartPosition(row, position);
                    column = 0;
                }
            }

            // by this point, cell should fit at [column, row]
            cells.put(position, new GridCell(row, spanInfo.rowSpan, column, spanInfo.columnSpan));

            // update the high water mark book-keeping
            for (int columnsSpanned = 0; columnsSpanned < spanInfo.columnSpan; columnsSpanned++) {
                rowHWM[column + columnsSpanned] = row + spanInfo.rowSpan;
            }

            // if we're spanning rows then record the 'first child position' as the first item
            // *in the row the spanned item starts*. i.e. the position might not actually sit
            // within the row but it is the earliest position we need to render in order to fill
            // the requested row.
            if (spanInfo.rowSpan > 1) {
                int rowStartPosition = getFirstPositionInSpannedRow(row);
                for (int rowsSpanned = 1; rowsSpanned < spanInfo.rowSpan; rowsSpanned++) {
                    int spannedRow = row + rowsSpanned;
                    recordSpannedRowStartPosition(spannedRow, rowStartPosition);
                }
            }

            // increment the current position
            column += spanInfo.columnSpan;
        }
        totalRows = rowHWM[0];
        for (int i = 1; i < rowHWM.length; i++) {
            if (rowHWM[i] > totalRows) {
                totalRows = rowHWM[i];
            }
        }
    }

    private SpanInfo getSpanInfoFromAttachedView(int position) {
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (position == getPosition(child)) {
                LayoutParams lp = (LayoutParams) child.getLayoutParams();
                return new SpanInfo(lp.columnSpan, lp.rowSpan);
            }
        }
        // errrrr?
        return SpanInfo.SINGLE_CELL;
    }

    private void recordSpannedRowStartPosition(final int rowIndex, final int position) {
        if (getSpannedRowCount() < (rowIndex + 1)) {
            firstChildPositionForRow.add(position);
        }
    }

    private int getRowIndex(final int position) {
        return position < cells.size() ? cells.get(position).row : -1;
    }

    private int getSpannedRowCount() {
        return firstChildPositionForRow.size();
    }

    private int getNextSpannedRow(int rowIndex) {
        int firstPositionInRow = getFirstPositionInSpannedRow(rowIndex);
        int nextRow = rowIndex + 1;
        while (nextRow < getSpannedRowCount()
                && getFirstPositionInSpannedRow(nextRow) == firstPositionInRow) {
            nextRow++;
        }
        return nextRow;
    }

    private int getFirstPositionInSpannedRow(int rowIndex) {
        return firstChildPositionForRow.get(rowIndex);
    }

    private int getLastPositionInSpannedRow(final int rowIndex, RecyclerView.State state) {
        int nextRow = getNextSpannedRow(rowIndex);
        return (nextRow != getSpannedRowCount()) ? // check if reached boundary
                getFirstPositionInSpannedRow(nextRow) - 1
                : state.getItemCount() - 1;
    }

    /**
     * Lay out a given 'row'. We might actually add more that one row if the requested row contains
     * a row-spanning cell. Returns the pixel height of the rows laid out.
     * <p>
     * To simplify logic & book-keeping, views are attached in adapter order, that is child 0 will
     * always be the earliest position displayed etc.
     */
    private int layoutRow(
            int rowIndex, int startTop, RecyclerView.Recycler recycler, RecyclerView.State state) {
        int firstPositionInRow = getFirstPositionInSpannedRow(rowIndex);
        int lastPositionInRow = getLastPositionInSpannedRow(rowIndex, state);
        boolean containsRemovedItems = false;

        int insertPosition = (rowIndex < firstVisibleRow) ? 0 : getChildCount();
        for (int position = firstPositionInRow;
             position <= lastPositionInRow;
             position++, insertPosition++) {

            View view = recycler.getViewForPosition(position);
            LayoutParams lp = (LayoutParams) view.getLayoutParams();
            containsRemovedItems |= lp.isItemRemoved();
            GridCell cell = cells.get(position);
            addView(view, insertPosition);

            // TODO use orientation helper
            int wSpec = getChildMeasureSpec(
                    cellBorders[cell.column + cell.columnSpan] - cellBorders[cell.column],
                    View.MeasureSpec.EXACTLY, 0, lp.width, false);
            int hSpec = getChildMeasureSpec(cell.rowSpan * cellHeight,
                    View.MeasureSpec.EXACTLY, 0, lp.height, true);
            measureChildWithDecorationsAndMargin(view, wSpec, hSpec);

            int left = cellBorders[cell.column] + lp.leftMargin;
            int top = startTop + (cell.row * cellHeight) + lp.topMargin;
            int right = left + getDecoratedMeasuredWidth(view);
            int bottom = top + getDecoratedMeasuredHeight(view);
            layoutDecorated(view, left, top, right, bottom);
            lp.columnSpan = cell.columnSpan;
            lp.rowSpan = cell.rowSpan;
        }

        if (firstPositionInRow < firstVisiblePosition) {
            firstVisiblePosition = firstPositionInRow;
            firstVisibleRow = getRowIndex(firstVisiblePosition);
        }
        if (lastPositionInRow > lastVisiblePosition) {
            lastVisiblePosition = lastPositionInRow;
            lastVisibleRow = getRowIndex(lastVisiblePosition);
        }
        if (containsRemovedItems) return 0; // don't consume space for rows with disappearing items

        GridCell first = cells.get(firstPositionInRow);
        GridCell last = cells.get(lastPositionInRow);
        return (last.row + last.rowSpan - first.row) * cellHeight;
    }

    /**
     * Remove and recycle all items in this 'row'. If the row includes a row-spanning cell then all
     * cells in the spanned rows will be removed.
     */
    private void recycleRow(
            int rowIndex, RecyclerView.Recycler recycler, RecyclerView.State state) {
        int firstPositionInRow = getFirstPositionInSpannedRow(rowIndex);
        int lastPositionInRow = getLastPositionInSpannedRow(rowIndex, state);
        int toRemove = lastPositionInRow;
        while (toRemove >= firstPositionInRow) {
            int index = toRemove - firstVisiblePosition;
            removeAndRecycleViewAt(index, recycler);
            toRemove--;
        }
        if (rowIndex == firstVisibleRow) {
            firstVisiblePosition = lastPositionInRow + 1;
            firstVisibleRow = getRowIndex(firstVisiblePosition);
        }
        if (rowIndex == lastVisibleRow) {
            lastVisiblePosition = firstPositionInRow - 1;
            lastVisibleRow = getRowIndex(lastVisiblePosition);
        }
    }

    private void layoutDisappearingViews(
            RecyclerView.Recycler recycler, RecyclerView.State state, int startTop) {
        // TODO
    }

    private void calculateWindowSize() {
        // TODO use OrientationHelper#getTotalSpace
        int cellWidth =
                (int) Math.floor((getWidth() - getPaddingLeft() - getPaddingRight()) / columns);
        cellHeight = (int) Math.floor(cellWidth * (1f / cellAspectRatio));
        calculateCellBorders();
    }

    private void reset() {
        cells = null;
        firstChildPositionForRow = null;
        firstVisiblePosition = 0;
        firstVisibleRow = 0;
        lastVisiblePosition = 0;
        lastVisibleRow = 0;
        cellHeight = 0;
        forceClearOffsets = false;
    }

    private void resetVisibleItemTracking() {
        // maintain the firstVisibleRow but reset other state vars
        // TODO make orientation agnostic
        int minimumVisibleRow = getMinimumFirstVisibleRow();
        if (firstVisibleRow > minimumVisibleRow) firstVisibleRow = minimumVisibleRow;
        firstVisiblePosition = getFirstPositionInSpannedRow(firstVisibleRow);
        lastVisibleRow = firstVisibleRow;
        lastVisiblePosition = firstVisiblePosition;
    }

    private int getMinimumFirstVisibleRow() {
        int maxDisplayedRows = (int) Math.ceil((float) getHeight() / cellHeight) + 1;
        if (totalRows < maxDisplayedRows) return 0;
        int minFirstRow = totalRows - maxDisplayedRows;
        // adjust to spanned rows
        return getRowIndex(getFirstPositionInSpannedRow(minFirstRow));
    }

    /* Adapted from GridLayoutManager */

    private void calculateCellBorders() {
        cellBorders = new int[columns + 1];
        int totalSpace = getWidth() - getPaddingLeft() - getPaddingRight();
        int consumedPixels = getPaddingLeft();
        cellBorders[0] = consumedPixels;
        int sizePerSpan = totalSpace / columns;
        int sizePerSpanRemainder = totalSpace % columns;
        int additionalSize = 0;
        for (int i = 1; i <= columns; i++) {
            int itemSize = sizePerSpan;
            additionalSize += sizePerSpanRemainder;
            if (additionalSize > 0 && (columns - additionalSize) < sizePerSpanRemainder) {
                itemSize += 1;
                additionalSize -= columns;
            }
            consumedPixels += itemSize;
            cellBorders[i] = consumedPixels;
        }
    }

    private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec) {
        calculateItemDecorationsForChild(child, itemDecorationInsets);
        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
        widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + itemDecorationInsets.left,
                lp.rightMargin + itemDecorationInsets.right);
        heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + itemDecorationInsets.top,
                lp.bottomMargin + itemDecorationInsets.bottom);
        child.measure(widthSpec, heightSpec);
    }

    private int updateSpecWithExtra(int spec, int startInset, int endInset) {
        if (startInset == 0 && endInset == 0) {
            return spec;
        }
        int mode = View.MeasureSpec.getMode(spec);
        if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) {
            return View.MeasureSpec.makeMeasureSpec(
                    View.MeasureSpec.getSize(spec) - startInset - endInset, mode);
        }
        return spec;
    }

    /* Adapted from ConstraintLayout */

    private void parseAspectRatio(String aspect) {
        if (aspect != null) {
            int colonIndex = aspect.indexOf(':');
            if (colonIndex >= 0 && colonIndex < aspect.length() - 1) {
                String nominator = aspect.substring(0, colonIndex);
                String denominator = aspect.substring(colonIndex + 1);
                if (nominator.length() > 0 && denominator.length() > 0) {
                    try {
                        float nominatorValue = Float.parseFloat(nominator);
                        float denominatorValue = Float.parseFloat(denominator);
                        if (nominatorValue > 0 && denominatorValue > 0) {
                            cellAspectRatio = Math.abs(nominatorValue / denominatorValue);
                            return;
                        }
                    } catch (NumberFormatException e) {
                        // Ignore
                    }
                }
            }
        }
        throw new IllegalArgumentException("Could not parse aspect ratio: '" + aspect + "'");
    }

}

attrs.xml

<?xml version="1.0" encoding="utf-8"?> 
<resources>

    <declare-styleable name="SpannedGridLayoutManager">
        <attr name="android:orientation" />
        <attr name="spanCount" />
        <attr name="aspectRatio" format="string" />
    </declare-styleable>

</resources>

The code is also available here.

Example usage

The code requires RecyclerView 23.2.0 or higher. So add the following line to your build.gradle, if you didn't already do so.

dependencies {
    compile 'com.android.support:recyclerview-v7:24.2.1'
}

To achieve the layout shown in the initial post, we define the LayoutManager as follows

recyclerView.setLayoutManager(new SpannedGridLayoutManager(
        new SpannedGridLayoutManager.GridSpanLookup() {
            @Override
            public SpannedGridLayoutManager.SpanInfo getSpanInfo(int position) {
                if (position == 0) {
                    return new SpannedGridLayoutManager.SpanInfo(2, 2);
                } else {
                    return new SpannedGridLayoutManager.SpanInfo(1, 1);
                }
            }
        }, 
        3 /* Three columns */, 
        1f /* We want our items to be 1:1 ratio */));
Florian Dreier
  • 419
  • 6
  • 9
  • This is really helpful and I would advise looking at this over TwoWay or Flow – Graham Smith Sep 14 '16 at 16:31
  • i am unable to find this method setAutoMeasureEnabled(true); can you please send me the usage of this class @Florian Dreier – Juturu Yugandhar Oct 04 '16 at 07:57
  • @juturu-yugandhar I added a sample code on how to use the class, as well as the missing attrs.xml – Florian Dreier Oct 05 '16 at 15:32
  • I´ve encountered a little problem, when the layout encounters a blank space (because not always the spans match perfect inside the grid) the layoutmanager behaves strange and crash. The workaround is inserting elements in those spaces as air bubble so the layout manager can render properly. @FlorianDreier – Heraldo Oct 28 '16 at 16:04
  • Exactly what I am looking for. But for some reason, none of items are displayed... – kristyna Apr 19 '17 at 13:11
  • @kristyna Did you set an adapter? Without your code it will be hard to know whats wrong – Florian Dreier Apr 20 '17 at 17:34
  • 2
    @FlorianDreier yes I did. I wasnt able to found the problem, so finally I've solved it with this https://github.com/nickbutcher/plaid/blob/6a3cb17ede0ffeffb4ca16e4fd3c2ab60900d6b8/app/src/main/java/io/plaidapp/ui/recyclerview/SpannedGridLayoutManager.java – kristyna Apr 21 '17 at 09:45
  • It worked but when i have only 2 items, onBindViewHolder() 's called multiple times for each item :( – Duy Pham Aug 29 '17 at 06:57
  • I am trying your answer however, there is an error on line 54 TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.SpannedGridLayoutManager, defStyleAttr, defStyleRes); - //R.styleable.SpannedGridLayoutManager is not found – – SNos Oct 26 '17 at 14:58
  • Seems that you did not create the attrs.xml file properly as explained in my post. – Florian Dreier Oct 26 '17 at 18:13
  • @FlorianDreier may I get the demo code for this because I am not able understand how should i go for this.Please help – Sagar Feb 16 '18 at 10:06
  • 2
    It works fine when you have multiple items on the grid. If you have just one item, it doesn't display anything. – Daniel Ben May 02 '18 at 15:06
  • @BenAsuquoDaniel, that's correct. One item is not showing in the recyclerview. I recommend that its fixed to up-vote this answer. – blueware May 14 '18 at 09:39
  • This is so useful for me, thanks for save my day, working fine. no need to use any thirdparty library. – Parth Patel May 29 '19 at 11:53
  • It hides the first item when I clear all other items except the 1st item. please help me to resolve it – Parth Patel May 31 '19 at 09:40
  • @BenAsuquoDaniel have you found any solutions for that single item? – Parth Patel May 31 '19 at 10:19
  • @ParthPatel unfortunately not. – Daniel Ben May 31 '19 at 12:07
  • What about horizontal orientation? – Sevastyan Savanyuk Aug 28 '19 at 16:20
  • Hi I have try this but my screen is blank, Not inflate view can you please help me – Patel Hiren Feb 09 '20 at 04:42
2

You can use SpannedGridLayoutManager library wrote by Arasthel in here

This is the result

SpannedGridLayoutManager

Frank Nguyen
  • 6,493
  • 3
  • 38
  • 37
0

You can achieve this behavior by using RecycleView for rows only, with ViewHolder for each row. So you will have RowViewHolder for simple rows and something like DoubleRowViewHolder for custom layout that will have 3 items, just the way you want.

Stanislav
  • 438
  • 7
  • 13
Ilya Gazman
  • 31,250
  • 24
  • 137
  • 216