Adding ItemDecoration to Recycler View Causes Headers to Shrink - android

I have a RecyclerView that is using a StaggeredGridLayoutManager to display items that are sectioned by date. For each section(day) I have a header view that is inserted into the recycler view and in my OnBindViewHolderAction method I set the headers to be FullSpan. My issue is, when I add my ItemDecoration to the RecyclerView to give all the items spacing, the header views get covered by the spacing (as if padding were added without the view size increasing) instead of margin spacing being added. I am not sure where to look next, any suggestions would be helpful. Thanks.
Without spacing item decoration:
With spacing item decoration:
What is should look like:
This last picture is the same RecyclerView and ItemDecoration but on a GridLayoutManager instead of a StaggeredGridLayoutManager.
public void OnBindViewHolderAction(RecyclerView.ViewHolder holder, int position)
{
var item = _collectionAdapter.SectionedList[position];
if (holder is ActivityFeedItemViewHolder) {
var viewHolder = (ActivityFeedItemViewHolder)holder;
viewHolder.Bind((ActivityFeedItemViewModel)item);
StaggeredGridLayoutManager.LayoutParams layoutParams = (StaggeredGridLayoutManager.LayoutParams)holder.ItemView.LayoutParameters;
layoutParams.FullSpan = false;
}
if (holder is RecyclerHeaderViewHolder) {
var viewHolder = (RecyclerHeaderViewHolder)holder;
viewHolder.Initialize(((RecyclerViewHeaderItem)item).SectionName);
StaggeredGridLayoutManager.LayoutParams layoutParams = (StaggeredGridLayoutManager.LayoutParams)holder.ItemView.LayoutParameters;
layoutParams.FullSpan = true;
}
}
public override void GetItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)
{
// STACKOVERFLOW NOTE: _headerTypeId is set to 0 when the method is called
int pos = parent.GetChildAdapterPosition(view);
// apply top spacing to first header only if there is one
if (pos == 0 && _headerTypeId != -1) {
outRect.Top = 2 * _spacing;
}
// these are fixed spacings for all cells
outRect.Bottom = 2 * _spacing;
outRect.Left = _spacing;
outRect.Right = _spacing;
// only apply spacing to the top row if we don't have headers
if (_headerTypeId == -1) {
// adjust the position index to account for headers
for (int i = 0; i < pos; i++) {
if (parent.GetAdapter().GetItemViewType(i) == _headerTypeId) {
pos--;
}
}
// apply top spacing to only the top row of cells
if (pos < _layoutManager.SpanCount) {
outRect.Top = 2 * _spacing;
}
}
}

So I was able to fix this by simply setting the height of the header view to wrap content instead of having it set to a value in the axml (32dp in my case). Thanks to Eugen Pechanec.

change
outRect.Top = 2 * _spacing;
to
outRect.Top =_spacing;
if that is not working just comment that line and try.

Related

Reset Recycler view decoration with diff utils

I have a grid recycler view that I update using diff utils, but if something in the list data causes an item to go from any row below the first row, up to the first row then the decoration lacks padding top, this is kind of expected as I'm only adding padding to the top of an item in the list if it is in the first row. I then add bottom padding to every item,
Recycler View Decoration
public class GridSpacing extends RecyclerView.ItemDecoration {
private final int spacing;
private final int numColumns;
public GridSpacing(int spacing, int numColumns) {
this.spacing = spacing;
this.numColumns = numColumns;
}
#Override
public void getItemOffsets(#NonNull Rect outRect, #NonNull View view, #NonNull RecyclerView parent, #NonNull RecyclerView.State state) {
int position = parent.getChildLayoutPosition(view);
if (position < numColumns){
outRect.top = spacing;
}
if (position % 2 != 0) {
outRect.right = spacing;
}
outRect.left = spacing;
outRect.bottom = spacing;
}
}
If I call notifyDatasetChanged, this will recalculate my decoration and add the padding to the top row, but diff utils is not calling notifyDatasetChanged (think it calls a combination of item range changed, item inserted, item removed, to keep animations) so my decoration doesn't get reapplied, I could probably reapply it myself but that seems like a hack to have to remove and reapply the decoration every time an item changes, does anyone know of a better approach?
so instead of trying to reapply the decorator I'm going to remove the padding top in the decorator and instead add padding to the top and clip to padding false on the recycler view
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="8dp"
android:clipToPadding="false"
android:id="#+id/recyclerView"/>

