LayoutManager for RecyclerView Grid with different cell width

29,686

Solution 1

You can use StaggeredGridLayoutManager.LayoutParams to change width and height of cells, What you expect to design follows a simple pattern, If you assign every cell an index (in the order that StaggeredGridLayoutManager arranges them by default) you will have this:

index % 4 == 3           --->  full span cell
index % 8 == 0, 5        --->  half span cell
index % 8 == 1, 2, 4, 6  ---> quarter span cell

first declare some constants in adapter to define span types:

private static final int TYPE_FULL = 0;
private static final int TYPE_HALF = 1;
private static final int TYPE_QUARTER = 2;

Then override getItemViewType method in your adapter like this:

@Override
public int getItemViewType(int position) {
    final int modeEight = position % 8;
    switch (modeEight) {
        case 0:
        case 5:
            return TYPE_HALF;
        case 1:
        case 2:
        case 4:
        case 6:
            return TYPE_QUARTER;
    }
    return TYPE_FULL;
}

All you need to do is change layoutparams considering viewType of the holder:

@Override
public MyHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
    final View itemView =
            LayoutInflater.from(mContext).inflate(R.layout.items, parent, false);
    itemView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
        @Override
        public boolean onPreDraw() {
            final int type = viewType;
            final ViewGroup.LayoutParams lp = itemView.getLayoutParams();
            if (lp instanceof StaggeredGridLayoutManager.LayoutParams) {
                StaggeredGridLayoutManager.LayoutParams sglp =
                        (StaggeredGridLayoutManager.LayoutParams) lp;
                switch (type) {
                    case TYPE_FULL:
                        sglp.setFullSpan(true);
                        break;
                    case TYPE_HALF:
                        sglp.setFullSpan(false);
                        sglp.width = itemView.getWidth() / 2;
                        break;
                    case TYPE_QUARTER:
                        sglp.setFullSpan(false);
                        sglp.width = itemView.getWidth() / 2;
                        sglp.height = itemView.getHeight() / 2;
                        break;
                }
                itemView.setLayoutParams(sglp);
                final StaggeredGridLayoutManager lm =
                        (StaggeredGridLayoutManager) ((RecyclerView) parent).getLayoutManager();
                lm.invalidateSpanAssignments();
            }
            itemView.getViewTreeObserver().removeOnPreDrawListener(this);
            return true;
        }
    });

    MyHolder holder = new MyHolder(itemView);
    return holder;
}

My adapter always returns 50 for items count(just a test) and I used a simple layout file for items, It contains a LinearLayout and a TextView to show the position of holder, Remember you should pass 2 for spanCount (int the StaggeredGridLayoutManager constructor) because of your design.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    RecyclerView rv = (RecyclerView) findViewById(R.id.rv);
    StaggeredGridLayoutManager lm =
            new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
    rv.setLayoutManager(lm);
    rv.setAdapter(new MyAdapter(this));

}

enter image description here

PS: For lazy people like me maybe it's a simpler way rather than extending my custom LayoutManager, But I'm sure there are better ways to achieve this.

Update: Updated part of your question is more simpler, You can use GridLayoutManager:

    GridLayoutManager glm = new GridLayoutManager(this, 3);
    glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
        @Override
        public int getSpanSize(int position) {
            if (position % 3 == 2) {
                return 3;
            }
            switch (position % 4) {
                case 1:
                case 3:
                    return 1;
                case 0:
                case 2:
                    return 2;
                default:
                    //never gonna happen
                    return  -1 ;
            }
        }
    });
    rv.setLayoutManager(glm);

In this way you set 3 for default span size, Then considering the position of your span, Each item occupies 1, 2 or 3 spans, Something like this:

enter image description here

Item0.width == 2 X Item1.width
Item2.width == 3 X Item1.width
Item0.width == 2/3 X Item1.width

Solution 2

For everyone else, looking for a solution without simplifying your layout, you can have a look to my answer here, or you can read more.

