Get child view height in RecyclerView ItemDecoration.getItemOffsets - android

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)
}

Related

How to overlap RecyclerView over ScrollView [duplicate]

I'm trying to implement a layout which contains RecyclerView and ScrollView at the same layout.
Layout template:
<RelativeLayout>
<ScrollView android:id="#+id/myScrollView">
<unrelated data>...</unrealated data>
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/my_recycler_view" />
</ScrollView>
</RelativeLayout>
Problems: I can scroll until the last element of ScrollView.
Things I tried:
Card view inside the ScrollView (now ScrollView contains RecyclerView) - can see the card up until the RecyclerView.
Initial thought was to implement this ViewGroup using RecyclerView instead of ScrollView where one of it's views type is the CardView, but I got the exact same results as with the ScrollView.
use NestedScrollView instead of ScrollView
Please go through NestedScrollView reference document for more information.
and add recyclerView.setNestedScrollingEnabled(false); to your RecyclerView
I know I am late it the game, but the issue still exists even after google has made fix on the android.support.v7.widget.RecyclerView
The issue I get now is RecyclerView with layout_height=wrap_content not taking height of all the items issue inside ScrollView that only happens on Marshmallow and Nougat+ (API 23, 24, 25) versions.
(UPDATE: Replacing ScrollView with android.support.v4.widget.NestedScrollView works on all versions. I somehow missed testing accepted solution. Added this in my github project as demo.)
After trying different things, I have found workaround that fixes this issue.
Here is my layout structure in a nutshell:
<ScrollView>
<LinearLayout> (vertical - this is the only child of scrollview)
<SomeViews>
<RecyclerView> (layout_height=wrap_content)
<SomeOtherViews>
The workaround is the wrap the RecyclerView with RelativeLayout. Don't ask me how I found this workaround!!! ¯\_(ツ)_/¯
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="blocksDescendants">
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLayout>
Complete example is available on GitHub project - https://github.com/amardeshbd/android-recycler-view-wrap-content
Here is a demo screencast showing the fix in action:
Although the recommendation that
you should never put a scrollable view inside another scrollable view
Is a sound advice, however if you set a fixed height on the recycler view it should work fine.
If you know the height of the adapter item layout you could just calculate the height of the RecyclerView.
int viewHeight = adapterItemSize * adapterData.size();
recyclerView.getLayoutParams().height = viewHeight;
In case setting fixed height for the RecyclerView didn't work for someone (like me), here is what I've added to the fixed height solution:
mRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
#Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
int action = e.getAction();
switch (action) {
case MotionEvent.ACTION_MOVE:
rv.getParent().requestDisallowInterceptTouchEvent(true);
break;
}
return false;
}
#Override
public void onTouchEvent(RecyclerView rv, MotionEvent e) {
}
#Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
});
The new Android Support Library 23.2 solves that problem, you can now set wrap_content as the height of your RecyclerView and works correctly.
Android Support Library 23.2
RecyclerViews are fine to put in ScrollViews so long as they aren't scrolling themselves. In this case, it makes sense to make it a fixed height.
The proper solution is to use wrap_content on the RecyclerView height and then implement a custom LinearLayoutManager that can properly handle the wrapping.
Copy this LinearLayoutManager into your project: link
Then wrap the RecyclerView:
<android.support.v7.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
And set it up like so:
RecyclerView list = (RecyclerView)findViewById(R.id.list);
list.setHasFixedSize(true);
list.setLayoutManager(new com.example.myapp.LinearLayoutManager(list.getContext()));
list.setAdapter(new MyViewAdapter(data));
Edit: This can cause complications with scrolling because the RecyclerView can steal the ScrollView's touch events. My solution was just to ditch the RecyclerView in all and go with a LinearLayout, programmatically inflate subviews, and add them to the layout.
For ScrollView, you could use fillViewport=true and make layout_height="match_parent" as below and put RecyclerView inside:
<ScrollView
android:fillViewport="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="#+id/llOptions">
<android.support.v7.widget.RecyclerView
android:id="#+id/rvList"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>
No further height adjustment needed through code.
Try this. Very late answer, but surely helps anyone in future.
Change your ScrollView to NestedScrollView:
<android.support.v4.widget.NestedScrollView>
<android.support.v7.widget.RecyclerView
...
... />
</android.support.v4.widget.NestedScrollView>
In your UI code, update it for Recyclerview:
recyclerView.setNestedScrollingEnabled(false);
recyclerView.setHasFixedSize(false);
Calculating RecyclerView's height manually is not good, better is to use a custom LayoutManager.
The reason for above issue is any view which has it's scroll (ListView, GridView, RecyclerView) failed to calculate it's height when add as a child in another view has scroll. So overriding its onMeasure method will solve the issue.
Please replace the default layout manager with the below:
public class MyLinearLayoutManager extends android.support.v7.widget.LinearLayoutManager {
private static boolean canMakeInsetsDirty = true;
private static Field insetsDirtyField = null;
private static final int CHILD_WIDTH = 0;
private static final int CHILD_HEIGHT = 1;
private static final int DEFAULT_CHILD_SIZE = 100;
private final int[] childDimensions = new int[2];
private final RecyclerView view;
private int childSize = DEFAULT_CHILD_SIZE;
private boolean hasChildSize;
private int overScrollMode = ViewCompat.OVER_SCROLL_ALWAYS;
private final Rect tmpRect = new Rect();
#SuppressWarnings("UnusedDeclaration")
public MyLinearLayoutManager(Context context) {
super(context);
this.view = null;
}
#SuppressWarnings("UnusedDeclaration")
public MyLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
this.view = null;
}
#SuppressWarnings("UnusedDeclaration")
public MyLinearLayoutManager(RecyclerView view) {
super(view.getContext());
this.view = view;
this.overScrollMode = ViewCompat.getOverScrollMode(view);
}
#SuppressWarnings("UnusedDeclaration")
public MyLinearLayoutManager(RecyclerView view, int orientation, boolean reverseLayout) {
super(view.getContext(), orientation, reverseLayout);
this.view = view;
this.overScrollMode = ViewCompat.getOverScrollMode(view);
}
public void setOverScrollMode(int overScrollMode) {
if (overScrollMode < ViewCompat.OVER_SCROLL_ALWAYS || overScrollMode > ViewCompat.OVER_SCROLL_NEVER) {
throw new IllegalArgumentException("Unknown overscroll mode: " + overScrollMode);
}
if (this.view == null) throw new IllegalStateException("view == null");
this.overScrollMode = overScrollMode;
ViewCompat.setOverScrollMode(view, overScrollMode);
}
public static int makeUnspecifiedSpec() {
return View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
}
#Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec) {
final int widthMode = View.MeasureSpec.getMode(widthSpec);
final int heightMode = View.MeasureSpec.getMode(heightSpec);
final int widthSize = View.MeasureSpec.getSize(widthSpec);
final int heightSize = View.MeasureSpec.getSize(heightSpec);
final boolean hasWidthSize = widthMode != View.MeasureSpec.UNSPECIFIED;
final boolean hasHeightSize = heightMode != View.MeasureSpec.UNSPECIFIED;
final boolean exactWidth = widthMode == View.MeasureSpec.EXACTLY;
final boolean exactHeight = heightMode == View.MeasureSpec.EXACTLY;
final int unspecified = makeUnspecifiedSpec();
/**
* In case of exact calculations for both dimensions let's
* use default "onMeasure" implementation.
*/
if (exactWidth && exactHeight) {
super.onMeasure(recycler, state, widthSpec, heightSpec);
return;
}
final boolean vertical = getOrientation() == VERTICAL;
initChildDimensions(widthSize, heightSize, vertical);
int width = 0;
int height = 0;
/**
* It's possible to get scrap views in recycler which are bound to old (invalid)
* adapter entities. This happens because their invalidation happens after "onMeasure"
* method. As a workaround let's clear the recycler now (it should not cause
* any performance issues while scrolling as "onMeasure" is never called whiles scrolling).
*/
recycler.clear();
final int stateItemCount = state.getItemCount();
final int adapterItemCount = getItemCount();
/**
* Adapter always contains actual data while state might contain old data
* (f.e. data before the animation is done). As we want to measure the view
* with actual data we must use data from the adapter and not from the state.
*/
for (int i = 0; i < adapterItemCount; i++) {
if (vertical) {
if (!hasChildSize) {
if (i < stateItemCount) {
/**
* We should not exceed state count, otherwise we'll get IndexOutOfBoundsException.
* For such items we will use previously calculated dimensions.
*/
measureChild(recycler, i, widthSize, unspecified, childDimensions);
} else {
logMeasureWarning(i);
}
}
height += childDimensions[CHILD_HEIGHT];
if (i == 0) {
width = childDimensions[CHILD_WIDTH];
}
if (hasHeightSize && height >= heightSize) {
break;
}
} else {
if (!hasChildSize) {
if (i < stateItemCount) {
/**
* We should not exceed state count, otherwise we'll get IndexOutOfBoundsException.
* For such items we will use previously calculated dimensions.
*/
measureChild(recycler, i, unspecified, heightSize, childDimensions);
} else {
logMeasureWarning(i);
}
}
width += childDimensions[CHILD_WIDTH];
if (i == 0) {
height = childDimensions[CHILD_HEIGHT];
}
if (hasWidthSize && width >= widthSize) {
break;
}
}
}
if (exactWidth) {
width = widthSize;
} else {
width += getPaddingLeft() + getPaddingRight();
if (hasWidthSize) {
width = Math.min(width, widthSize);
}
}
if (exactHeight) {
height = heightSize;
} else {
height += getPaddingTop() + getPaddingBottom();
if (hasHeightSize) {
height = Math.min(height, heightSize);
}
}
setMeasuredDimension(width, height);
if (view != null && overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS) {
final boolean fit = (vertical && (!hasHeightSize || height < heightSize))
|| (!vertical && (!hasWidthSize || width < widthSize));
ViewCompat.setOverScrollMode(view, fit ? ViewCompat.OVER_SCROLL_NEVER : ViewCompat.OVER_SCROLL_ALWAYS);
}
}
private void logMeasureWarning(int child) {
if (BuildConfig.DEBUG) {
Log.w("MyLinearLayoutManager", "Can't measure child #" + child + ", previously used dimensions will be reused." +
"To remove this message either use #setChildSize() method or don't run RecyclerView animations");
}
}
private void initChildDimensions(int width, int height, boolean vertical) {
if (childDimensions[CHILD_WIDTH] != 0 || childDimensions[CHILD_HEIGHT] != 0) {
/** Already initialized, skipping. */
return;
}
if (vertical) {
childDimensions[CHILD_WIDTH] = width;
childDimensions[CHILD_HEIGHT] = childSize;
} else {
childDimensions[CHILD_WIDTH] = childSize;
childDimensions[CHILD_HEIGHT] = height;
}
}
#Override
public void setOrientation(int orientation) {
/** Might be called before the constructor of this class is called. */
//noinspection ConstantConditions
if (childDimensions != null) {
if (getOrientation() != orientation) {
childDimensions[CHILD_WIDTH] = 0;
childDimensions[CHILD_HEIGHT] = 0;
}
}
super.setOrientation(orientation);
}
public void clearChildSize() {
hasChildSize = false;
setChildSize(DEFAULT_CHILD_SIZE);
}
public void setChildSize(int childSize) {
hasChildSize = true;
if (this.childSize != childSize) {
this.childSize = childSize;
requestLayout();
}
}
private void measureChild(RecyclerView.Recycler recycler, int position, int widthSize, int heightSize, int[] dimensions) {
final View child;
try {
child = recycler.getViewForPosition(position);
} catch (IndexOutOfBoundsException e) {
if (BuildConfig.DEBUG) {
Log.w("MyLinearLayoutManager", "MyLinearLayoutManager doesn't work well with animations. Consider switching them off", e);
}
return;
}
final RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) child.getLayoutParams();
final int hPadding = getPaddingLeft() + getPaddingRight();
final int vPadding = getPaddingTop() + getPaddingBottom();
final int hMargin = p.leftMargin + p.rightMargin;
final int vMargin = p.topMargin + p.bottomMargin;
/** We must make insets dirty in order calculateItemDecorationsForChild to work. */
makeInsetsDirty(p);
/** This method should be called before any getXxxDecorationXxx() methods. */
calculateItemDecorationsForChild(child, tmpRect);
final int hDecoration = getRightDecorationWidth(child) + getLeftDecorationWidth(child);
final int vDecoration = getTopDecorationHeight(child) + getBottomDecorationHeight(child);
final int childWidthSpec = getChildMeasureSpec(widthSize, hPadding + hMargin + hDecoration, p.width, canScrollHorizontally());
final int childHeightSpec = getChildMeasureSpec(heightSize, vPadding + vMargin + vDecoration, p.height, canScrollVertically());
child.measure(childWidthSpec, childHeightSpec);
dimensions[CHILD_WIDTH] = getDecoratedMeasuredWidth(child) + p.leftMargin + p.rightMargin;
dimensions[CHILD_HEIGHT] = getDecoratedMeasuredHeight(child) + p.bottomMargin + p.topMargin;
/** As view is recycled let's not keep old measured values. */
makeInsetsDirty(p);
recycler.recycleView(child);
}
private static void makeInsetsDirty(RecyclerView.LayoutParams p) {
if (!canMakeInsetsDirty) return;
try {
if (insetsDirtyField == null) {
insetsDirtyField = RecyclerView.LayoutParams.class.getDeclaredField("mInsetsDirty");
insetsDirtyField.setAccessible(true);
}
insetsDirtyField.set(p, true);
} catch (NoSuchFieldException e) {
onMakeInsertDirtyFailed();
} catch (IllegalAccessException e) {
onMakeInsertDirtyFailed();
}
}
private static void onMakeInsertDirtyFailed() {
canMakeInsetsDirty = false;
if (BuildConfig.DEBUG) {
Log.w("MyLinearLayoutManager", "Can't make LayoutParams insets dirty, decorations measurements might be incorrect");
}
}
}
If you put RecyclerView inside NestedScrollView and enable recyclerView.setNestedScrollingEnabled(false);, scrolling will working well.
However, there is a problem
RecyclerView don't recycle
For example, your RecyclerView (inside NestedScrollView or ScrollView) have 100 item.
When Activity launch, 100 item will create (onCreateViewHolder and onBindViewHolder of 100 item will called at same time).
Example, for each item, you will load a large image from API => activity created -> 100 image will load.
It make starting Activity slowness and lagging.
Possible solution:
- Thinking about using RecyclerView with multiple type.
However, if in your case, there are just a few item in RecyclerView and recycle or don't recycle don't affect performance a lot, you can use RecyclerView inside ScrollView for simple
UPDATE:
this answer is out dated now as there are widgets like NestedScrollView and RecyclerView that support nested scrolling.
you should never put a scrollable view inside another scrollable view !
i suggest you make your main layout recycler view and put your views as items of recycler view.
take a look at this example it show how to use multiple views inside recycler view adapter.
link to example
Add this line to your RecyclerView xml view:
android:nestedScrollingEnabled="false"
And your RecyclerView will be smoothly scrolled with flexible height.
Hope it helps.
It seems that NestedScrollView does solve the problem.
I've tested using this layout:
<android.support.v4.widget.NestedScrollView
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"
android:id="#+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/dummy_text" />
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp">
<android.support.v7.widget.RecyclerView
android:id="#+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</android.support.v7.widget.CardView>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
And it works without issues.
I was having the same problem. That's what i tried and it works. I am sharing my xml and java code. Hope this will help someone.
Here is the xml
<?xml version="1.0" encoding="utf-8"?>
< NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="#+id/iv_thumbnail"
android:layout_width="match_parent"
android:layout_height="200dp" />
<TextView
android:id="#+id/tv_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Description" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Buy" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Reviews" />
<android.support.v7.widget.RecyclerView
android:id="#+id/rc_reviews"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</android.support.v7.widget.RecyclerView>
</LinearLayout>
</NestedScrollView >
Here is the related java code. It works like a charm.
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.setNestedScrollingEnabled(false);
I used CustomLayoutManager to disable RecyclerView Scrolling.
Also don't use Recycler View as WrapContent, use it as 0dp, Weight=1
public class CustomLayoutManager extends LinearLayoutManager {
private boolean isScrollEnabled;
// orientation should be LinearLayoutManager.VERTICAL or HORIZONTAL
public CustomLayoutManager(Context context, int orientation, boolean isScrollEnabled) {
super(context, orientation, false);
this.isScrollEnabled = isScrollEnabled;
}
#Override
public boolean canScrollVertically() {
//Similarly you can customize "canScrollHorizontally()" for managing horizontal scroll
return isScrollEnabled && super.canScrollVertically();
}
}
Use CustomLayoutManager in RecyclerView:
CustomLayoutManager mLayoutManager = new CustomLayoutManager(getBaseActivity(), CustomLayoutManager.VERTICAL, false);
recyclerView.setLayoutManager(mLayoutManager);
((DefaultItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
recyclerView.setAdapter(statsAdapter);
UI XML:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#drawable/background_main"
android:fillViewport="false">
<LinearLayout
android:id="#+id/contParentLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<edu.aku.family_hifazat.libraries.mpchart.charts.PieChart
android:id="#+id/chart1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="#dimen/x20dp"
android:minHeight="#dimen/x300dp">
</edu.aku.family_hifazat.libraries.mpchart.charts.PieChart>
</FrameLayout>
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
</android.support.v7.widget.RecyclerView>
</LinearLayout>
</ScrollView>
Actually the main purpose of the RecyclerView is to compensate for ListView and ScrollView. Instead of doing what you're actually doing: Having a RecyclerView in a ScrollView, I would suggest having only a RecyclerView that can handle many types of children.
This does the trick:
recyclerView.setNestedScrollingEnabled(false);
First you should use NestedScrollView instead of ScrollView and put the RecyclerView inside NestedScrollView.
Use Custom layout class to measure the height and width of screen:
public class CustomLinearLayoutManager extends LinearLayoutManager {
public CustomLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
private int[] mMeasuredDimension = new int[2];
#Override
public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state,
int widthSpec, int heightSpec) {
final int widthMode = View.MeasureSpec.getMode(widthSpec);
final int heightMode = View.MeasureSpec.getMode(heightSpec);
final int widthSize = View.MeasureSpec.getSize(widthSpec);
final int heightSize = View.MeasureSpec.getSize(heightSpec);
int width = 0;
int height = 0;
for (int i = 0; i < getItemCount(); i++) {
if (getOrientation() == HORIZONTAL) {
measureScrapChild(recycler, i,
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
heightSpec,
mMeasuredDimension);
width = width + mMeasuredDimension[0];
if (i == 0) {
height = mMeasuredDimension[1];
}
} else {
measureScrapChild(recycler, i,
widthSpec,
View.MeasureSpec.makeMeasureSpec(i, View.MeasureSpec.UNSPECIFIED),
mMeasuredDimension);
height = height + mMeasuredDimension[1];
if (i == 0) {
width = mMeasuredDimension[0];
}
}
}
switch (widthMode) {
case View.MeasureSpec.EXACTLY:
width = widthSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
}
switch (heightMode) {
case View.MeasureSpec.EXACTLY:
height = heightSize;
case View.MeasureSpec.AT_MOST:
case View.MeasureSpec.UNSPECIFIED:
}
setMeasuredDimension(width, height);
}
private void measureScrapChild(RecyclerView.Recycler recycler, int position, int widthSpec,
int heightSpec, int[] measuredDimension) {
View view = recycler.getViewForPosition(position);
recycler.bindViewToPosition(view, position);
if (view != null) {
RecyclerView.LayoutParams p = (RecyclerView.LayoutParams) view.getLayoutParams();
int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
getPaddingLeft() + getPaddingRight(), p.width);
int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
getPaddingTop() + getPaddingBottom(), p.height);
view.measure(childWidthSpec, childHeightSpec);
measuredDimension[0] = view.getMeasuredWidth() + p.leftMargin + p.rightMargin;
measuredDimension[1] = view.getMeasuredHeight() + p.bottomMargin + p.topMargin;
recycler.recycleView(view);
}
}
}
And implement below code in the activity/fragment of RecyclerView:
final CustomLinearLayoutManager layoutManager = new CustomLinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(mAdapter);
recyclerView.setNestedScrollingEnabled(false); // Disables scrolling for RecyclerView, CustomLinearLayoutManager used instead of MyLinearLayoutManager
recyclerView.setHasFixedSize(false);
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int visibleItemCount = layoutManager.getChildCount();
int totalItemCount = layoutManager.getItemCount();
int lastVisibleItemPos = layoutManager.findLastVisibleItemPosition();
Log.i("getChildCount", String.valueOf(visibleItemCount));
Log.i("getItemCount", String.valueOf(totalItemCount));
Log.i("lastVisibleItemPos", String.valueOf(lastVisibleItemPos));
if ((visibleItemCount + lastVisibleItemPos) >= totalItemCount) {
Log.i("LOG", "Last Item Reached!");
}
}
});
If RecyclerView showing only one row inside ScrollView. You just need to set height of your row to android:layout_height="wrap_content".
Sorry being late to the party, but it seems like there is another solution which works perfectly for the case you mentioned.
If you use a recycler view inside a recycler view, it seems to work perfectly fine. I have personally tried and used it, and it seems to give no slowness and no jerkyness at all. Now I am not sure if this is a good practice or not, but nesting multiple recycler views , even nested scroll view slows down. But this seems to work nicely. Please give it a try. I am sure nesting is going to be perfectly fine with this.
Another approach to address the issue is to use ConstraintLayout inside ScrollView:
<ScrollView>
<ConstraintLayout> (this is the only child of ScrollView)
<...Some Views...>
<RecyclerView> (layout_height=wrap_content)
<...Some Other Views...>
But I would still stick to the androidx.core.widget.NestedScrollView approach, proposed by Yang Peiyong.
You can try with setting recycler view Hight as wrap_content.
in my case its working fine. I am trying with 2 different recycler view in scroll view
The best solution is to keep multiple Views in a Single View / View Group and then keep that one view in the SrcollView. ie.
Format -
<ScrollView>
<Another View>
<RecyclerView>
<TextView>
<And Other Views>
</Another View>
</ScrollView>
Eg.
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:text="any text"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:text="any text"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</ScrollView>
Another Eg. of ScrollView with multiple Views
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="vertical"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/imageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FFFFFF"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="10dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#string/CategoryItem"
android:textSize="20sp"
android:textColor="#000000"
/>
<TextView
android:textColor="#000000"
android:text="₹1000"
android:textSize="18sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:textColor="#000000"
android:text="so\nugh\nos\nghs\nrgh\n
sghs\noug\nhro\nghreo\nhgor\ngheroh\ngr\neoh\n
og\nhrf\ndhog\n
so\nugh\nos\nghs\nrgh\nsghs\noug\nhro\n
ghreo\nhgor\ngheroh\ngr\neoh\nog\nhrf\ndhog"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
For those people who trying to do it just for design purposes - leave it. Redesign your app and leave only RecyclerView. It will be better solution than doing ANY hardcode.
You can also override LinearLayoutManager to make RecyclerView roll smoothly:
#Override
public boolean canScrollVertically(){
return false;
}
Solution which worked for me
Use NestedScrollView with height as wrap_content, and for your RecyclerView setup this:
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
app:layoutManager="android.support.v7.widget.LinearLayoutManager" />
And set view holder layout params:
android:layout_width="match_parent"
android:layout_height="wrap_content"