RecyclerView/SnapHelper - How to set variable position of the cards so that they peek differently based on position

I am new to android and hence RV and I am trying to achieve the layout where the first and last card are not centered and instead show more of the cards after and before them. Maybe at In this case I can see 16dp for the second cards and same thing for the penultimate card which makes the first and last card not centered.
But 8dp each for the rest of the cards so the intermediate cards appear centered. Maybe using itemDecoration somehow for the 2nd and the penultimate card somehow.
I was able to achieve showing parts of next and prev cards by following what is suggested here, but that only centers all the cards uniformly :
How to show part of next/previous card RecyclerView
I tried overriding getItemOffsets but it gets triggered everytime I scroll to the first or the last card and moves the 2nd and 2nd to last card incorrectly
and also doesn't center them correctly when I scroll to them.
public static class MyItemDecoration extends RecyclerView.ItemDecoration {
#Override
public void getItemOffsets(#NonNull Rect outRect, #NonNull View view, #NonNull RecyclerView parent, #NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
final int itemPosition = parent.getChildAdapterPosition(view);
if (itemPosition == RecyclerView.NO_POSITION) {
return;
}
final int itemCount = state.getItemCount();
if (itemCount > 0 && itemPosition == 1) {
outRect.left -= 16;
outRect.right -= 16;
}
else if (itemCount > 0 && itemPosition == itemCount - 1) {
outRect.left += 16;
outRect.right += 16;
}
}
}
RV Setup
SnapHelper snapHelper = new PagerSnapHelper();
RecyclerView rv = getBinding().rv;
rv.setOnFlingListener(null);
snapHelper.attachToRecyclerView(rv);
PagerSnapHelper centers the RecyclerView items including the decorations, so, unless the decoration widths are balanced, they won't always be centered. This may be what you are seeing.
Try the following for the decoration. This code applies the full-width decoration to the start of the first item and the end of the last item; otherwise, a half decoration width is used. By setting up the decorations this way, you are centering items that have balanced left and right decorations.
DividerItemDecoration decoration =
new DividerItemDecoration(getApplicationContext(), HORIZONTAL) {
private int mDecorationWidth = (int) (getResources().getDisplayMetrics().density * 8);
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
final int pos = parent.getChildAdapterPosition(view);
if (pos == RecyclerView.NO_POSITION) {
return;
}
if (pos == 0) {
outRect.set(mDecorationWidth, 0, mDecorationWidth / 2, 0);
} else if (pos == parent.getAdapter().getItemCount() - 1) {
outRect.set(mDecorationWidth / 2, 0, mDecorationWidth, 0);
} else {
outRect.set(mDecorationWidth / 2, 0, mDecorationWidth / 2, 0);
}
}
};
Here is a video showing the results with gray vertical dividers.
If you already have the decorations working to your satisfaction, you can override calculateDistanceToFinalSnap() in PagerSnapHelper to center all views except the first and last view as follows. See calculatedistancetofinalsnap(). Once the PageSnapHelper identifies a target view to snap to, calculatedistancetofinalsnap() is called to determine how many pixels to move to perform the snap. Here, we are moving just enough pixels to center the view (without decorations) in the RecyclerView. PageSnapHelper does the right thing for the first and last items, so we just call the super for these.
PagerSnapHelper pagerSnapHelper = new PagerSnapHelper() {
#Override
public int[] calculateDistanceToFinalSnap(#NonNull RecyclerView.LayoutManager layoutManager,
#NonNull View targetView) {
LinearLayoutManager lm = (LinearLayoutManager) layoutManager;
int pos = mRecycler.getChildAdapterPosition(targetView);
// If first or last view, the default implementation works.
if (pos == 0 || pos == lm.getItemCount() - 1) {
return super.calculateDistanceToFinalSnap(layoutManager, targetView);
}
// Force centering in the view without its decorations.
// targetCenter is the location of the center of the view we want to center.
int targetCenter = targetView.getLeft() + targetView.getWidth() / 2;
// Distance is the number of pixels to move the target so that its center
// lines up with the center of the RecyclerView (mRecycler.getWidth() / 2)
int distance = targetCenter - mRecycler.getWidth() / 2;
return new int[]{distance, 0};
}
};
Either way will work.

