Adapter / LinearLayoutManager / RecyclerView

Scrolling RecyclerView – Part 1

In this series of articles we’ll take a look in to scrolling behaviour of RecyclerView, and discover some oddities that may crop up, and some options for fixing them.

Recently I encountered some strange behaviour when using smoothScrollToPosition() on RecyclerView. So let’s start by creating a simple RecyclerView-based project to demonstrate the problem.

Let’s start with a layout containing a single RecyclerView:

<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/recyclerview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:scrollbars="vertical"
    android:fadeScrollbars="true"
    tools:context=".MainActivity" />

Next a menu containing a couple of actions:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/action_top"
        android:icon="@drawable/menu_up"
        android:showAsAction="ifRoom"
        android:title="@string/to_top" />

    <item
        android:id="@+id/action_bottom"
        android:icon="@drawable/menu_down"
        android:showAsAction="ifRoom"
        android:title="@string/to_bottom" />

</menu>

There are also a couple of VectorDrawable icons (menu_up and menu_down) which I haven’t listed here, but are available in the source.

We also need a simple Adapter for our RecyclerView which will create a large list of 1000 simple list items:

public final class LargeAdapter extends RecyclerView.Adapter<LargeAdapter.ViewHolder> {
    private static final int SIZE = 1000;
    private final List<String> items;

    public static LargeAdapter newInstance(Context context) {
        List<String> items = new ArrayList<>();
        String format = context.getString(R.string.item_string);
        for (int i = 0; i < SIZE; i++) {
            items.add(String.format(format, i + 1));
        }
        return new LargeAdapter(items);
    }

    private LargeAdapter(List<String> items) {
        this.items = items;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_list_item_1, parent, false);
        return ViewHolder.newInstance(view);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        String text = items.get(position);
        holder.setText(text);
    }

    @Override
    public int getItemCount() {
        return items.size();
    }

    public static final class ViewHolder extends RecyclerView.ViewHolder {
        private final TextView textView;

        public static ViewHolder newInstance(View itemView) {
            TextView textView = (TextView) itemView.findViewById(android.R.id.text1);
            return new ViewHolder(itemView, textView);
        }

        private ViewHolder(View itemView, TextView textView) {
            super(itemView);
            this.textView = textView;
        }

        public void setText(CharSequence text) {
            textView.setText(text);
        }
    }
}

I won’t bother with an explanation of this as there’s nothing in here which hasn’t already been covered in Material – Part 5.

Finally we hook this all up in our main Activity class:

public class MainActivity extends Activity {

    private RecyclerView recyclerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        recyclerView = (RecyclerView) findViewById(R.id.recyclerview);
        recyclerView.setAdapter(LargeAdapter.newInstance(this));
        recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main_menu, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();

        if (id == R.id.action_top) {
            recyclerView.smoothScrollToPosition(0);
            return true;
        } else if (id == R.id.action_bottom) {
            recyclerView.smoothScrollToPosition(recyclerView.getAdapter().getItemCount());
            return true;
        }

        return super.onOptionsItemSelected(item);
    }
}

So what we’ve created here is a RecyclerView using a LinearLayoutManager with an Adapter of 1000 very simply list items. Then we have two menu actions which will perform a smooth scroll to the top and bottom of the list.

This is all pretty straightforward stuff.

But if we try this out, we begin to see the problem:

If we scroll a small distance down and then hit the “go to top” action everything is fine. However if we’re at or near the top and hit the “go to bottom” button then it certainly scrolls smoothly to the bottom but actually takes around seven second to get there. This is far too long to keep the user waiting and would result in some very frustrated users if we were to include this behaviour as-is in a real app.

My initial thought when I saw this behaviour is that there must me a simple setting somewhere which would speed up the scroll speed. However that proved not to be the case because there is an awful lot more going on here than is immediately apparent.

In the next article we’ll have a deep explore of how this scrolling is actually performed so that we may fully understand the why it isn’t possible to have a simple setting to change the scroll duration.

The source code for this article is available here.

© 2015, Mark Allison. All rights reserved.

Copyright © 2015 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

8 Comments

  1. This is one of the components in RecyclerView where I wish we had more control on. I managed to use a custom Interpolator and set a different scrolling speed, but the code doesn’t look pretty at all :/

  2. Great article!

    recyclerView.smoothScrollToPosition(recyclerView.getAdapter().getItemCount());

    Should not it be getItemCount() – 1 ?
    If you tried using scrollToPossition(), it doesn’t work unless you decrement the item count by one.
    I think it would be more consistent to decrement it here too.
    Correct me if I’m wrong

    Cheers

  3. Great post man, conrgatulations.
    It helped me a lot, but I still have som issues with smoothScroll, the thing is that I am using smoothScrollBy(x,y), and i am passing the method the width of my next view, every view has the same width not even a dp of difference, but sometimes the method does not scroll the appropriatte distance.
    Do you have an idea why this is happening??

  4. SmoothScrollToPosition not working if recycler view is inside NestedScrollView did you face any problem like this

  5. public static ViewHolder newInstance(View itemView) {
    TextView textView = (TextView) itemView.findViewById(android.R.id.text1);
    return new ViewHolder(itemView, textView);
    }

    In above method, in which xml file contains the view – text1 ? . There is no such control called text1

    1. Yes there is. It is in the layout inflated in onCreateViewHolder(): android.R.layout.simple_list_item_1 which is a standard system layout, hence the use of android.R prefix rather than accessing resources in the local R.java

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.