RecyclerView in ConstraintLayout. Show only complety visible items on a screen

I need to create a horizontal list of items that only displays fully visible items.
But as you can see, my recycler view show a particular element. I use a horizontal LinearLayoutManager.
I add 10 elements, but recycler view has room only for 3. I need to show only 3, but it always show me 3 and particular element.
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recycler"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintEnd_toStartOf="#+id/textView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
My item layout:
<LinearLayout
android:id="#+id/itemLayout"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="10dp">
<TextView
android:id="#+id/tvAnimalName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ASDAS"
android:background="#color/colorAccent"
android:padding="10dp"
android:textSize="17sp"/>
</LinearLayout>
Adapter and activity are plain.
How can I show only visible 3 items?
Edit.
I must to disable scroll. So i am using:
layoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) {
#Override
public boolean canScrollHorizontally() {
return false;
}
};
Edit 2. These methods show -1 always:
int findLastVisibleItemPosition();
int findLastCompletelyVisibleItemPosition();
class HideLastDecorator() : RecyclerView.ItemDecoration() {
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
val count = parent.childCount
for (i in 0 until count) {
parent.getChildAt(i).visibility = if (count == i - 1) View.INVISIBLE else View.VISIBLE
}
}
}
and add it to your recyclerView Decorations
appsRecyclerView.addItemDecoration(HideLastDecorator())
Sorry for Kotlin :)
View.INVISIBLE is important, because if the View becomes GONE, it will be removed from the measuring of the RecyclerView's content and the new ViewHolder would be added.
I prefer to work careful with OnClickListener if any is set for the ViewHolder's content.
Below code will work for you. A little explanation: Extend RecyclerView and override onLayout() method. Once RecyclerView is ready iterate through all visible (on-screen) children of RecyclerView and apply your logic. In our case we'll draw BounddingBox for every nth child and RecyclerView. If child's bounds lie inside RecyclerView's bounds then show that child otherwise set visibility to GONE/INVISIBLE.
public class CustomRecyclerView extends RecyclerView {
Rect recyclerViewBounds = new Rect();
Rect currentChildViewBounds = new Rect();
public CustomRecyclerView(#NonNull Context context) {
super(context);
}
public CustomRecyclerView(#NonNull Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomRecyclerView(#NonNull Context context, #Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
recyclerViewBounds.set(l, t, r, b);
for (int i = 0; i < getChildCount(); i++) {
View currentChild = getChildAt(i);
currentChildViewBounds.set(currentChild.getLeft(), currentChild.getTop(), currentChild.getRight(), currentChild.getBottom());
currentChild.setVisibility(recyclerViewBounds.contains(currentChildViewBounds) ? VISIBLE : GONE); // or INVISBLE instead of GONE
}
}
}
And most importantly: In your xml file use com.your.packagename.CustomRecyclerView instead of androidx.recyclerview.widget.RecyclerView.
NOTE: Please refrain from any object initialization inside onLayout(). What I mean is don't move the object initializations inside onLayout() to make it "fancier".
None of the proposed answer worked as expected, so there ItemDecoration that i made, it checks if view completely visible in layout manager, and hide rest of views
class HideNotFullyVisibleDecorator : RecyclerView.ItemDecoration() {
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
val count = parent.childCount
for (i in 0 until count) {
val currentChild = parent.getChildAt(i)
currentChild.visibility =
if (parent.layoutManager?.isViewPartiallyVisible(currentChild, true, false) == true)
View.VISIBLE
else
View.GONE
}
}
}
Usage: recycler.addItemDecoration(HideNotFullyVisibleDecorator())
Also in my case I disabled scrolling for recycler
Recycler view is a Scrollable container which holds viewholders and recycles on scroll-up and down,
So it will display as much data as possible on screen, and that fourth half-visible item that you've shown in screenshot is just default behaviour of every scrollable view in android.
You have to customize your viewholders to adjust accordingly on runtime so that only fully visible items should be rendered.
You can do something like:
val availableWidth = screenWidth - (textViewWidth)
val itemWidth = (availableWidth / 3)