setting a custom margin between view of a recyclerview

I am trying to make recyclerview in which I can put a custom margin on view item based on an id in android
I tried custome adapter with a different view which we can put in one recycler view but I need only one view and only want to do is put custom margin between view based on id.
if (category_detail.getId()==0){
ViewGroup.MarginLayoutParams marginLayoutParams =
(ViewGroup.MarginLayoutParams) rec_discount.getLayoutParams();
marginLayoutParams.setMargins(0, 500, 0, 0);
rec_discount.setLayoutParams(marginLayoutParams);
}
You can use ItemDecoration to achieve this.
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.getChildAdapterPosition(view) == 0) {
outRect.top = space;
}
}
}
This will add a margin of "space" pixels on the left, right and bottom sides of all views in the recycler view. You can customize as per your requirements.
To use this class you can do this:
recyclerView.addItemDecoration(new SpacesItemDecoration(10));

Divide width between two columns in GridLayoutManager

I used GridLayoutManager as my layout manger for recycler view.
mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
the result is like below:
but I want to divide width between two columns equally. how can I do that?
Try adding this on your recyclerView:
mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view); // item position
int spanCount = 2;
int spacing = 10;//spacing between views in grid
if (position >= 0) {
int column = position % spanCount; // item column
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 = 0;
outRect.right = 0;
outRect.top = 0;
outRect.bottom = 0;
}
}
});
Setting the column count like you do:
mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
... actually makes the grid splitting into columns of the same width.
This suggests that the problem lies somewhere else, probably in the item layout. I suppose that some whitespaces (paddings/margins) are responsible for the fact that the items are shifted to the right. Try to remove these spacings and check if it works.
If it helps, you can always add new spacing with ItemDecoration but it isn't the reason for the incorrect size of your columns.
I think that turning on Show Layout Bounds option may be helpful for you.
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.
You can use custom ItemDecoration and you can split the space as you want :
SpacesItemDecoration can help you with this:
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.bottom = space;
// Add top margin only for the first item to avoid double space between items
if (parent.getChildLayoutPosition(view)%2 == 0) {
outRect.left = space;
outRect.right = space;
} else {
outRect.right = space;
}
}
}
In your acitivty or fragment :
int spacingInPixels = getResources().getDimensionPixelSize(R.dimen.spacing);
spacesItemDecoration = new SpacesItemDecoration(spacingInPixels);
recyclerview.addItemDecoration(spacesItemDecoration);
You can change the outRect.left & outRect.Right as per your need
You can use ItemDecoration for this purpose,have a look at here
And there are some very good answers here also

Margin/padding in last Child in RecyclerView

