Why is the space between my RecyclerView items not the same? - android

I use ItemDecoration to set the space between the grid RecyclerView items, and I want the space between each item to be equal.
ItemDecoration
public class SpaceItemDecoration extends RecyclerView.ItemDecoration {
private int space;
public SpaceItemDecoration(int space) {
this.space = space;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
if (parent.getLayoutManager() instanceof GridLayoutManager) {
GridLayoutManager.LayoutParams layoutParams = (GridLayoutManager.LayoutParams) view.getLayoutParams();
GridLayoutManager gridLayoutManager = (GridLayoutManager) parent.getLayoutManager();
int spanSize = layoutParams.getSpanSize();
int spanIndex = layoutParams.getSpanIndex();
int totalSpanSize = gridLayoutManager.getSpanCount();
if (spanIndex == 0) {//left
outRect.left = space;
outRect.right = space / 2;
} else if (spanSize + spanIndex == totalSpanSize) {//right
outRect.right = space;
outRect.left = space / 2;
} else if (spanIndex > 0 && spanSize + spanIndex < totalSpanSize) {
outRect.left = space / 2;
outRect.right = space / 2;
}
outRect.bottom = space;
}
}
RecyclerView
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
item xml
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="90dp"
android:layout_height="90dp"
android:background="#ff6600"
android:orientation="vertical">
</LinearLayout>

Try this,
int spanCount;
final private Context mContext =YourActivity.this;
int value = activity.getResources().getConfiguration().orientation;
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
recycler_view=(RecyclerView)findViewById(R.id.recycler_view);
recycler_view.setLayoutManager(new GridLayoutManager(mContext, 2));
if (value == Configuration.ORIENTATION_PORTRAIT) {
spanCount = 2;
}
else if (value == Configuration.ORIENTATION_LANDSCAPE) {
spanCount = 3;
}
int spacing_left = 50; // 50px
int spacing_top=10;
recycler_view.addItemDecoration(new GridSpacingItemDecoration(spanCount, spacing_left, spacing_top));
}
#Override
protected void onResume() {
if (value == Configuration.ORIENTATION_PORTRAIT) {
spanCount = 2;
}
else if (value == Configuration.ORIENTATION_LANDSCAPE) {
spanCount = 3;
}
super.onResume();
}
GridSpacingItemDecoration:
/* set spacing for grid view */
public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration
{
final private int spanCount,spacing,spacing_top;
final private boolean includeEdge;
public GridSpacingItemDecoration(int spanCount, int spacing_left, int spacing_top)
{
this.spanCount = spanCount;
this.spacing = spacing_left;
this.includeEdge = true;
this.spacing_top=spacing_top;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)
{
int position = parent.getChildAdapterPosition(view); // item phases_position
int column = position % spanCount; // item column
if (includeEdge)
{
outRect.left = spacing - column * spacing / spanCount; // spacing - column * ((1f / spanCount) * spacing)
outRect.right = (column + 1) * spacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing)
if (position < spanCount)
{ // top edge
outRect.top = spacing_top;
}
outRect.bottom = spacing_top; // item bottom
}
else
{
outRect.left = column * spacing / spanCount; // column * ((1f / spanCount) * spacing)
outRect.right = spacing - (column + 1) * spacing / spanCount; // spacing - (column + 1) * ((1f / spanCount) * spacing)
if (position >= spanCount)
{
outRect.top = spacing_top; // item top
}
}
}
}

Use GridItemDecoration:
import android.content.Context;
import android.graphics.Rect;
import android.support.annotation.DimenRes;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.View;
public class GridItemDecoration extends RecyclerView.ItemDecoration {
private int mItemOffset;
public GridItemDecoration(int itemOffset) {
mItemOffset = itemOffset;
}
public GridItemDecoration(#NonNull Context context, #DimenRes int itemOffsetId) {
this(context.getResources().getDimensionPixelSize(itemOffsetId));
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
outRect.set(mItemOffset, mItemOffset, mItemOffset, mItemOffset);
}
}
From your Activity:
RecyclerView mRecyclerView;
RecyclerView.LayoutManager mLayoutManager;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
................
........................
mRecyclerView = (RecyclerView) findViewById(R.id.recyclerview);
mRecyclerView.setHasFixedSize(true);
// Grid layout manager
mLayoutManager = new GridLayoutManager(mContext, 3); // spanCount = 3 for 3 columns
mRecyclerView.setLayoutManager(mLayoutManager);
// Grid item spacing
GridItemDecoration itemDecoration = new GridItemDecoration(mContext, R.dimen.grid_item_spacing); // R.dimen.grid_item_spacing is 2dp
mRecyclerView.addItemDecoration(itemDecoration);
............
......................
}

Why don't you try to remove the code for the custom spacing, and use the grid parameters android:horizontalSpacing and android:verticalSpacing?

Add android:scaleType="fitXY" inside your item xml which will scale the image using FILL so that all items occupy same area.

Related

How to remove margin between RecyclerView items

I am trying to make a screen as shown in the picture, as you can see there is no spacing between items in the RecyclerView.
Desired Image:
So far my item_layout for each of the item in recycler view is:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<android.support.v7.widget.CardView
android:layout_width="120dp"
android:layout_margin="0dp"
android:layout_height="190dp"
app:cardCornerRadius="0dp">
<LinearLayout
android:id="#+id/colorBox"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="bottom"
android:orientation="horizontal">
<TextView
android:id="#+id/colorNameBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:paddingBottom="10dp"
android:paddingLeft="10dp"
android:text="asd"
android:textColor="#fff"
android:textSize="16dp"
>
</TextView>
</LinearLayout>
</android.support.v7.widget.CardView>
my ItemDecoration in ColorShadesActivity is given below:
public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {
private int spanCount; //3
private int spacing; //0
private boolean includeEdge; //false
public GridSpacingItemDecoration(int spanCount, int spacing, boolean includeEdge) {
this.spanCount = spanCount;
this.spacing = spacing;
this.includeEdge = includeEdge;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view); // item position
int column = position % spanCount; // item column
if (includeEdge) {
outRect.left = spacing - column * spacing / spanCount; // spacing - column * ((1f / spanCount) * spacing)
outRect.right = (column + 1) * spacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing)
if (position < spanCount) { // top edge
outRect.top = spacing;
}
outRect.bottom = spacing; // item bottom
} else {
outRect.left = spacing; // column * ((1f / spanCount) * spacing)
outRect.right = spacing; // spacing - (column + 1) * ((1f / spanCount) * spacing)
if (position >= spanCount) {
outRect.top = spacing; // item top
}
}
}
}
But the result I am getting is:
To show GridLayout using RecyclerView You need to make only few steps:
tell RecyclerView that You want to show the elements in GridLayout
declare how many columns You would like to use in GridLayout
set the same height for every element in case You would like to show all elements in equal rows
Here is a working sample code in simplified way to achieve that what You want. I am dynamically change item's height but You can use xml layout for that.
public class MainActivity extends AppCompatActivity {
RecyclerView recyclerView;
RecyclerView.LayoutManager layoutManager;
private Random random;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
random = new Random(System.currentTimeMillis());
recyclerView = findViewById(R.id.recyclerView);
layoutManager = new GridLayoutManager(this, 2);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(new RecyclerView.Adapter() {
#NonNull
#Override
public RecyclerView.ViewHolder onCreateViewHolder(#NonNull ViewGroup viewGroup, int i) {
View inflate = LayoutInflater.from(viewGroup.getContext()).inflate(android.R.layout.simple_list_item_1, viewGroup, false);
ViewGroup.LayoutParams layoutParams = inflate.getLayoutParams();
layoutParams.height = (int) (getResources().getDisplayMetrics().scaledDensity * 56);
inflate.setLayoutParams(layoutParams);
int randomColor = random.nextInt(Colors.COLORS.length);
inflate.setBackgroundColor(Color.parseColor(Colors.COLORS[randomColor]));
return new RecyclerViewViewHolder(inflate);
}
#Override
public void onBindViewHolder(#NonNull RecyclerView.ViewHolder viewHolder, int i) {
((TextView) viewHolder.itemView.findViewById(android.R.id.text1)).setText(Cheeses.CHEESES[i]);
}
#Override
public int getItemCount() {
return Cheeses.CHEESES.length;
}
});
}
private class RecyclerViewViewHolder extends RecyclerView.ViewHolder {
RecyclerViewViewHolder(#NonNull View itemView) {
super(itemView);
}
}
}
Source on Github