How to set the height of an item row in GridLayoutManager

My Recycler Item which inflate in onCreateViewHolder
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp">
<ImageView
android:id="#+id/gridListImageView"
android:layout_width="96dp"
android:layout_height="96dp"
android:src="#drawable/a" />
<TextView
android:id="#+id/gridListView_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="Large Text"
android:textAppearance="?android:attr/textAppearanceLarge" />
</LinearLayout>
I want to display something like this
Which has one row of half the height of recycler View?
And add padding to the rest of the space?
Can i do this by GridLayoutManager?
And this is my GridLayoutManager
GridLayoutManager glm = new GridLayoutManager(getActivity(), 2);
recyclerView.setLayoutManager(glm);
When inflating layout for your views in adapter, you can set their height programmatically. In order to evaluate proper height to use you can rely on parent ViewGroup (that is the RecyclerView itself). Here it is a sample:
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = mLayoutInflater.inflate(R.layout.view_item, parent, false);
// work here if you need to control height of your items
// keep in mind that parent is RecyclerView in this case
int height = parent.getMeasuredHeight() / 4;
itemView.setMinimumHeight(height);
return new ItemViewHolder(itemView);
}
Hope this could help.
As of support library 25.1.0, ABOVE ANSWER DOESN'T WORK. I suggest below modifications:
public MyAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item, parent, false);
GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams) v.getLayoutParams();
lp.height = parent.getMeasuredHeight() / 4;
v.setLayoutParams(lp);
return new ViewHolder(v);
}
You don't need to set the height of an item. The problem here is that image tries to fill all the space. Just add
android:adjustViewBounds="true" to your ImageView and it will not add blank spaces
v.getLayoutParams().width = parent.getMeasuredWidth() / 2;
v.getLayoutParams().height = parent.getMeasuredWidth() / 2;
kotlin version
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
binding.root.post {
binding.root.layoutParams.height = parent.width/3
binding.root.requestLayout()
}
return ViewHolder(binding)
}
here 3 is the span count of your GridLayoutManager
. You can replace binding.root with your itemView , if you are not using Databinding
sometime, getting size of inflate view in adapter return 0 or negative. another approach is get required size from out side the adapter, manipulate it and set it into view. in my case, another problem was size set effectless. so i set the size using layout parameter
here is setting my adapter in activity:
Display display = MainActivity.this.getWindowManager().getDefaultDisplay();
Point size = new Point();
display.getSize(size);
int y = size.y;
y=(int)y/2;
GridLayoutManager linearLayoutManager = new GridLayoutManager(MainActivity.this,2);
recyclerView.setLayoutManager(linearLayoutManager);
NewOrderAdapter newOrderAdapter=new NewOrderAdapter(MainActivity.this,arrayListname,arrayListimage,y);
recyclerView.setAdapter(newOrderAdapter);
and i set view size like this:
#Override
public myviewholder onCreateViewHolder(ViewGroup viewGroup, int i) {
View view = inflator.inflate(R.layout.new_order_row, viewGroup, false);
GridLayoutManager.LayoutParams params = (GridLayoutManager.LayoutParams) view.getLayoutParams();
params.height = ysize;
view.setLayoutParams(params);
myviewholder holder = new myviewholder(view);
return holder;
}
and dont forget to set a height to layout in your layout for initializatiin
On a recent API Level currently 25 the height from the recycler in onCreateViewHolder
is always empty. This snippet is to set the hight after the recycler view's onMeasure
is invoked and set the correct height to the inflated list view.
#Override
public DataBindingViewHolder onCreateViewHolder(final ViewGroup parent, int viewType) {
// You may inflate your view here.
parent.post(new Runnable() {
#Override
public void run() {
int height = parent.getMeasuredHeight() / rows;
View view = holder.getBinding().getRoot();
view.getLayoutParams().height = height;
}
});
return holder;
}
This is my code for adjusting height of recycle view element based on actual aspect-ration required.
#Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext()) .inflate(R.layout.adapter_offers, parent, false);
int width = parent.getMeasuredWidth();
float height = (float) width / Config.ASPECT_RATIO;//(Width/Height)
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) itemView.getLayoutParams();
params.height = Math.round(height);
itemView.setLayoutParams(params);
return new MyViewHolder(itemView);
}
I had a similar requirement which needed a dynamic height for each row in the grid with the ability to add and remove items from the RecyclerView as well. The problem with setting the height layout params in onCreateViewHolder as few of the answers suggests here, is that when an item is added/removed from the RecyclerView, the view is recycled and the onCreateViewHolder is not called again but rather a view within the pool is re-used (if available) and the layout manager would do a pass to calculate the height/width for the item and we would lose the original intended height/width set in the onCreateViewHolder.
This approach below might help who is facing a similar issue.
The key step here is to extend GridLayoutManager and override the following -
generateDefaultLayoutParams
generateLayoutParams
checkLayoutParams
The layout manager would looks something like this -
SpanGridLayoutManager : GridLayoutManager {
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) :
super(context, attrs, defStyleAttr, defStyleRes)
constructor(context: Context, spanCount: Int) : super(context, spanCount)
constructor(context: Context, spanCount: Int, orientation: Int, reverseLayout: Boolean) :
super(context, spanCount, orientation, reverseLayout)
override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {
return spanLayoutSize(super.generateDefaultLayoutParams())
}
override fun generateLayoutParams(c: Context, attrs: AttributeSet): RecyclerView.LayoutParams {
return spanLayoutSize(super.generateLayoutParams(c, attrs))
}
override fun generateLayoutParams(lp: ViewGroup.LayoutParams): RecyclerView.LayoutParams {
return spanLayoutSize(super.generateLayoutParams(lp))
}
override fun checkLayoutParams(lp: RecyclerView.LayoutParams): Boolean {
val layoutParams = generateDefaultLayoutParams()
return super.checkLayoutParams(lp) &&
layoutParams.width == lp.width &&
layoutParams.height == lp.height
}
private fun spanLayoutSize(layoutParams: RecyclerView.LayoutParams): RecyclerView.LayoutParams {
layoutParams.height = if (some_condition) x else y
return layoutParams
}
Here x and y can be the height in pixels that you would need to supply.

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)