Big thanks to Nick Butcher! His layout manager called SpannableGridLayoutManager is capable of doing this.

  1. Download SpannableGridLayoutManager from here

    For some reason I had to change this line:

    while (availableSpace > 0 && lastVisiblePosition < lastItemPosition) {
    

    to

    while (lastVisiblePosition < lastItemPosition) {
    

    to got the manager working.

  2. Set SpannableGridLayoutManger to your RecyclerView

In your case:

SpannedGridLayoutManager manager = new SpannedGridLayoutManager(
            new SpannedGridLayoutManager.GridSpanLookup() {
                @Override
                public SpannedGridLayoutManager.SpanInfo getSpanInfo(int position) {
                    switch (position % 8) {
                        case 0:
                        case 5:
                            return new SpannedGridLayoutManager.SpanInfo(2, 2);

                        case 3:
                        case 7:
                            return new SpannedGridLayoutManager.SpanInfo(3, 2);

                        default:
                            return new SpannedGridLayoutManager.SpanInfo(1, 1);
                    }
                }
            },
            3, // number of columns
            1f // default size of item
    );

Which will give you exactly what is in the first picture of the question.

enter image description here

Solution 3

Finally did it, it will help everyone who is trying to achieve any kind of patterns

I was looking for the same result but with one header and lazy loading footer. I tried @Kristyna answers of Nick Butcher Library too. But it has many issues like fast scrolling view disappear problem with the extra space or fixed cell height issue.

I found another library of Arasthel SpannedGridLayoutManager and tried to implement it. But it also has many issues like fixed height and footers extra space issue

Finally, after 5hr of digging Arasthel's SpannedGridLayoutManager, I fixed the issues and found my result.

My final edited forked library download from here SpannedGridLayoutManager.

enter image description here enter image description here

In my case

  val adapter = GridItemAdapter()//This is your adapter
  val spannedGridLayoutManager = SpannedGridLayoutManager(orientation = VERTICAL, spans = 3)
  spannedGridLayoutManager.itemOrderIsStable = true
  recyclerview.layoutManager = spannedGridLayoutManager

For the first image result

  spannedGridLayoutManager.spanSizeLookup = SpannedGridLayoutManager.SpanSizeLookup { position ->
        when {
            position == 0 -> {
                /**
                 * 150f is now static 
                 * should calculate programmatically in runtime
                 * for to manage header hight for different resolution devices
                 */
                SpanSize(3, 1, 150f)
            }
            position % 7 == 1 ->
                SpanSize(2, 2)
            else ->
                SpanSize(1, 1)
        }
    }
  recyclerview.adapter = adapter

And the second image result

 spannedGridLayoutManager.spanSizeLookup = SpannedGridLayoutManager.SpanSizeLookup { position ->
        when (position % 8) {
            0, 5 ->
                SpanSize(2, 2)
            3, 7 ->
                SpanSize(3, 2)
            else ->
                SpanSize(1, 1)
        }
    }
    recyclerview.adapter = adapter

This spanSizeLookup logic may vary according to the requirement, for me I had set it randomly for testing purposes.

Share:
29,686
random
Author by

random

Updated on March 17, 2021

Comments

  • random
    random over 3 years

    StaggeredGridLayoutManager doesn't seem to allow customising a cell width or span multiple columns (except full span) for vertical orientation.

    enter image description here

    What is a preferred LayoutManager for organising cells as shown above?

    P.S. I just want to know how to customise cell width and not height with StaggeredGridLayoutManager. I know height can be customised as implemented in this sample.

    public class VerticalStaggeredGridFragment extends RecyclerFragment {
    
        public static VerticalStaggeredGridFragment newInstance() {
            VerticalStaggeredGridFragment fragment = new VerticalStaggeredGridFragment();
            Bundle args = new Bundle();
            fragment.setArguments(args);
            return fragment;
        }
    
        @Override
        protected RecyclerView.LayoutManager getLayoutManager() {
            return new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
        }
    
        @Override
        protected RecyclerView.ItemDecoration getItemDecoration() {
            return new InsetDecoration(getActivity());
        }
    
        @Override
        protected int getDefaultItemCount() {
            return 100;
        }
    
        @Override
        protected SimpleAdapter getAdapter() {
            return new SimpleStaggeredAdapter();
        }
    }
    

    Adapter

    public class SimpleStaggeredAdapter extends SimpleAdapter {
        @Override
        public void onBindViewHolder(VerticalItemHolder itemHolder, int position) {
            super.onBindViewHolder(itemHolder, position);
    
            final View itemView = itemHolder.itemView;
            if (position % 4 == 0) {
                int height = itemView.getContext().getResources()
                        .getDimensionPixelSize(R.dimen.card_staggered_height);
                itemView.setMinimumHeight(height);
            } else {
                itemView.setMinimumHeight(0);
            }
        }
    }
    

    itemView.setMinimumWidth(customWidth) doesn't affect cell width.

    To simplify, I update my grid layout to

    enter image description here

    How to increase cell 0 width as compared to cell 1 in a StaggeredGridLayoutManager or any other layout manager?

  • random
    random about 8 years
    Are you able to set cell width to 1/4 or 3/4 or say specific size in dp for a grid with span size 2? I was able to set custom height but not width with staggeredgridlayoutmanager. You'll notice cells in my image are not of equal width in the 1st row. I will try your solution too in some time and get back.
  • Farshad Tahmasbi
    Farshad Tahmasbi about 8 years
    That's exactly what I did if you check onCreateViewHolder method.
  • random
    random about 8 years
    Can you update code in your answer for the grid image i provided? I wont be able to try your solution until tomorrow.
  • random
    random about 8 years
    NaN I tried your solution. Unfortunately I cannot change cell width in the first row with the approach. I updated my question to simplify layout. Thanks a lot for your interest. +1 for the effort.
  • Vinay Vissh
    Vinay Vissh over 7 years
    Thanks a lot!! The last part about gridlayout solved my problem :)
  • Vivek Solanki
    Vivek Solanki almost 7 years
    It was super great help. Shout out to open communities :D
  • blueware
    blueware about 6 years
    I downloaded the file you mentioned, included it in my project, made the change you mentioned but when scroll through items, the app crashes with this: java.lang.NullPointerException: Attempt to invoke virtual method 'int android.view.View.getTop()' on a null object reference
  • kristyna
    kristyna about 6 years
    @blueware Where Can you tell me where? Which file on what line?
  • blueware
    blueware about 6 years
    file: SpannableGridLayoutManger, no exact line. If you want I can attach my stacktrace for that exception.
  • kristyna
    kristyna about 6 years
    @blueware you can put your stacktrace to Pastebin and share the url
  • Taufik Nur Rahmanda
    Taufik Nur Rahmanda over 5 years
    can I have getSpanSize inside onBindViewHolder so I know which item from my data should have different span size?
  • Farshad Tahmasbi
    Farshad Tahmasbi over 5 years
    @TaufikNurRahmanda GridLayoutManager.SpanSizeLookup is an abstract class and YOU are telling it what it should return, So you undoubtedly know the pattern, In case that you insist on using getSpanSize method, just pass an instance to the adapter!
  • Vishal Yadav
    Vishal Yadav about 5 years
    @kristyna ... recyclerView takes lots of space from top please help me out
  • kristyna
    kristyna about 5 years
    @VishalYadav I would have to see your code. Any padding you forgot to delete in the code or xml? Maybe the image on its own has some blank space?
  • Yudi karma
    Yudi karma almost 5 years
    @VishalYadav i have same isue, recyclerView takes lots of space from top, you are already fix it ? please help me too
  • Yudi karma
    Yudi karma almost 5 years
    @kristyna you found the problem about this issue ? "recyclerView takes lots of space from top"
  • Yudi karma
    Yudi karma almost 5 years
  • Tgo1014
    Tgo1014 almost 4 years
    Just wanted to talk you. This lib is amazing and exactly what I needed. I spend some days before finding something like this.
  • Nabin
    Nabin almost 4 years
    Thanks @Tgo1014. If you want to talk to me, please send me an email.
  • Tgo1014
    Tgo1014 almost 4 years
    ops, I meant thank you instead of talk you. And thanks too for making yourself available for talking on email :)
  • sejn
    sejn over 3 years
    @FarshadTahmasbi Can you have a look into this stackoverflow.com/questions/64607773/…
  • neo
    neo about 3 years
    that was help me alot