21

I have RecyclerView and I need next behavior:

  • if there are a lot of items (more then fits screen) - footer is last item
  • if few item/no item - footer is located at screen bottom

Please advise how can I implement this behavior.

resource8218
  • 1,357
  • 16
  • 30
  • you can put your last item with another layout to the bottom of the screen and you can put your recyclerview to top of it.so it will be always at bottom and your recycleview wont be effected – salihgueler Nov 24 '15 at 10:12
  • 1
    nomad, sorry, but it is not expected result: if I would have last recyclerview item transparent - so on scroll last item (my footer) would not have proper animation (instead of to be sticky to last-1 item it would be appears from bottom). I hope you've understood my mind. – resource8218 Nov 24 '15 at 10:18
  • checkout https://gist.github.com/mheras/0908873267def75dc746 and this SO Answer : http://stackoverflow.com/a/29168617/3140227 – Vipul Asri Nov 27 '15 at 05:40
  • Vipus Asri, sorry, but it is not expected result. Please see my comment for RexSplode suggested solution. – resource8218 Nov 27 '15 at 10:36

8 Answers8

11

You can use 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.

lelloman
  • 12,742
  • 5
  • 55
  • 75
  • Calling `notifyDataSetChanged()` in `getItemOffset` is a bad idea, this can result in an infinite loop. Which happened in my case. – AmeyaB Jan 11 '17 at 19:25
  • it's a very nice solution, just one thing I had to add in `visibleChildsHeightWithFooter`, top and bottom margin for each view – lelloman Jul 05 '17 at 10:21
  • @AmeyaB below solution solves the problem i believe. – Abhimaan Dec 17 '17 at 15:10
  • I found this method to be really bad for a couple of reason. `ItemDecoration` is only called when the view is created or recycled and got back to view. Therefore, if you add that space to move your footer down, that space will never change until the view with the space add gets recycled or replaced. This is very problematic for dynamic list. calling `adapter.notifyDataSetChanged()` is not only bad in on itself but also really bad when you call it inside `ItemDecoration` which have no business doing so. – Archie G. Quiñones May 27 '20 at 09:19
  • Another thing, view.getHeight() returns 0 because it isn't mesured yet for performance reason. I will post a different answer of how I solve this issue to guide anyone else who's having the same trouble. – Archie G. Quiñones May 27 '20 at 09:20
10

Improvising on Dmitriy Korobeynikov and solving the problem of calling notify dataset changed

public class StickyFooterItemDecoration extends RecyclerView.ItemDecoration {

  @Override
  public void getItemOffsets(Rect outRect, final View view, final RecyclerView parent,
      RecyclerView.State state) {

    int position = parent.getChildAdapterPosition(view);
    int adapterItemCount = parent.getAdapter().getItemCount();
    if (adapterItemCount == RecyclerView.NO_POSITION || (adapterItemCount - 1) != position) {
      return;
    }
    outRect.top = calculateTopOffset(parent, view, adapterItemCount);
  }


  private int calculateTopOffset(RecyclerView parent, View footerView, int itemCount) {
    int topOffset =
        parent.getHeight() - parent.getPaddingTop() - parent.getPaddingBottom()
            - visibleChildHeightWithFooter(parent, footerView, itemCount);
    return topOffset < 0 ? 0 : topOffset;
  }



  private int visibleChildHeightWithFooter(RecyclerView parent, View footerView, int itemCount) {
    int totalHeight = 0;
    int onScreenItemCount = Math.min(parent.getChildCount(), itemCount);
    for (int i = 0; i < onScreenItemCount - 1; i++) {
      RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) parent.getChildAt(i)
          .getLayoutParams();
      int height =
          parent.getChildAt(i).getHeight() + layoutParams.topMargin
              + layoutParams.bottomMargin;
      totalHeight += height;
    }
    int footerHeight = footerView.getHeight();
    if (footerHeight == 0) {
      fixLayoutSize(footerView, parent);
      footerHeight = footerView.getHeight();
    }
    footerHeight = footerHeight + footerView.getPaddingBottom() + footerView.getPaddingTop();

    return totalHeight + footerHeight;
  }

  private void fixLayoutSize(View view, ViewGroup parent) {
    // Check if the view has a layout parameter and if it does not create one for it
    if (view.getLayoutParams() == null) {
      view.setLayoutParams(new ViewGroup.LayoutParams(
          ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
    }

    // Create a width and height spec using the parent as an example:
    // For width we make sure that the item matches exactly what it measures from the parent.
    //  IE if layout says to match_parent it will be exactly parent.getWidth()
    int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
    // For the height we are going to create a spec that says it doesn't really care what is calculated,
    //  even if its larger than the screen
    int heightSpec = View.MeasureSpec
        .makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);

    // Get the child specs using the parent spec and the padding the parent has
    int childWidth = ViewGroup.getChildMeasureSpec(widthSpec,
        parent.getPaddingLeft() + parent.getPaddingRight(), view.getLayoutParams().width);
    int childHeight = ViewGroup.getChildMeasureSpec(heightSpec,
        parent.getPaddingTop() + parent.getPaddingBottom(), view.getLayoutParams().height);

    // Finally we measure the sizes with the actual view which does margin and padding changes to the sizes calculated
    view.measure(childWidth, childHeight);

    // And now we setup the layout for the view to ensure it has the correct sizes.
    view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
  }
}
Abhimaan
  • 1,875
  • 2
  • 18
  • 26
  • tap Home button to hide app, open app again -> footer is below the bottom of the screen and I need to scroll to see it (recycler view has 3 items in total with footer). visibleChildHeightWithFooter() return different values, as I figured - height of children is different when open screen from another screen, and when open app from background on that screen. parent.getChildCount() return 2 instead of 3 items. What can cause such behaviour? – Wackaloon Mar 30 '18 at 08:30
  • 1
    @AlexanderAgeychenko when every you data is changed pls remember to to call the function invalidateItemDecorations. Hope this helps – Abhimaan Apr 02 '18 at 07:49