how to display footer view to the end of the screen in the case when the list has very few items?

I want to add a footer to the listview. When the number of list items are more,the footer works fine.
But when listview has very few items,the footer gets displayed in the middle of the screen,just below the listview .which looks shabby.In such case i want the footer to align parent bottom.
Thankyou in anticipation.
it is a simplest example of what you want. you can customize it:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/RelativeLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<ListView
android:id="#+id/listView1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="#+id/footer"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true" >
</ListView>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="90dp"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:id="#+id/footer" >
</RelativeLayout>
</RelativeLayout>
if you want do that you said in comment you must set layout param's of footer in code, you must get the size of your list, then get the number of row that shows in screen, then
if (listSize < numRow)
//set footer to bottom of your list
else
// android:layout_alignParentBottom="true"
Maybe your listview's height is set to wrap_content?
As far as I know the footer is added at the bottom of the listview. If you set the listview's height to match_parent it should be aligned to the bottom and also the footer should be displayed there.
(If you use a relative layout simply set listview's attribute alignParentBottom="true" in your .xml file)
You can use RecyclerView with RecyclerView.ItemDecoration to implement this behavior.
public class StickyFooterItemDecoration extends RecyclerView.ItemDecoration {
/**
* Top offset to completely hide footer from the screen and therefore avoid noticeable blink during changing position of the footer.
*/
private static final int OFF_SCREEN_OFFSET = 5000;
#Override
public void getItemOffsets(Rect outRect, final View view, final RecyclerView parent, RecyclerView.State state) {
int adapterItemCount = parent.getAdapter().getItemCount();
if (isFooter(parent, view, adapterItemCount)) {
//For the first time, each view doesn't contain any parameters related to its size,
//hence we can't calculate the appropriate offset.
//In this case, set a big top offset and notify adapter to update footer one more time.
//Also, we shouldn't do it if footer became visible after scrolling.
if (view.getHeight() == 0 && state.didStructureChange()) {
hideFooterAndUpdate(outRect, view, parent);
} else {
outRect.set(0, calculateTopOffset(parent, view, adapterItemCount), 0, 0);
}
}
}
private void hideFooterAndUpdate(Rect outRect, final View footerView, final RecyclerView parent) {
outRect.set(0, OFF_SCREEN_OFFSET, 0, 0);
footerView.post(new Runnable() {
#Override
public void run() {
parent.getAdapter().notifyDataSetChanged();
}
});
}
private int calculateTopOffset(RecyclerView parent, View footerView, int itemCount) {
int topOffset = parent.getHeight() - visibleChildsHeightWithFooter(parent, footerView, itemCount);
return topOffset < 0 ? 0 : topOffset;
}
private int visibleChildsHeightWithFooter(RecyclerView parent, View footerView, int itemCount) {
int totalHeight = 0;
//In the case of dynamic content when adding or removing are possible itemCount from the adapter is reliable,
//but when the screen can fit fewer items than in adapter, getChildCount() from RecyclerView should be used.
int onScreenItemCount = Math.min(parent.getChildCount(), itemCount);
for (int i = 0; i < onScreenItemCount - 1; i++) {
totalHeight += parent.getChildAt(i).getHeight();
}
return totalHeight + footerView.getHeight();
}
private boolean isFooter(RecyclerView parent, View view, int itemCount) {
return parent.getChildAdapterPosition(view) == itemCount - 1;
}
}
Make sure to set match_parent for the RecyclerView height.
Please have a look at the sample application https://github.com/JohnKuper/recyclerview-sticky-footer and how it works http://sendvid.com/nbpj0806
A Huge drawback of this solution is it works correctly only after notifyDataSetChanged() throughout an application(not inside decoration). With more specific notifications it won't work properly and to support them, it requires a way more logic. Also, you can get insights from the library recyclerview-stickyheaders by eowise and improve this solution.

Categories

Resources