Items are not the same width when using RecyclerView GridLayoutManager to make column spacing by ItemDecoration

13,096

Solution 1

I found the reason of the problem by myself. The offset that made in ItemDecoration is regarded as a part of item's dimensions(width and height)!

Let's take a look at the sample code and screen capture in the question above. The width of the screen capture is 480 pixels, and here is 3 columns, each item's width is 480/3 = 160 pixels. In SpacingDecoration, I add a left offset (20 pixels) on the first and second column, so the content's width of first and second column item is 160-20=140, then I add both left and right offset on the 3rd column item, so the content's width of 3rd column item is 160-20-20=120.

Now we want to make each item's content(the colored rectangle) has the same width, we must calculate how much each column item divide the total spacing of one row, but I am poor to write detailed analysis, so here I write a rough calculating process, you can pass it and jump to the conclusion.

spacing = 20
columnCount = 3
rowWidth = 480
itemWidth = rowWidth / columnCount
itemOccupiedSpacing = (spacing * (columnCount + 1)) / columnCount = spacing + spacing * (1/columnCount)
itemContentWidth = itemWidth - itemOccupiedSpacing

firstItemLeftOffset = spacing = spacing * (3/columnCount)
firstItemRightOffset = itemOccupiedSpacing - spacing = spacing * (1/columnCount)
secondItemLeftOffset = spacing - firstRightOffset = spacing * (2/columnCount)
secondItemRightOffset = itemOccupiedSpacing - secondLeftOffset = spacing * (2/columnCount)
thirdItemLeftOffset = itemOccupiedSpacing - secondLeftOffset = spacing * (1/columnCount)
thirdItemRightOffset = spacing = spacing * (3/columnCount)

We can conclude :

itemLeftOffset = spacing * ((columnCount - colunmnIndex) / columnCount)
itemRightOffset = spacing * ((colunmnIndex + 1) / columnCount)

colunmnIndex is greater than 0 and less than columnCount.


Here is my custom ItemDecoration for spacing, it works well with LinearLayoutManager,GridLayoutManager and StaggeredGridLayoutManager, all items are the same width. You can use it directly in your code.

public class SpacingDecoration extends ItemDecoration {

    private int mHorizontalSpacing = 0;
    private int mVerticalSpacing = 0;
    private boolean mIncludeEdge = false;

    public SpacingDecoration(int hSpacing, int vSpacing, boolean includeEdge) {
        mHorizontalSpacing = hSpacing;
        mVerticalSpacing = vSpacing;
        mIncludeEdge = includeEdge;
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        // Only handle the vertical situation
        int position = parent.getChildAdapterPosition(view);
        if (parent.getLayoutManager() instanceof GridLayoutManager) {
            GridLayoutManager layoutManager = (GridLayoutManager) parent.getLayoutManager();
            int spanCount = layoutManager.getSpanCount();
            int column = position % spanCount;
            getGridItemOffsets(outRect, position, column, spanCount);
        } else if (parent.getLayoutManager() instanceof StaggeredGridLayoutManager) {
            StaggeredGridLayoutManager layoutManager = (StaggeredGridLayoutManager) parent.getLayoutManager();
            int spanCount = layoutManager.getSpanCount();
            StaggeredGridLayoutManager.LayoutParams lp = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
            int column = lp.getSpanIndex();
            getGridItemOffsets(outRect, position, column, spanCount);
        } else if (parent.getLayoutManager() instanceof LinearLayoutManager) {
            outRect.left = mHorizontalSpacing;
            outRect.right = mHorizontalSpacing;
            if (mIncludeEdge) {
                if (position == 0) {
                    outRect.top = mVerticalSpacing;
                }
                outRect.bottom = mVerticalSpacing;
            } else {
                if (position > 0) {
                    outRect.top = mVerticalSpacing;
                }
            }
        }
    }

    private void getGridItemOffsets(Rect outRect, int position, int column, int spanCount) {
        if (mIncludeEdge) {
            outRect.left = mHorizontalSpacing * (spanCount - column) / spanCount;
            outRect.right = mHorizontalSpacing * (column + 1) / spanCount;
            if (position < spanCount) {
                outRect.top = mVerticalSpacing;
            }
            outRect.bottom = mVerticalSpacing;
        } else {
            outRect.left = mHorizontalSpacing * column / spanCount;
            outRect.right = mHorizontalSpacing * (spanCount - 1 - column) / spanCount;
            if (position >= spanCount) {
                outRect.top = mVerticalSpacing;
            }
        }
    }
}

Solution 2

I've wrote a more robust ItemDecoration based on @AvatarQing 's answer: SCommonItemDecoration

You can set same vertical or horizontal space between items, besides you can set different space to different type of items.

enter image description here

Share:
13,096
Riki
Author by

Riki

Updated on June 14, 2022