StaggeredLayoutManager disturbs positions of cells during adjustment in RecyclerView

StaggeredLayoutManager sometimes makes cells position disordered during scroll when trying to adjust them. I've a HeaderView and NormalViews in my RecyclerView. Following is the setup code:
int spacing = (int) getResources().getDimension(R.dimen.post_item_spacing);
mBindings.rvPosts.addItemDecoration(new PostsEqualGapItemDecoration(AppConstants.POSTS_SPAN_COUNT, spacing));
mPostsLayoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
mBindings.rvPosts.setLayoutManager(mPostsLayoutManager);
mProfileAdapter.setRecyclerView(mBindings.rvPosts);
mBindings.rvPosts.setAdapter(mAdapter);
mBindings.rvPosts.setNestedScrollingEnabled(false);
ItemDecoration class:
public class PostsEqualGapItemDecoration extends RecyclerView.ItemDecoration {
private int spanCount;
private int spacing;
public PostsEqualGapItemDecoration(int spanCount, int spacing) {
this.spanCount = spanCount;
this.spacing = spacing;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
StaggeredGridLayoutManager.LayoutParams layoutParams = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
if (layoutParams.isFullSpan()) {
outRect.set(0, 0, 0, 0);
} else {
int spanIndex = layoutParams.getSpanIndex();
int layoutPosition = layoutParams.getViewLayoutPosition();
int itemCount = parent.getAdapter().getItemCount();
boolean leftEdge = spanIndex == 0;
boolean rightEdge = spanIndex == (spanCount - 1);
boolean topEdge = spanIndex < spanCount;
boolean bottomEdge = layoutPosition >= (itemCount - spanCount);
int halfSpacing = spacing / 2;
/**
* Updated values to keep cells width same and spacing from left and right
* most item to be double than gap between items themselves.
*/
outRect.set(
leftEdge ? spacing * 2 : rightEdge ? 0 : spacing,
topEdge ? spacing : halfSpacing,
rightEdge ? spacing * 2 : leftEdge ? 0 : spacing,
bottomEdge ? spacing : 0
);
}
}
}
Screenshot:
You have to write custom item decoration for that case to your child item views
public class SpacesItemDecoration extends RecyclerView.ItemDecoration {
private int space;
public SpacesItemDecoration(int space) {
this.space = space;
}
#Override
public void getItemOffsets(Rect outRect, View view,
RecyclerView parent, RecyclerView.State state) {
outRect.left = space;
outRect.right = space;
outRect.bottom = space;
// Add top margin only for the first item to avoid double space between items
if (parent.getChildLayoutPosition(view) == 0) {
outRect.top = space;
} else {
outRect.top = 0;
}
}
}

How to make GridLayoutManager use dynamic spaces? [duplicate]

This question already has answers here:
Different (dynamic) items height in GridLayoutManager
(2 answers)
Closed 3 years ago.
I am currently using a GridLayoutManager for a RecyclerView which displays Previews of different Articles, which have different text lengths and therefor different item heights. The question is, how can I use the full height and dont have too much spacing?
Use StaggeredGridLayoutManager!
https://developer.android.com/reference/android/support/v7/widget/StaggeredGridLayoutManager
Try to set your layout manager like this for recyclerview.
adapter = new CustomAdapter(R.layout.layout, list, context);
final StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(numberOfrows, StaggeredGridLayoutManager.HORIZONTAL);
recyclerView_sk.setHasFixedSize(true);
recyclerView_sk.addItemDecoration(new SpacesItemDecorationNew(0));//For equal distribution of columns.
recyclerView_sk.setLayoutManager(layoutManager);
recyclerView_sk.setAdapter(adapter);
SpacesItemDecorationNew
public class SpacesItemDecorationNew extends RecyclerView.ItemDecoration {
private int mItemOffset;
public SpacesItemDecorationNew(int itemOffset) {
mItemOffset = itemOffset;
}
public SpacesItemDecorationNew(#NonNull Context context, #DimenRes int itemOffsetId) {
this(context.getResources().getDimensionPixelSize(itemOffsetId));
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
outRect.set(mItemOffset, mItemOffset, mItemOffset, mItemOffset);
}
}
Also, you can try with this
mDashboardAdapter = new DashboardAdapter(getActivity(), mDashBoardList, imageId);
RecyclerView.LayoutManager mLayoutManager = new GridLayoutManager(getActivity(), 4);
grid.setLayoutManager(mLayoutManager);
grid.addItemDecoration(new GridSpacingItemDecoration(4, 10, true));
grid.setItemAnimator(new DefaultItemAnimator());
grid.setAdapter(mDashboardAdapter);
public static class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {
private int spanCount;
private int spacing;
private boolean includeEdge;
public GridSpacingItemDecoration(int spanCount, int spacing, boolean includeEdge) {
this.spanCount = spanCount;
this.spacing = spacing;
this.includeEdge = includeEdge;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view); // item position
int column = position % spanCount; // item column
if (includeEdge) {
outRect.left = spacing - column * spacing / spanCount; // spacing - column * ((1f / spanCount) * spacing)
outRect.right = (column + 1) * spacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing)
if (position < spanCount) { // top edge
outRect.top = spacing;
}
outRect.bottom = spacing; // item bottom
} else {
outRect.left = column * spacing / spanCount; // column * ((1f / spanCount) * spacing)
outRect.right = spacing - (column + 1) * spacing / spanCount; // spacing - (column + 1) * ((1f / spanCount) * spacing)
if (position >= spanCount) {
outRect.top = spacing; // item top
}
}
}
}

