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)
Related
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"/>
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));
I am attempting to create an ItemDecoration that will maintain a minimum height for a RecyclerView by dynamically adding padding to the last item.
To calculate the amount of padding I need to know the total height of all children. I am approximating this by getting the height of a single view and multiplying it by the number of items in the Adapter.
The problem I am experiencing is that view.getHeight() doesn't always return the correct height. Usually the first time getItemOffsets() is called the height is much smaller than it should be (e.g. 30px vs 300px). This happens even if I give the child views a fixed height in the layout XML. I am guessing this has something to do with the measure/layout cycle by I am unsure of how to get the correct view dimensions in the ItemDecoration.
What is the correct way to get the child views height programmatically in getItemOffsets()?
ItemDecoration:
public class MinHeightPaddingItemDecoration extends RecyclerView.ItemDecoration {
private static final String LOG_TAG = MinHeightPaddingItemDecoration.class.getSimpleName();
private int mMinHeight;
public MinHeightPaddingItemDecoration(int minHeight) {
super();
mMinHeight = minHeight;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView recyclerView, RecyclerView.State state) {
int itemCount = state.getItemCount();
int lastPosition = itemCount - 1;
int itemPosition = recyclerView.getChildAdapterPosition(view);
int layoutPosition = recyclerView.getChildLayoutPosition(view);
// If this view isnt on screen then do nothing
if (layoutPosition != RecyclerView.NO_POSITION && itemPosition == lastPosition) {
// NOTE: view.getHeight() doesn't always return the correct height, even if the layout is given a fixed height in the XML
int childHeight = view.getHeight();
int totalChildHeight = childHeight * itemCount;
int minHeight = getMinHeight();
if (totalChildHeight < minHeight) {
outRect.bottom = minHeight - totalChildHeight;
}
}
}
private int getMinHeight() {
return mMinHeight;
}
}
recycler_view_item.xml
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable name="controller" type="my.ViewController"/>
</data>
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_gravity="center"
android:clickable="true"
android:onClick="#{() -> controller.doSomething()}">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Child layout"
/>
</RelativeLayout>
</android.support.v7.widget.CardView>
</layout>
I found this workaround for fixed sized child views. I am still not sure how to handle dynamically sized layouts.
// NOTE: view.getHeight() doesn't always return the correct height
// NOTE: even if the layout is given a fixed height in the XML.
// NOTE: Instead directly access the LayoutParams height value
int childHeight = view.getLayoutParams().height;
I found a kind-of hack, after reading https://yoda.entelect.co.za/view/9627/how-to-android-recyclerview-item-decorations.
Before calculating padding based on child.width, I manually measure the View, if needed:
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
if(view.width == 0) fixLayoutSize(view, parent)
calculatePadding(outRect, view, parent, state)
}
private fun fixLayoutSize(view: View, parent: ViewGroup) {
if (view.layoutParams == null) {
view.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
)
}
val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)
val childWidth = ViewGroup.getChildMeasureSpec(
widthSpec, parent.paddingLeft + parent.paddingRight, view.layoutParams.width
)
val childHeight = ViewGroup.getChildMeasureSpec(
heightSpec, parent.paddingTop + parent.paddingBottom, view.layoutParams.height
)
view.measure(childWidth, childHeight)
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
}
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.
Is that possible to add footer with item decoration, and not using adapter? Since I'm working with a very complex adapter with already lot of different viewholder types, I'd like to add an identical footer seamlessly to every list in my app.
As far as i can tell, this inst't best practice.
Here's description from RecyclerView.ItemDecoration class:
/**
* An ItemDecoration allows the application to add a special drawing and layout offset
* to specific item views from the adapter's data set. This can be useful for drawing dividers
* between items, highlights, visual grouping boundaries and more.
However You can set specific behaviour when implementing your own divider based on adapter viewtype divider has to deal with. Here's an example code i used in one online course:
public class Divider extends RecyclerView.ItemDecoration {
private Drawable mDivider;
private int mOrientation;
public Divider(Context context, int orientation) {
mDivider = ContextCompat.getDrawable(context, R.drawable.divider);
if (orientation != LinearLayoutManager.VERTICAL) {
throw new IllegalArgumentException("This Item Decoration can be used only with a RecyclerView that uses a LinearLayoutManager with vertical orientation");
}
mOrientation = orientation;
}
#Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (mOrientation == LinearLayoutManager.VERTICAL) {
drawHorizontalDivider(c, parent, state);
}
}
private void drawHorizontalDivider(Canvas c, RecyclerView parent, RecyclerView.State state) {
int left, top, right, bottom;
left = parent.getPaddingLeft();
right = parent.getWidth() - parent.getPaddingRight();
int count = parent.getChildCount();
for (int i = 0; i < count; i++) {
//here we check the itemViewType we deal with, you can implement your own behaviour for Footer type.
// In this example i draw a drawable below every item that IS NOT Footer, as i defined Footer as a button in view
if (Adapter.FOOTER != parent.getAdapter().getItemViewType(i)) {
View current = parent.getChildAt(i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) current.getLayoutParams();
top = current.getTop() - params.topMargin;
bottom = top + mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
if (mOrientation == LinearLayoutManager.VERTICAL) {
outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
}
}
Or you can use a library called Flexible Divider that allows for using a custom drawable or resource to be set.