Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to force RecyclerView to scroll when there's not enough items to fill the height of the screen

I made a RecyclerView inside a NavigationView with a custom first item as an equivalent to ListView's addHeaderView(). On the bottom part of that header layout I placed an EditText so that I can filter out the list below.

What I wanted to do was to scroll the RecyclerView all the way up until the EditText would be the topmost item whenever the EditText gains focus. However if my item count is small enough that it wouldn't be able to fill-up the entire height of the NavigationView, the RecyclerView refuses to scroll all the way up and thus showing a bit more of the header other than the EditText. As an added problem, even if the list is large enough, it stills scrolls back down once I start filtering the list items.

How can I force my RecyclerView to still scroll all the way up even if I don't have enough items in to fill the height of my view so that the rest of my header do not take a peek?

Here's a visualization of what I'm trying to achieve:

 _______________
| HEADER LINE 1 |
| HEADER LINE 2 |
| HEADER LINE 3 |
| ------------- |
|  Search box   |
| ------------- |
| List item 1   |
| List item 2   |
| List item 3   |
 ---------------

Expected behavior when I focus on the search box:

 _______________
| ------------- |
|  Search box   | <- becomes the topmost item. I can already achieve this
| ------------- |    with RecyclerView.smoothScrollBy(dx, dy)
| List item 1   |    except when the list is small enough (see below)
| List item 2   |
| List item 3   |
| List item 4   |
|               | <- empty space is okay and is desired if dataset
|               |    is small enough
 ---------------

What actually happens:

 _______________
| HEADER LINE 2 |  <- the header is 'bleeding' on top
| HEADER LINE 3 |
| ------------- |
|  Search box   |  <- search box cannot take the topmost spot
| ------------- |     as the recycler tries to position itself to fill in
| List item 1   |     the bottom part with items
| List item 2   |
| List item 3   |
| List item 4   |  <- items are pushed down to the bottom to fill
 ---------------      in the space

Here's a snippet of my NavigationView xml (just your typical setup):

<android.support.design.widget.NavigationView
    android:id="@+id/navigation_view"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:layout_gravity="start"
    android:fitsSystemWindows="true">
    <android.support.v7.widget.RecyclerView
        android:id="@+id/drawer_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</android.support.design.widget.NavigationView>

ViewHolder with custom header here (RecyclerView Adapter):

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    if(viewType == HEADER) {
        return new HeaderHolder(mHeaderView);
    } else {
        View v = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.list_item, parent, false);

        return new ItemViewHolder(v);
    }
}

The drawer header is just an xml layout I inflated in the main activity (I assigned listeners and animations there) and passed on to my RecyclerView adapter via a setHeaderView(View header) I made.

like image 527
leondzn Avatar asked Feb 22 '17 04:02

leondzn


1 Answers

What you need is a footer which is the same height as your header.

There are two scenarios to consider here:

  1. Your header has a fixed height
  2. Your header has a dynamic height

In the first case, the solution is very simple. Just define the height of your header in some value like <dimen name="header_height">300dp</dimen> and then define your header and footer like the following:

<FrameLayout
    android:layout_height="@dimen/header_height">

    <!-- Content goes here -->

</FrameLayout>

In the second case, we're going to have to do a couple of things:

  • Retrieve the header's height whenever it changes
  • Update the footer's height to whatever value the header's height is

There are lots of ways to do this. One such way that I have used before is that you could use a ViewTreeObserver.OnPreDrawListener on the RecyclerView to detect when the header has a height (and subsequently if it has changed) and then pass that height to your RecyclerView.Adapter which will use it to set the requested height for your footer when it inflates it, and then you just need to call notifyItemChanged(int) on the adapter with your footer's index to tell it to re-inflate your footer. Here's most of what I just described in code:

final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.drawer_content);
recyclerView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
    @Override
    public boolean onPreDraw() {
        if(recyclerView.getChildCount() < 1 || recyclerView.getChildAt(0) == null || recyclerView.getChildAt(0).getHeight() == 0) {
            return true;
        }

        int headerHeight = recyclerView.getChildAt(0).getHeight();
        adapter.setFooterHeight(headerHeight);

        return true;
    }
});

And in your adapter:

int footerHeight = 0;

public void setFooterHeight(int footerHeight){
    if(this.footerHeight == footerHeight){
        return;
    }

    this.footerHeight = footerHeight;
    notifyItemChanged(getItemCount() - 1);
}

And then the only other thing you'd need to do is inflate your footer like you are your header already and set its height to footerHeight.

Hope this helps.

like image 55
DanielLaneDC Avatar answered Oct 18 '22 22:10

DanielLaneDC