How to remove this padding in Recyclerview?

CODE :
private void configViews(View view) {
int spanCount = 3; // 3 columns
int spacing = 3;
boolean includeEdge = true;
mRecyclerView = (RecyclerView) view.findViewById(R.id.rv_flower_red);
mRecyclerView.addItemDecoration(new GridSpacingItemDecoration(spanCount, spacing, includeEdge));
mRecyclerView.setHasFixedSize(true);
mRecyclerView.setRecycledViewPool(new RecyclerView.RecycledViewPool());
mRecyclerView.setLayoutManager(new GridLayoutManager(getActivity().getApplicationContext(), spanCount));
mFlowerAdapter = new FlowerAdapter_grid(this);
mRecyclerView.setAdapter(mFlowerAdapter);
}
public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {
private int spanCount;
private int spacing;
private boolean includeEdge;
public GridSpacingItemDecoration(int spanCount, int spacing, boolean includeEdge) {
this.spanCount = spanCount;
this.spacing = spacing;
this.includeEdge = includeEdge;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view); // item position
int column = position % spanCount; // item column
if (includeEdge) {
outRect.left = spacing - column * spacing / spanCount; // spacing - column * ((1f / spanCount) * spacing)
outRect.right = (column + 1) * spacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing)
if (position < spanCount) { // top edge
outRect.top = spacing;
}
outRect.bottom = spacing; // item bottom
} else {
outRect.left = column * spacing / spanCount; // column * ((1f / spanCount) * spacing)
outRect.right = spacing - (column + 1) * spacing / spanCount; // spacing - (column + 1) * ((1f / spanCount) * spacing)
if (position >= spanCount) {
outRect.top = spacing; // item top
}
}
}
}
The core of that CODE is from https://stackoverflow.com/a/30701422/6736285
But when i use that code,
Gridlayout has padding on image's top or bottom.
As you see, there are too much vertical padding.
Question : How do i remove that padding? Please Would you let me know answer?
Here is my FlowerAdapter_grid:
public class FlowerAdapter_grid extends RecyclerView.Adapter<FlowerAdapter_grid.Holder> {
private static final Object TAG = FlowerAdapter_grid.class.getSimpleName();
private final FlowerClickListener mListener;
private List<Flower> mFlowers;
public FlowerAdapter_grid(FlowerClickListener listener) {
mFlowers = new ArrayList<>();
mListener = listener;
}
#Override
public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
View row = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_item_grid, null, false);
return new Holder(row);
}
#Override
public void onBindViewHolder(Holder holder, int position) {
Flower currFlower = mFlowers.get(position);
Glide.with(holder.itemView.getContext())
.load(currFlower.getImage())
.thumbnail(0.5f)
.override(300, 300)
.centerCrop()
.into(holder.mImage);
}
#Override
public int getItemCount() {
return mFlowers.size();
}
public void addFlower(Flower flower) {
mFlowers.add(flower);
notifyDataSetChanged();
}
public void clear(){
mFlowers.clear();
notifyDataSetChanged();
}
public Flower getSelectedFlower(int position) {
return mFlowers.get(position);
}
public class Holder extends RecyclerView.ViewHolder implements View.OnClickListener {
private ImageView mImage;
public Holder(View itemView) {
super(itemView);
mImage = (ImageView) itemView.findViewById(R.id.iv_photo);
itemView.setOnClickListener(this);
}
#Override
public void onClick(View view) {
mListener.onClick(getLayoutPosition());
}
}
public interface FlowerClickListener {
void onClick(int position);
}
}
and
row_item_grid
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/iv_photo"
/>
</LinearLayout>
and My fragment layout
<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/swipeContainer_red"
>
<android.support.v7.widget.RecyclerView
android:id="#+id/rv_flower_red"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none">
</android.support.v7.widget.RecyclerView>
</android.support.v4.widget.SwipeRefreshLayout>
After trying, it returned this. What was problem?
I too faced this issue when I started working with recycler view.
Issue is the you are setting the grid as vertical scrolling grid and setting the row count to 3.
So it resizes your row_item_grid layout to fit 3 items in a row. So the issue here is you have used match parent for the height parent LinearLayout in the same Layout. but the image is 300dpx300dp which is at the center of the LinearLayout.
So that extra padding is because of your LinearLayout.
Instead you can simple have
row_item_grid.xml
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/iv_photo"
/>
Honestly you dont need parent layouts when your view consists of single layout. I didnt check your itemDecoration code though, for my knowledege, you wont even need it for vertical spacing if you follow these steps

Android Recyclerview GridLayoutManager column spacing