Comments

  • Riki
    Riki about 2 years

    I'm trying to use RecyclerView and GridLayoutManager to make a 3 columns grid, and I use ItemDecoration to make column spacing, now the problem is the item's width in third column is smaller than the item in first and second column! See the screen capture below.

    enter image description here

    If I don't add the custom ItemDecoration to RecyclerView, everything is OK.

    Here is my code:

    MainActivity.java:

    public class MainActivity extends AppCompatActivity {
    
        private RecyclerView mRecyclerView;
        private MyAdapter mAdapter;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
            mAdapter = new MyAdapter();
            mRecyclerView.setAdapter(mAdapter);
    
            GridLayoutManager gridLayoutManager = new GridLayoutManager(this, 3);
            mRecyclerView.setLayoutManager(gridLayoutManager);
    
            int horizontalSpacing = 20;
            int verticalSpacing = 10;
            SpacingDecoration decoration = new SpacingDecoration(horizontalSpacing, verticalSpacing, true);
            mRecyclerView.addItemDecoration(decoration);
        }
    
    
        private static class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    
            private int[] mColors = new int[]{Color.RED, Color.BLUE, Color.MAGENTA};
    
            private static class ItemHolder extends RecyclerView.ViewHolder {
    
                public MyTextView title;
    
                public ItemHolder(View itemView) {
                    super(itemView);
                    title = (MyTextView) itemView.findViewById(android.R.id.text1);
                    title.setTextColor(Color.WHITE);
                }
            }
    
            @Override
            public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
                View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
                ItemHolder holder = new ItemHolder(itemView);
                holder.itemView.setOnClickListener(itemClickListener);
                return holder;
            }
    
            @Override
            public void onBindViewHolder(RecyclerView.ViewHolder rHolder, int position) {
                ItemHolder holder = (ItemHolder) rHolder;
    
                holder.title.setText(String.format("[%d]width:%d", position, holder.itemView.getWidth()));
                holder.itemView.setBackgroundColor(mColors[position % mColors.length]);
                holder.itemView.setTag(position);
                holder.title.setTag(position);
            }
    
            @Override
            public int getItemCount() {
                return 50;
            }
    
            private View.OnClickListener itemClickListener = new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    int position = (int) v.getTag();
                    showText(v.getContext(), String.format("[%d]->width:%d", position, v.getWidth()));
                }
            };
    
        }
    
        public static class SpacingDecoration extends RecyclerView.ItemDecoration {
    
            private int mHorizontalSpacing = 5;
            private int mVerticalSpacing = 5;
            private boolean isSetMargin = true;
    
            public SpacingDecoration(int hSpacing, int vSpacing, boolean setMargin) {
                isSetMargin = setMargin;
                mHorizontalSpacing = hSpacing;
                mVerticalSpacing = vSpacing;
            }
    
            @Override
            public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
                boolean isSetMarginLeftAndRight = this.isSetMargin;
                int bottomOffset = mVerticalSpacing;
                int leftOffset = 0;
                int rightOffset = 0;
    
                RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams();
                if (parent.getLayoutManager() instanceof GridLayoutManager) {
                    GridLayoutManager lm = (GridLayoutManager) parent.getLayoutManager();
                    GridLayoutManager.LayoutParams gridLp = (GridLayoutManager.LayoutParams) lp;
    
                    if (gridLp.getSpanSize() == lm.getSpanCount()) {
                        // Current item is occupied the whole row
                        // We just need to care about margin left and right now
                        if (isSetMarginLeftAndRight) {
                            leftOffset = mHorizontalSpacing;
                            rightOffset = mHorizontalSpacing;
                        }
                    } else {
                        // Current item isn't occupied the whole row
                        if (gridLp.getSpanIndex() > 0) {
                            // Set space between items in one row
                            leftOffset = mHorizontalSpacing;
                        } else if (gridLp.getSpanIndex() == 0 && isSetMarginLeftAndRight) {
                            // Set left margin of a row
                            leftOffset = mHorizontalSpacing;
                        }
                        if (gridLp.getSpanIndex() == lm.getSpanCount() - gridLp.getSpanSize() && isSetMarginLeftAndRight) {
                            // Set right margin of a row
                            rightOffset = mHorizontalSpacing;
                        }
                    }
                }
                outRect.set(leftOffset, 0, rightOffset, bottomOffset);
            }
        }
    
    
        private static Toast sToast;
    
        public static void showText(Context context, String text) {
            if (sToast != null) {
                sToast.cancel();
            }
            sToast = Toast.makeText(context, text, Toast.LENGTH_LONG);
            sToast.show();
        }
    }
    

    activity_main.xml

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent">
    
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    
    </RelativeLayout>
    

    item.xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:orientation="vertical">
    
        <com.example.liuqing.rvgldemo.MyTextView
            android:id="@android:id/text1"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:padding="5dp"
            android:textColor="#ffffff"
            android:textAppearance="?android:attr/textAppearanceMedium"/>
    </LinearLayout>
    

    MyTextView.java

    public class MyTextView extends TextView {
    
        public MyTextView(Context context) {
            super(context);
        }
    
        public MyTextView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }
    
        @Override
        public void onWindowFocusChanged(boolean hasWindowFocus) {
            super.onWindowFocusChanged(hasWindowFocus);
            if (hasWindowFocus) {
                setText("[" + getTag() + "]width:" + getWidth());
            }
        }
    }
    

    It will be much appreciate if someone can explain this problem.