I'm trying to add Padding/Margin Bottom in the last row and Padding/Margin Top in the first row. I can not do it in the item xml as it would affect all of my Children.
I have headers and children in my RecyclerView Adapter so I can not use the
android:padding="4dp"
android:clipToPadding="false"
I need to use it individually on the last first row of each header
This issue is even easier to solve. You can apply necessary padding to the RecylerView itself and set clipToPadding to false, otherwise, the padding will chop off your scrolling area. Here is an example
<android.support.v7.widget.RecyclerView
android:padding="4dp"
android:clipToPadding="false"
android:layout_width="match_parent"
android:layout_height="match_parent" />
See the padding will add 4dp on all sides including top and bottom. Then the clipToPadding parameter makes sure your child items are not chopped off. Now, add 4dp padding to all sides for your child items, and you are good to go. In total you get 8dp padding on sides and between items.
Instead of adding padding to both the top and bottom items, You can just add the padding to the top and bottom of your RecyclerView and set the clipToPadding attribute to false.
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="8dp"
android:paddingBottom="8dp" />
<android.support.v7.widget.RecyclerView
android:id="#+id/rv_tpf"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingBottom="100dp" />
Add android:clipToPadding="false" and android:paddingBottom="100dp" in your recyclerview.
use ItemDecoration:
private 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) {
int position = parent.getChildAdapterPosition(view);
boolean isLast = position == state.getItemCount()-1;
if(isLast){
outRect.bottom = space;
outRect.top = 0; //don't forget about recycling...
}
if(position == 0){
outRect.top = space;
// don't recycle bottom if first item is also last
// should keep bottom padding set above
if(!isLast)
outRect.bottom = 0;
}
}
}
and
//8dp as px
int space = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8,
getResources().getDisplayMetrics()); // calculated
//int space = getResources().getDimensionPixelSize(
// R.dimen.list_item_padding_vertical); // from resources
recyclerView.addItemDecoration(new SpacesItemDecoration(space));
Add android:clipToPadding="false" and android:paddingBottom="65dp" in your recyclerview. If you are using fab menu button and actions on recycler view cell.
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/dinner_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingBottom="65dp"/>
I use this in kotlin to give bottom margin to last item only
override fun onBindViewHolder(holder: RecyclerView.ViewHolder(view), position: Int) {
if (position == itemsList.lastIndex){
val params = holder.itemView.layoutParams as FrameLayout.LayoutParams
params.bottomMargin = 100
holder.itemView.layoutParams = params
}else{
val params = holder.itemView.layoutParams as RecyclerView.LayoutParams
params.bottomMargin = 0
holder.itemView.layoutParams = params
}
//other codes ...
}
For some reason the old clipToPadding=false solution isn't working for me. So I added an ItemDecoration
https://gist.github.com/kassim/582888fa5960791264fc92bc41fb6bcf
public class BottomPaddingDecoration extends RecyclerView.ItemDecoration {
private final int bottomPadding;
public BottomPaddingDecoration(int bottomPadding) {
this.bottomPadding = bottomPadding;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int position = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition();
if (position == parent.getAdapter().getItemCount() - 1) {
outRect.set(0, 0, 0, bottomPadding);
}
}
}
Java equivalent to #Radesh answer:
#Override
public void onBindViewHolder(#NonNull RecyclerView.ViewHolder holder, int position) {
if (position == itemsList.size() - 1) {
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) holder.itemView.getLayoutParams();
params.bottomMargin = 100;
holder.itemView.setLayoutParams(params);
} else {
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) holder.itemView.getLayoutParams();
params.bottomMargin = 0;
holder.itemView.setLayoutParams(params);
}
}
I have modified amazing answer #snachmsm answer for better and give you idea how to use
properly
public class SpacesItemDecoration extends DividerItemDecoration {
private int space;
public SpacesItemDecoration(Context clContext,int oriantation,int space) {
super(clContext,oriantation);
this.space = space;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect,view,parent,state);
int position = parent.getChildAdapterPosition(view);
boolean isLast = position == state.getItemCount()-1;
if(isLast){
outRect.bottom = space;
outRect.top = 0; //don't forget about recycling...
}
/* if(position == 0){
outRect.top = space;
// don't recycle bottom if first item is also last
// should keep bottom padding set above
if(!isLast)
outRect.bottom = 0;
}*/
}
}
Long story short :
int freeSpaceAtBottom = 100; // the bottom free space in pixels
myRecyclerView.setClipToPadding(false);
myRecyclerView.setPadding(0,0,0,freeSpaceAtBottom);
setClipToPadding Sets whether this list view will clip its children to its padding and resize (but not clip) any EdgeEffect to the padded region, if padding is present. (1)

Categories

Resources