1

I am using a Linearlayout with weights. I created multiple values for the footer weight, it works perfectly.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    android:orientation="vertical"

<include layout="@layout/header" />

    <android.support.v7.widget.RecyclerView
    android:id="@+id/recycleView"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="0.5"
    tools:layout_height="0dp"
    tools:listitem="@layout/row" />

<TextView
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="@dimen/footer_weight"
    android:padding="@dimen/extra_padding"
    android:paddingEnd="@dimen/small_padding"
    android:paddingLeft="@dimen/small_padding"
    android:paddingRight="@dimen/small_padding"
    android:paddingStart="@dimen/small_padding"
    android:text="@string/contact"
    android:textColor="@color/grey" />

 </LinearLayout>
Karoly
  • 5,070
  • 4
  • 30
  • 56
0

I know, that this is an old question, but I'll add an answer for those who would search for such decision in future. It is POSSIBLE to keep last item at the bottom of the screen in case you have only few or no items and make the last item to scroll with the recyclerview when you have many items.

How to achieve. Your RecyclerView adapter should apply several view types: views, which should be shown as a list item; view, which should be shown as footer; an empty view. You may check how to put items with different views to the RecyclerView here: https://stackoverflow.com/a/29362643/6329995 Locate an empty view between your main list and the footer view. Then in onBindViewHolder for the empty view check whether your main list views and footer view take all screen. If yes - set empty view height to zero, otherwise set it to the height which appears to be not taken by items and footer. That's all. You may also update that height dynamically, when you delete/add rows. Just call notifyItemChanged for your empty space item after you update the list.

You'd also set your RecyclerView height to match_parent or exact height, NOT wrap_content!

Hope this helps.

Community
  • 1
  • 1
Lev161
  • 103
  • 5
  • I get how it works when there are few items, but when there are lots of items how do you make sure the footer item is at the bottom of the screen? – Adam Katz Mar 03 '17 at 13:23
0

All these solutions don't work. When you minimize app and open it again, footer flies lower than the bottom of the screen and you need to scroll to see it, even if there are just 1-2 items. You can add footer view below your recycler view in xml.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 
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"
android:background="@android:color/white">

<android.support.v4.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true"
    android:overScrollMode="never"
    android:scrollbars="none">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

        <android.support.v4.widget.Space
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:minHeight="1dp" />

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <include layout="@layout/recyclerView_footer" />
        </FrameLayout>
    </LinearLayout>
</android.support.v4.widget.NestedScrollView>

pay attention - I used NestedScrollView with

    recyclerView.isNestedScrollingEnabled = false

and SpaceView has weight 1 and height = 0dp and all this stuff inside of linear layout and NestedScrollView has height = match_parent, now I have footer stuck to the bottom and it is moving further when list become larger

Wackaloon
  • 1,775
  • 1
  • 12
  • 30
0
class FooterViewHolder(private val parent: ViewGroup) {

...

fun bind(item: Item) {
    ...
    itemView.post(::adjustTop)
}

private fun adjustTop() {
    val parent = parent as RecyclerView
    var topOffset = parent.height
    for (child in parent.children) topOffset -= child.height
    (itemView.layoutParams as ViewGroup.MarginLayoutParams)
        .setMargins(0, topOffset.coerceAtLeast(0), 0, 0)
}
}
Skotos
  • 81
  • 1
  • 1
0

The selected answer is flawed. I already commented on it and explained why so. You may want to read that if your interested.

So if the selected answer is wrong, whats a different better way to solve this?

1) Create you layout like so:

<ConsraintLayout>
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipToPadding="false"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

    <-- This is your footer and it can be anything you want -->
    <TextView
        android:id="@+id/yourFooter"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

</ConstraintLayout>

2) Set the height of your footer as bottomPadding of your RecyclerView. It is crucial to do on preDraw so you can have the proper height or size of yur footer.

view.doOnPreDraw {
    val footerheight = yourFooter.height
    recyclerView.updatePadding(bottom = footerHeight)
    ...
}

3) Now all you need to do is to listen to recyclerview scroll and listen when you need to translate you footer at the correct time. So do something like:

view.doOnPreDraw {
    val footerheight = yourFooter.height
    recyclerView.updatePadding(bottom = footerHeight)

    recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            val range = recyclerView.computeVerticalScrollRange()
            val extent = recyclerView.computeVerticalScrollExtent()
            val offset = recyclerView.computeVerticalScrollOffset()
            val threshHold = range - footerHeight
            val currentScroll = extent + offset
            val excess = currentScroll - threshHold
            yourFooter.transalationX = if (excess > 0)
                footerHeight * (excess.toFloat()/footerHeight.toFloat()) else 0F
        }
    })
}

Hope this would be helpful to someone in the future.

Archie G. Quiñones
  • 5,945
  • 5
  • 32
  • 71
-1

If you cannot forget about RecyclerView and use ListView, then go check this link out Is there an addHeaderView equivalent for RecyclerView? it has everything you need. It's about header, but it's pretty much the same, except that header is in the beginning of your list and footer is in the end.

Community
  • 1
  • 1
RexSplode
  • 1,347
  • 1
  • 11
  • 22
  • 2
    Thanks for your link! Unfortunately, there are no solution for keeping footer at the screen bottom if there are no/few items. – resource8218 Nov 24 '15 at 15:43