How do you set the column spacing with a RecyclerView using a GridLayoutManager?
Setting the margin/padding inside my layout has no effect.
Following code works well, and each column has same width:
public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {
private int spanCount;
private int spacing;
private boolean includeEdge;
public GridSpacingItemDecoration(int spanCount, int spacing, boolean includeEdge) {
this.spanCount = spanCount;
this.spacing = spacing;
this.includeEdge = includeEdge;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view); // item position
int column = position % spanCount; // item column
if (includeEdge) {
outRect.left = spacing - column * spacing / spanCount; // spacing - column * ((1f / spanCount) * spacing)
outRect.right = (column + 1) * spacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing)
if (position < spanCount) { // top edge
outRect.top = spacing;
}
outRect.bottom = spacing; // item bottom
} else {
outRect.left = column * spacing / spanCount; // column * ((1f / spanCount) * spacing)
outRect.right = spacing - (column + 1) * spacing / spanCount; // spacing - (column + 1) * ((1f / spanCount) * spacing)
if (position >= spanCount) {
outRect.top = spacing; // item top
}
}
}
}
Usage
1. no edge
int spanCount = 3; // 3 columns
int spacing = 50; // 50px
boolean includeEdge = false;
recyclerView.addItemDecoration(new GridSpacingItemDecoration(spanCount, spacing, includeEdge));
2. with edge
int spanCount = 3; // 3 columns
int spacing = 50; // 50px
boolean includeEdge = true;
recyclerView.addItemDecoration(new GridSpacingItemDecoration(spanCount, spacing, includeEdge));
RecyclerViews support the concept of ItemDecoration: special offsets and drawing around each element. As seen in this answer, you can use
public class SpacesItemDecoration extends RecyclerView.ItemDecoration {
private int space;
public SpacesItemDecoration(int space) {
this.space = space;
}
#Override
public void getItemOffsets(Rect outRect, View view,
RecyclerView parent, RecyclerView.State state) {
outRect.left = space;
outRect.right = space;
outRect.bottom = space;
// Add top margin only for the first item to avoid double space between items
if (parent.getChildLayoutPosition(view) == 0) {
outRect.top = space;
} else {
outRect.top = 0;
}
}
}
Then add it via
mRecyclerView = (RecyclerView) rootView.findViewById(R.id.my_recycler_view);
int spacingInPixels = getResources().getDimensionPixelSize(R.dimen.spacing);
mRecyclerView.addItemDecoration(new SpacesItemDecoration(spacingInPixels));
The following is the step-by-step simple solution if you want the equal spacing around items and equal item sizes.
ItemOffsetDecoration
public class ItemOffsetDecoration extends RecyclerView.ItemDecoration {
private int mItemOffset;
public ItemOffsetDecoration(int itemOffset) {
mItemOffset = itemOffset;
}
public ItemOffsetDecoration(#NonNull Context context, #DimenRes int itemOffsetId) {
this(context.getResources().getDimensionPixelSize(itemOffsetId));
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
outRect.set(mItemOffset, mItemOffset, mItemOffset, mItemOffset);
}
}
Implementation
In your source code, add ItemOffsetDecoration to your RecyclerView.
Item offset value should be half size of the actual value you want to add as space between items.
mRecyclerView.setLayoutManager(new GridLayoutManager(context, NUM_COLUMNS);
ItemOffsetDecoration itemDecoration = new ItemOffsetDecoration(context, R.dimen.item_offset);
mRecyclerView.addItemDecoration(itemDecoration);
Also, set item offset value as padding for itsRecyclerView, and specify android:clipToPadding=false.
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerview_grid"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="#dimen/item_offset"/>
Try this. It'll take care of equal spacing all around. Works both with List, Grid, and StaggeredGrid.
Edited
The updated code should handle most of the corner cases with spans, orientation, etc.
Note that if using setSpanSizeLookup() with GridLayoutManager, setting setSpanIndexCacheEnabled() is recommended for performance reasons.
Note, it seems that with StaggeredGrid, there's seems to be a bug where the index of the children gets wacky and hard to track so the code below might not work very well with StaggeredGridLayoutManager.
public class ListSpacingDecoration extends RecyclerView.ItemDecoration {
private static final int VERTICAL = OrientationHelper.VERTICAL;
private int orientation = -1;
private int spanCount = -1;
private int spacing;
private int halfSpacing;
public ListSpacingDecoration(Context context, #DimenRes int spacingDimen) {
spacing = context.getResources().getDimensionPixelSize(spacingDimen);
halfSpacing = spacing / 2;
}
public ListSpacingDecoration(int spacingPx) {
spacing = spacingPx;
halfSpacing = spacing / 2;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
if (orientation == -1) {
orientation = getOrientation(parent);
}
if (spanCount == -1) {
spanCount = getTotalSpan(parent);
}
int childCount = parent.getLayoutManager().getItemCount();
int childIndex = parent.getChildAdapterPosition(view);
int itemSpanSize = getItemSpanSize(parent, childIndex);
int spanIndex = getItemSpanIndex(parent, childIndex);
/* INVALID SPAN */
if (spanCount < 1) return;
setSpacings(outRect, parent, childCount, childIndex, itemSpanSize, spanIndex);
}
protected void setSpacings(Rect outRect, RecyclerView parent, int childCount, int childIndex, int itemSpanSize, int spanIndex) {
outRect.top = halfSpacing;
outRect.bottom = halfSpacing;
outRect.left = halfSpacing;
outRect.right = halfSpacing;
if (isTopEdge(parent, childCount, childIndex, itemSpanSize, spanIndex)) {
outRect.top = spacing;
}
if (isLeftEdge(parent, childCount, childIndex, itemSpanSize, spanIndex)) {
outRect.left = spacing;
}
if (isRightEdge(parent, childCount, childIndex, itemSpanSize, spanIndex)) {
outRect.right = spacing;
}
if (isBottomEdge(parent, childCount, childIndex, itemSpanSize, spanIndex)) {
outRect.bottom = spacing;
}
}
#SuppressWarnings("all")
protected int getTotalSpan(RecyclerView parent) {
RecyclerView.LayoutManager mgr = parent.getLayoutManager();
if (mgr instanceof GridLayoutManager) {
return ((GridLayoutManager) mgr).getSpanCount();
} else if (mgr instanceof StaggeredGridLayoutManager) {
return ((StaggeredGridLayoutManager) mgr).getSpanCount();
} else if (mgr instanceof LinearLayoutManager) {
return 1;
}
return -1;
}
#SuppressWarnings("all")
protected int getItemSpanSize(RecyclerView parent, int childIndex) {
RecyclerView.LayoutManager mgr = parent.getLayoutManager();
if (mgr instanceof GridLayoutManager) {
return ((GridLayoutManager) mgr).getSpanSizeLookup().getSpanSize(childIndex);
} else if (mgr instanceof StaggeredGridLayoutManager) {
return 1;
} else if (mgr instanceof LinearLayoutManager) {
return 1;
}
return -1;
}
#SuppressWarnings("all")
protected int getItemSpanIndex(RecyclerView parent, int childIndex) {
RecyclerView.LayoutManager mgr = parent.getLayoutManager();
if (mgr instanceof GridLayoutManager) {
return ((GridLayoutManager) mgr).getSpanSizeLookup().getSpanIndex(childIndex, spanCount);
} else if (mgr instanceof StaggeredGridLayoutManager) {
return childIndex % spanCount;
} else if (mgr instanceof LinearLayoutManager) {
return 0;
}
return -1;
}
#SuppressWarnings("all")
protected int getOrientation(RecyclerView parent) {
RecyclerView.LayoutManager mgr = parent.getLayoutManager();
if (mgr instanceof LinearLayoutManager) {
return ((LinearLayoutManager) mgr).getOrientation();
} else if (mgr instanceof GridLayoutManager) {
return ((GridLayoutManager) mgr).getOrientation();
} else if (mgr instanceof StaggeredGridLayoutManager) {
return ((StaggeredGridLayoutManager) mgr).getOrientation();
}
return VERTICAL;
}
protected boolean isLeftEdge(RecyclerView parent, int childCount, int childIndex, int itemSpanSize, int spanIndex) {
if (orientation == VERTICAL) {
return spanIndex == 0;
} else {
return (childIndex == 0) || isFirstItemEdgeValid((childIndex < spanCount), parent, childIndex);
}
}
protected boolean isRightEdge(RecyclerView parent, int childCount, int childIndex, int itemSpanSize, int spanIndex) {
if (orientation == VERTICAL) {
return (spanIndex + itemSpanSize) == spanCount;
} else {
return isLastItemEdgeValid((childIndex >= childCount - spanCount), parent, childCount, childIndex, spanIndex);
}
}
protected boolean isTopEdge(RecyclerView parent, int childCount, int childIndex, int itemSpanSize, int spanIndex) {
if (orientation == VERTICAL) {
return (childIndex == 0) || isFirstItemEdgeValid((childIndex < spanCount), parent, childIndex);
} else {
return spanIndex == 0;
}
}
protected boolean isBottomEdge(RecyclerView parent, int childCount, int childIndex, int itemSpanSize, int spanIndex) {
if (orientation == VERTICAL) {
return isLastItemEdgeValid((childIndex >= childCount - spanCount), parent, childCount, childIndex, spanIndex);
} else {
return (spanIndex + itemSpanSize) == spanCount;
}
}
protected boolean isFirstItemEdgeValid(boolean isOneOfFirstItems, RecyclerView parent, int childIndex) {
int totalSpanArea = 0;
if (isOneOfFirstItems) {
for (int i = childIndex; i >= 0; i--) {
totalSpanArea = totalSpanArea + getItemSpanSize(parent, i);
}
}
return isOneOfFirstItems && totalSpanArea <= spanCount;
}
protected boolean isLastItemEdgeValid(boolean isOneOfLastItems, RecyclerView parent, int childCount, int childIndex, int spanIndex) {
int totalSpanRemaining = 0;
if (isOneOfLastItems) {
for (int i = childIndex; i < childCount; i++) {
totalSpanRemaining = totalSpanRemaining + getItemSpanSize(parent, i);
}
}
return isOneOfLastItems && (totalSpanRemaining <= spanCount - spanIndex);
}
}
Hope it helps.
There is only one easy solution, that you can remember and implement wherever needed. No bugs, no crazy calculations. Put margin to the card / item layout and put the same size as padding to the RecyclerView:
item_layout.xml
<CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:margin="10dp">
activity_layout.xml
<RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"/>
UPDATE:
The following code will handle StaggeredGridLayoutManager, GridLayoutManager, and LinearLayoutManager.
public class SpacesItemDecoration extends RecyclerView.ItemDecoration {
private int halfSpace;
public SpacesItemDecoration(int space) {
this.halfSpace = space / 2;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
if (parent.getPaddingLeft() != halfSpace) {
parent.setPadding(halfSpace, halfSpace, halfSpace, halfSpace);
parent.setClipToPadding(false);
}
outRect.top = halfSpace;
outRect.bottom = halfSpace;
outRect.left = halfSpace;
outRect.right = halfSpace;
}
}
Then use it
mRecyclerView.addItemDecoration(new SpacesItemDecoration(mMargin));
Here is a solution that doesn't require "spanCount" (number of columns)
I use it because I use GridAutofitLayoutManager(calculates the number of columns according to the required cell size)
(beware that this will only work on GridLayoutManager)
public class GridSpacesItemDecoration extends RecyclerView.ItemDecoration {
private final boolean includeEdge;
private int spacing;
public GridSpacesItemDecoration(int spacing, boolean includeEdge) {
this.spacing = spacing;
this.includeEdge = includeEdge;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
if (parent.getLayoutManager() instanceof GridLayoutManager) {
GridLayoutManager layoutManager = (GridLayoutManager)parent.getLayoutManager();
int spanCount = layoutManager.getSpanCount();
int position = parent.getChildAdapterPosition(view); // item position
int column = position % spanCount; // item column
if (includeEdge) {
outRect.left = spacing - column * spacing / spanCount; // spacing - column * ((1f / spanCount) * spacing)
outRect.right = (column + 1) * spacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing)
if (position < spanCount) { // top edge
outRect.top = spacing;
}
outRect.bottom = spacing; // item bottom
} else {
outRect.left = column * spacing / spanCount; // column * ((1f / spanCount) * spacing)
outRect.right = spacing - (column + 1) * spacing / spanCount; // spacing - (column + 1) * ((1f / spanCount) * spacing)
if (position >= spanCount) {
outRect.top = spacing; // item top
}
}
}
}
}
Here is the GridAutofitLayoutManager is anyone is interested:
public class GridAutofitLayoutManager extends GridLayoutManager {
private int mColumnWidth;
private boolean mColumnWidthChanged = true;
public GridAutofitLayoutManager(Context context, int columnWidth)
{
/* Initially set spanCount to 1, will be changed automatically later. */
super(context, 1);
setColumnWidth(checkedColumnWidth(context, columnWidth));
}
public GridAutofitLayoutManager(Context context,int unit, int columnWidth)
{
/* Initially set spanCount to 1, will be changed automatically later. */
super(context, 1);
int pixColumnWidth = (int) TypedValue.applyDimension(unit, columnWidth, context.getResources().getDisplayMetrics());
setColumnWidth(checkedColumnWidth(context, pixColumnWidth));
}
public GridAutofitLayoutManager(Context context, int columnWidth, int orientation, boolean reverseLayout)
{
/* Initially set spanCount to 1, will be changed automatically later. */
super(context, 1, orientation, reverseLayout);
setColumnWidth(checkedColumnWidth(context, columnWidth));
}
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());
}
return columnWidth;
}
public void setColumnWidth(int newColumnWidth)
{
if (newColumnWidth > 0 && newColumnWidth != mColumnWidth)
{
mColumnWidth = newColumnWidth;
mColumnWidthChanged = true;
}
}
#Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)
{
int width = getWidth();
int height = getHeight();
if (mColumnWidthChanged && mColumnWidth > 0 && width > 0 && height > 0)
{
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;
}
super.onLayoutChildren(recycler, state);
}
}
Finally:
mDevicePhotosView.setLayoutManager(new GridAutofitLayoutManager(getContext(), getResources().getDimensionPixelSize(R.dimen.item_size)));
mDevicePhotosView.addItemDecoration(new GridSpacesItemDecoration(Util.dpToPx(getContext(), 2),true));
If you want to FIXED the size of your RecyclerView item in all devices. You can do like this
public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {
private int mSpanCount;
private float mItemSize;
public GridSpacingItemDecoration(int spanCount, int itemSize) {
this.mSpanCount = spanCount;
mItemSize = itemSize;
}
#Override
public void getItemOffsets(final Rect outRect, final View view, RecyclerView parent,
RecyclerView.State state) {
final int position = parent.getChildLayoutPosition(view);
final int column = position % mSpanCount;
final int parentWidth = parent.getWidth();
int spacing = (int) (parentWidth - (mItemSize * mSpanCount)) / (mSpanCount + 1);
outRect.left = spacing - column * spacing / mSpanCount;
outRect.right = (column + 1) * spacing / mSpanCount;
if (position < mSpanCount) {
outRect.top = spacing;
}
outRect.bottom = spacing;
}
}
recyclerview_item.xml
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="#dimen/recycler_view_item_width"
...
>
...
</LinearLayout>
dimens.xml
<dimen name="recycler_view_item_width">60dp</dimen>
Activity
int numberOfColumns = 3;
mRecyclerView.setLayoutManager(new GridLayoutManager(this, numberOfColumns));
mRecyclerView.setAdapter(...);
mRecyclerView.addItemDecoration(new GridSpacingItemDecoration(3,
getResources().getDimensionPixelSize(R.dimen.recycler_view_item_width)));
The selected answer is almost perfect, but depending on the space, items width can be not equal. (In my case it was critical). So i've ended up with this code which increases space a little bit, so items are all the same width.
class GridSpacingItemDecoration(private val columnCount: Int, #Px preferredSpace: Int, private val includeEdge: Boolean): RecyclerView.ItemDecoration() {
/**
* In this algorithm space should divide by 3 without remnant or width of items can have a difference
* and we want them to be exactly the same
*/
private val space = if (preferredSpace % 3 == 0) preferredSpace else (preferredSpace + (3 - preferredSpace % 3))
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State?) {
val position = parent.getChildAdapterPosition(view)
if (includeEdge) {
when {
position % columnCount == 0 -> {
outRect.left = space
outRect.right = space / 3
}
position % columnCount == columnCount - 1 -> {
outRect.right = space
outRect.left = space / 3
}
else -> {
outRect.left = space * 2 / 3
outRect.right = space * 2 / 3
}
}
if (position < columnCount) {
outRect.top = space
}
outRect.bottom = space
} else {
when {
position % columnCount == 0 -> outRect.right = space * 2 / 3
position % columnCount == columnCount - 1 -> outRect.left = space * 2 / 3
else -> {
outRect.left = space / 3
outRect.right = space / 3
}
}
if (position >= columnCount) {
outRect.top = space
}
}
}
}
Copied #edwardaa provided code and I make it perfect to support RTL:
public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {
private int spanCount;
private int spacing;
private boolean includeEdge;
private int headerNum;
private boolean isRtl = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == ViewCompat.LAYOUT_DIRECTION_RTL;
public GridSpacingItemDecoration(int spanCount, int spacing, boolean includeEdge, int headerNum) {
this.spanCount = spanCount;
this.spacing = spacing;
this.includeEdge = includeEdge;
this.headerNum = headerNum;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view) - headerNum; // item position
if (position >= 0) {
int column = position % spanCount; // item column
if(isRtl) {
column = spanCount - 1 - column;
}
if (includeEdge) {
outRect.left = spacing - column * spacing / spanCount; // spacing - column * ((1f / spanCount) * spacing)
outRect.right = (column + 1) * spacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing)
if (position < spanCount) { // top edge
outRect.top = spacing;
}
outRect.bottom = spacing; // item bottom
} else {
outRect.left = column * spacing / spanCount; // column * ((1f / spanCount) * spacing)
outRect.right = spacing - (column + 1) * spacing / spanCount; // spacing - (column + 1) * ((1f / spanCount) * spacing)
if (position >= spanCount) {
outRect.top = spacing; // item top
}
}
} else {
outRect.left = 0;
outRect.right = 0;
outRect.top = 0;
outRect.bottom = 0;
}
}
}
class VerticalGridSpacingDecoration(private val spacing: Int) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: State
) {
val layoutManager = parent.layoutManager as? GridLayoutManager
if (layoutManager == null || layoutManager.orientation != VERTICAL) {
return super.getItemOffsets(outRect, view, parent, state)
}
val spanCount = layoutManager.spanCount
val position = parent.getChildAdapterPosition(view)
val column = position % spanCount
with(outRect) {
left = if (column == 0) 0 else spacing / 2
right = if (column == spanCount.dec()) 0 else spacing / 2
top = if (position < spanCount) 0 else spacing
}
}
}
When using CardView for children problem with spaces between items can by solved by setting app:cardUseCompatPadding to true.
For bigger margins enlarge item elevation. CardElevation is optional (use default value).
<androidx.cardview.widget.CardView
xmlns:app="http://schemas.android.com/apk/res-auto"
app:cardUseCompatPadding="true"
app:cardElevation="2dp">
Answers above have clarified ways to set margin handling GridLayoutManager and LinearLayoutManager.
But for StaggeredGridLayoutManager, Pirdad Sakhizada's answer says: "It might not work very well with StaggeredGridLayoutManager". It should be the problem about IndexOfSpan.
You can get it by this way:
private static class MyItemDecoration extends RecyclerView.ItemDecoration {
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
int index = ((StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams()).getSpanIndex();
}
}
public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {
private int spanCount;
private int spacing;
private boolean includeEdge;
public GridSpacingItemDecoration(int spanCount, int spacing, boolean includeEdge) {
this.spanCount = spanCount;
this.spacing = spacing;
this.includeEdge = includeEdge;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
StaggeredGridLayoutManager.LayoutParams params = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
int column = params.getSpanIndex();
if (includeEdge) {
outRect.left = spacing - column * spacing / spanCount; // spacing - column * ((1f / spanCount) * spacing)
outRect.right = (column + 1) * spacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing)
if (position < spanCount) { // top edge
outRect.top = spacing;
}
outRect.bottom = spacing; // item bottom
} else {
outRect.left = column * spacing / spanCount; // column * ((1f / spanCount) * spacing)
outRect.right = spacing - (column + 1) * spacing / spanCount; // spacing - (column + 1) * ((1f / spanCount) * spacing)
if (position >= spanCount) {
outRect.top = spacing; // item top
}
}
}
}
A little bit different from edwardaa's answer, the difference is how the column is determined, because in cases such as items with various heights, the column can not be determined by simply % spanCount
yqritc's answer worked perfectly for me. I was using Kotlin however so here is the equivalent of that.
class ItemOffsetDecoration : RecyclerView.ItemDecoration {
// amount to add to padding
private val _itemOffset: Int
constructor(itemOffset: Int) {
_itemOffset = itemOffset
}
constructor(#NonNull context: Context, #DimenRes itemOffsetId: Int){
_itemOffset = context.resources.getDimensionPixelSize(itemOffsetId)
}
/**
* Applies padding to all sides of the [Rect], which is the container for the view
*/
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
outRect.set(_itemOffset, _itemOffset, _itemOffset, _itemOffset)
}
}
everything else is the same.
This is more flexible version I wrote in Kotlin, you can set your parameters in dp.
class ItemDividerGrid(private val numberOfColumns: Int, private val rowSpacingDP: Float = 0f, private val columnSpacingDP: Float = 0f, private val edgeSpacingVerticalDP: Float = 0f, private val edgeSpacingHorizontalDP: Float = 0f) : ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
val position = parent.getChildAdapterPosition(view)
val numberOfRows = (parent.adapter?.itemCount?:-1)/numberOfColumns
val column = position % numberOfColumns
val row = position / numberOfColumns
val context = view.context
///horizontal
when(column){
0 -> {
outRect.left = convertDpToPixel(edgeSpacingVerticalDP,context)
outRect.right = convertDpToPixel(columnSpacingDP/2, context)
}
numberOfColumns-1 -> {
outRect.left = convertDpToPixel(columnSpacingDP/2, context)
outRect.right = convertDpToPixel(edgeSpacingVerticalDP, context)
}
else -> {
outRect.left = convertDpToPixel(columnSpacingDP/2, context)
outRect.right = convertDpToPixel(columnSpacingDP/2, context)
}
}
//vertical
when(row){
0 -> {
outRect.top = convertDpToPixel(edgeSpacingHorizontalDP,context)
outRect.bottom = convertDpToPixel(rowSpacingDP/2, context)
}
numberOfRows -> {
outRect.top = convertDpToPixel(rowSpacingDP/2, context)
outRect.bottom = convertDpToPixel(edgeSpacingHorizontalDP, context)
}
else -> {
outRect.top = convertDpToPixel(rowSpacingDP/2, context)
outRect.bottom = convertDpToPixel(rowSpacingDP/2, context)
}
}
}
fun convertDpToPixel(dp: Float, context: Context?): Int {
return if (context != null) {
val resources = context.resources
val metrics = resources.displayMetrics
(dp * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).roundToInt()
} else {
val metrics = Resources.getSystem().displayMetrics
(dp * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).roundToInt()
}
}
}
Here is my modification of SpacesItemDecoration which can take numOfColums and space equally on top, bottom, left and right.
public class SpacesItemDecoration extends RecyclerView.ItemDecoration {
private int space;
private int mNumCol;
public SpacesItemDecoration(int space, int numCol) {
this.space = space;
this.mNumCol=numCol;
}
#Override
public void getItemOffsets(Rect outRect, View view,
RecyclerView parent, RecyclerView.State state) {
//outRect.right = space;
outRect.bottom = space;
//outRect.left = space;
//Log.d("ttt", "item position" + parent.getChildLayoutPosition(view));
int position=parent.getChildLayoutPosition(view);
if(mNumCol<=2) {
if (position == 0) {
outRect.left = space;
outRect.right = space / 2;
} else {
if ((position % mNumCol) != 0) {
outRect.left = space / 2;
outRect.right = space;
} else {
outRect.left = space;
outRect.right = space / 2;
}
}
}else{
if (position == 0) {
outRect.left = space;
outRect.right = space / 2;
} else {
if ((position % mNumCol) == 0) {
outRect.left = space;
outRect.right = space/2;
} else if((position % mNumCol) == (mNumCol-1)){
outRect.left = space/2;
outRect.right = space;
}else{
outRect.left=space/2;
outRect.right=space/2;
}
}
}
if(position<mNumCol){
outRect.top=space;
}else{
outRect.top=0;
}
// Add top margin only for the first item to avoid double space between items
/*
if (parent.getChildLayoutPosition(view) == 0 ) {
} else {
outRect.top = 0;
}*/
}
}
and use below code on your logic.
recyclerView.addItemDecoration(new SpacesItemDecoration(spacingInPixels, numCol));
For those who have problems with staggeredLayoutManager (like https://imgur.com/XVutH5u)
recyclerView's methods:
getChildAdapterPosition(view)
getChildLayoutPosition(view)
sometimes return -1 as index so we might face troubles setting itemDecor. My solution is to override deprecated ItemDecoration's method:
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent)
instead of the newbie:
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
like this:
recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
#Override
public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
TheAdapter.VH vh = (TheAdapter.VH) recyclerView.findViewHolderForAdapterPosition(itemPosition);
View itemView = vh.itemView; //itemView is the base view of viewHolder
//or instead of the 2 lines above maybe it's possible to use View itemView = layoutManager.findViewByPosition(itemPosition) ... NOT TESTED
StaggeredGridLayoutManager.LayoutParams itemLayoutParams = (StaggeredGridLayoutManager.LayoutParams) itemView.getLayoutParams();
int spanIndex = itemLayoutParams.getSpanIndex();
if (spanIndex == 0)
...
else
...
}
});
Seems to work for me so far :)
There is a very simple and yet flexible solution for this problem using only XML which works on every LayoutManager.
Assume you want an equal spacing of X (8dp for example).
Wrap your CardView item in another Layout
Give the outer Layout a padding of X/2 (4dp)
Make the outer Layout background transparent
...
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="200dp"
android:layout_height="200dp"
android:background="#android:color/transparent"
android:padding="4dip">
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v7.widget.CardView>
</LinearLayout>
Give your RecyclerView a padding of X/2 (4dp)
...
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="4dp" />
and thats it. You have perfect spacing of X (8dp).
The answers on for this question seem more complex than they should be. Here's my take on this.
Let's say you want 1dp spacing between grid items. Do the following:
Add a padding of 0.5dp to each item
Add a padding of -0.5dp to the RecycleView
That's it! :)
If you've scrolled far enough to reach this answer, I wrote a library for equal spacing which supports Vertical/Horizontal, LTR/RTL, LinearLayout/GridLayout manager and Edge inclusion. Its basically a single file, so you can copy paste that file into your code.
I tried to support StaggeredGridLayout but span index returned by this layout is not reliable. I would be glad to hear any suggestion for that.
This will work for RecyclerView with header as well.
public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {
private int spanCount;
private int spacing;
private boolean includeEdge;
private int headerNum;
public GridSpacingItemDecoration(int spanCount, int spacing, boolean includeEdge, int headerNum) {
this.spanCount = spanCount;
this.spacing = spacing;
this.includeEdge = includeEdge;
this.headerNum = headerNum;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view) - headerNum; // item position
if (position >= 0) {
int column = position % spanCount; // item column
if (includeEdge) {
outRect.left = spacing - column * spacing / spanCount; // spacing - column * ((1f / spanCount) * spacing)
outRect.right = (column + 1) * spacing / spanCount; // (column + 1) * ((1f / spanCount) * spacing)
if (position < spanCount) { // top edge
outRect.top = spacing;
}
outRect.bottom = spacing; // item bottom
} else {
outRect.left = column * spacing / spanCount; // column * ((1f / spanCount) * spacing)
outRect.right = spacing - (column + 1) * spacing / spanCount; // spacing - (column + 1) * ((1f / spanCount) * spacing)
if (position >= spanCount) {
outRect.top = spacing; // item top
}
}
} else {
outRect.left = 0;
outRect.right = 0;
outRect.top = 0;
outRect.bottom = 0;
}
}
}
}
For StaggeredGridLayoutManager users, be careful, lots of answers here including the most voted one calculates the item column with below code:
int column = position % spanCount
which assumes that the 1st/3rd/5th/.. items are always located at left side and 2nd/4th/6th/.. items are always located at right side. Is this assumption always true? No.
Let's say your 1st item is 100dp high and 2nd is only 50dp, guess where is your 3rd item located, left or right?
for anyone like me, who want the best answer but in kotlin, here it is:
class GridItemDecoration(
val spacing: Int,
private val spanCount: Int,
private val includeEdge: Boolean
) :
RecyclerView.ItemDecoration() {
/**
* Applies padding to all sides of the [Rect], which is the container for the view
*/
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val position = parent.getChildAdapterPosition(view) // item position
val column = position % spanCount // item column
if (includeEdge) {
outRect.left =
spacing - column * spacing / spanCount // spacing - column * ((1f / spanCount) * spacing)
outRect.right =
(column + 1) * spacing / spanCount // (column + 1) * ((1f / spanCount) * spacing)
if (position < spanCount) { // top edge
outRect.top = spacing
}
outRect.bottom = spacing // item bottom
} else {
outRect.left =
column * spacing / spanCount // column * ((1f / spanCount) * spacing)
outRect.right =
spacing - (column + 1) * spacing / spanCount // spacing - (column + 1) * ((1f / spanCount) * spacing)
if (position >= spanCount) {
outRect.top = spacing // item top
}
}
}
}
plus if you want to get the number from dimens.xml and then convert it to raw pixel you can do it easily using getDimensionPixelOffset easily like this:
recyclerView.addItemDecoration(
GridItemDecoration(
resources.getDimensionPixelOffset(R.dimen.h1),
3,
true
)
)
If you have a toggle switch that toggles between list to grid, don't forget to call recyclerView.removeItemDecoration(..) before setting any new Item decoration. If not then the new calculations for the spacing would be incorrect.
Something like this:
recyclerView.removeItemDecoration(gridItemDecorator)
recyclerView.removeItemDecoration(listItemDecorator)
if (showAsList) {
recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
recyclerView.addItemDecoration(listItemDecorator)
} else {
recyclerView.layoutManager = GridLayoutManager(this, spanCount)
recyclerView.addItemDecoration(gridItemDecorator)
}
Make sure you implement this in your gradle module:
implementation 'com.github.grzegorzojdana:SpacingItemDecoration:1.1.0'
Create this simple function:
public static int dpToPx(Context c, int dp) {
Resources r = c.getResources();
return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics()));
}
And lastly implement the spacing as shown in this line below:
photosRecycler.addItemDecoration(new SpacingItemDecoration(2, dpToPx(this, 4), true));
To made https://stackoverflow.com/a/29905000/1649371 (above) solution work I had to modify the following methods (and all subsequent calls)
#SuppressWarnings("all")
protected int getItemSpanSize(RecyclerView parent, View view, int childIndex) {
RecyclerView.LayoutManager mgr = parent.getLayoutManager();
if (mgr instanceof GridLayoutManager) {
return ((GridLayoutManager) mgr).getSpanSizeLookup().getSpanSize(childIndex);
} else if (mgr instanceof StaggeredGridLayoutManager) {
return ((StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams()).isFullSpan() ? spanCount : 1;
} else if (mgr instanceof LinearLayoutManager) {
return 1;
}
return -1;
}
#SuppressWarnings("all")
protected int getItemSpanIndex(RecyclerView parent, View view, int childIndex) {
RecyclerView.LayoutManager mgr = parent.getLayoutManager();
if (mgr instanceof GridLayoutManager) {
return ((GridLayoutManager) mgr).getSpanSizeLookup().getSpanIndex(childIndex, spanCount);
} else if (mgr instanceof StaggeredGridLayoutManager) {
return ((StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams()).getSpanIndex();
} else if (mgr instanceof LinearLayoutManager) {
return 0;
}
return -1;
}
Well for me the perfect solution was setting the width of RecyclerView who has layoutmanager as GridLayoutManager to "wrap_content"
If you are using Header with GridLayoutManager use this code written in Kotlin for spacing between the grids:
inner class SpacesItemDecoration(itemSpace: Int) : RecyclerView.ItemDecoration() {
var space: Int = itemSpace
override fun getItemOffsets(outRect: Rect?, view: View?, parent: RecyclerView?, state: RecyclerView.State?) {
super.getItemOffsets(outRect, view, parent, state)
val position = parent!!.getChildAdapterPosition(view)
val viewType = parent.adapter.getItemViewType(position)
// Check to not to set any margin to header item
if (viewType == GridViewAdapter.TYPE_HEADER) {
outRect!!.top = 0
outRect.left = 0
outRect.right = 0
outRect.bottom = 0
} else {
outRect!!.left = space
outRect.right = space
outRect.bottom = space
if (parent.getChildLayoutPosition(view) == 0) {
outRect.top = space
} else {
outRect.top = 0
}
}
}
}
And pass ItemDecoration to RecyclerView as:
gridView.addItemDecoration(SpacesItemDecoration(10))
I ended up doing it like that for my RecyclerView with GridLayoutManager and HeaderView.
In the code below I set a 4dp space between every item (2dp around every single item and 2dp padding around the whole RecyclerView).
In layout.xml:
<android.support.v7.widget.RecyclerView
android:id="#+id/recycleview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="2dp" />
In your fragment/activity:
GridLayoutManager manager = new GridLayoutManager(getContext(), 3);
recyclerView.setLayoutManager(manager);
int spacingInPixels = Utils.dpToPx(2);
recyclerView.addItemDecoration(new SpacesItemDecoration(spacingInPixels));
Create SpaceItemDecoration.java:
public class SpacesItemDecoration extends RecyclerView.ItemDecoration {
private int mSpacing;
public SpacesItemDecoration(int spacing) {
mSpacing = spacing;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView recyclerView, RecyclerView.State state) {
outRect.left = mSpacing;
outRect.top = mSpacing;
outRect.right = mSpacing;
outRect.bottom = mSpacing;
}
}
In Utils.java:
public static int dpToPx(final float dp) {
return Math.round(dp * (Resources.getSystem().getDisplayMetrics().xdpi / DisplayMetrics.DENSITY_DEFAULT));
}

Categories

Resources