Mobile - - By Valdio Veliu

Mastering Complex Lists with the Android RecyclerView

This article was updated on February 1, 2017, for Android 7.1 (API level 25)

For more on Android, watch our tutorial Outputting Data to Your Android App’s User Interface.

The RecyclerView was introduced with Google’s material design in Android 5.0 Lollipop.

If you are getting started with Android development or are already an experienced developer the RecyclerView is something worth investigating.

The idea of view recycling has been in Android since version 1 in the form of the ListView. The idea is simple: to present a large collection of data using a small collection of views, by recycling and rebinding these views.

The RecyclerView is a more flexible pattern of view recycling than ListViews and GridViews. What differentiates the RecyclerView from its predecessors is that it focuses only on recycling views. All the other actions needed to create a view, like how to present data set or inflate the views are delegated to the pluggable classes and that is what makes it so flexible. The hard part is setting up these classes to make a fully functional RecyclerView and that is what I will cover in this article.

To use the RecyclerView you need to follow the following steps:

  1. Add the support library
  2. Add the RecyclerView in the layout XML file
  3. Create a custom row Layout
  4. Create the  RecyclerView.Adapter to populate data into the RecyclerView
  5. Create the ViewHolder to provide a reference to the views for each data item
  6. Bind the Adapter to the RecyclerView in the Activity

Let’s get started with RecyclerView, you can find the code for the final project on GitHub.

Add dependencies

Open build.gradle (app) and add the dependencies needed.

dependencies {
    ...    
    compile 'com.android.support:cardview-v7:25.1.0'
    compile 'com.android.support:recyclerview-v7:25.1.0'
}

Sync Gradle and you are all set.

Add the RecyclerView to the layout file

<RelativeLayout
...    
<android.support.v7.widget.RecyclerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/recyclerview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

</RelativeLayout>

Create a custom row Layout

The row Layout represents the Layout of each single element displayed in the RecyclerView.

Create a file named row_layout.xml and add the following to it:

<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/cardView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="@dimen/activity_vertical_margin"
    android:clickable="true"
    android:focusable="true"
    android:foreground="?android:attr/selectableItemBackground"
    app:cardCornerRadius="@dimen/activity_vertical_margin"
    app:cardElevation="@dimen/activity_vertical_margin">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="16dp">

        <ImageView
            android:id="@+id/imageView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentTop="true"
            android:layout_marginRight="16dp" />

        <TextView
            android:id="@+id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentTop="true"
            android:layout_toRightOf="@+id/imageView"
            android:text="Title"
            android:textSize="30sp" />

        <TextView
            android:id="@+id/description"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/title"
            android:layout_toRightOf="@+id/imageView"
            android:text="Description" />

    </RelativeLayout>

</android.support.v7.widget.CardView>

Just a note here: when you create the item layout of the RecyclerView don’t forget to add the following lines in the ViewGroup container of the layout. This lines of code will add the ripple effect to the RecyclerView elements.

    android:clickable="true"
    android:focusable="true"
    android:foreground="?android:attr/selectableItemBackground"

The RecyclerView will be a list of randomly chosen movies and a description. We need a class that represents a single RecyclerView item data, create a file named Data.java and add the following:

public class Data {
    public String title;
    public String description;
    public int imageId;

    Data(String title, String description, int imageId) {
        this.title = title;
        this.description = description;
        this.imageId = imageId;
    }

}

The RecyclerView Adapter

The RecyclerView.Adapter is similar to the Adapters used on a ListView but with a ViewHolder required to improve performance. A ListView has adapters for different sources such as the ArrayAdapter for arrays and CursorAdapter for database results. The RecyclerView.Adapter requires a custom implementation to supply data to the adapter.

The adapter has three methods.

  • onCreateViewHolder() inflates the row layout is and initializes the View Holder. Once the View Holder is initialized it manages the findViewById() methods, finding the views once and recycling them to avoid repeated calls.
  • onBindViewHolder() uses the View Holder constructed in the onCreateViewHolder() method to populate the current row of the RecyclerView with data.

Create a new file named Recycler_View_Adapter.java with the following class:

public class Recycler_View_Adapter extends RecyclerView.Adapter<View_Holder> {

    List<Data> list = Collections.emptyList();
    Context context;

    public Recycler_View_Adapter(List<Data> list, Context context) {
        this.list = list;
        this.context = context;
    }

    @Override
    public View_Holder onCreateViewHolder(ViewGroup parent, int viewType) {
        //Inflate the layout, initialize the View Holder
        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_layout, parent, false);
        View_Holder holder = new View_Holder(v);
        return holder;

    }

    @Override
    public void onBindViewHolder(View_Holder holder, int position) {

        //Use the provided View Holder on the onCreateViewHolder method to populate the current row on the RecyclerView
        holder.title.setText(list.get(position).title);
        holder.description.setText(list.get(position).description);
        holder.imageView.setImageResource(list.get(position).imageId);

        //animate(holder);

    }

    @Override
    public int getItemCount() {
        //returns the number of elements the RecyclerView will display
        return list.size();
    }

    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
        super.onAttachedToRecyclerView(recyclerView);
    }

    // Insert a new item to the RecyclerView on a predefined position
    public void insert(int position, Data data) {
        list.add(position, data);
        notifyItemInserted(position);
    }

    // Remove a RecyclerView item containing a specified Data object
    public void remove(Data data) {
        int position = list.indexOf(data);
        list.remove(position);
        notifyItemRemoved(position);
    }

}

Creating the ViewHolder

The RecyclerView uses a ViewHolder to store the references to the relevant views for one entry in the RecyclerView. This solution avoids all the findViewById() method calls in the adapter to find the views to be filled with data.

Create a file named View_Holder.java with the following class:

public class View_Holder extends RecyclerView.ViewHolder {

   CardView cv;
   TextView title;
   TextView description;
   ImageView imageView;

   View_Holder(View itemView) {
       super(itemView);
       cv = (CardView) itemView.findViewById(R.id.cardView);
       title = (TextView) itemView.findViewById(R.id.title);
       description = (TextView) itemView.findViewById(R.id.description);
       imageView = (ImageView) itemView.findViewById(R.id.imageView);
   }
}

The LayoutManager

The Layout Manager attaches, measures, and lays out all the child views of the RecyclerView in real-time. As the user scrolls the view, the Layout Manager determines when new child views will be added and when the old child views will be detached and deleted.

These default implementations are available:

  • LinearLayoutManager – Displays items in a vertical or horizontal scrolling list.
  • GridLayoutManager – Displays items in a grid.
  • StaggeredGridLayoutManager – Displays items in a staggered grid.

You can create a custom LayoutManager by extending RecyclerView.LayoutManager or one of the implementations above and overriding the methods required.

Now that the RecyclerView is finished the next step is to fill it with some data. In the onCreate() method of the MainActivity class create an instance of the Recycler_View_Adapter and give this adapter the list of data and the context. The getApplication() method will provide the application context.

List<Data> data = fill_with_data();

RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerview);
Recycler_View_Adapter adapter = new Recycler_View_Adapter(data, getApplication());
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(new LinearLayoutManager(this));

We will also need sample data, for this example I have created a default function, in a real app, this may come from a data source.

public List<Data> fill_with_data() {

    List<Data> data = new ArrayList<>();

    data.add(new Data("Batman vs Superman", "Following the destruction of Metropolis, Batman embarks on a personal vendetta against Superman ", R.drawable.ic_action_movie));
    data.add(new Data("X-Men: Apocalypse", "X-Men: Apocalypse is an upcoming American superhero film based on the X-Men characters that appear in Marvel Comics ", R.drawable.ic_action_movie));
    data.add(new Data("Captain America: Civil War", "A feud between Captain America and Iron Man leaves the Avengers in turmoil.  ", R.drawable.ic_action_movie));
    data.add(new Data("Kung Fu Panda 3", "After reuniting with his long-lost father, Po  must train a village of pandas", R.drawable.ic_action_movie));
    data.add(new Data("Warcraft", "Fleeing their dying home to colonize another, fearsome orc warriors invade the peaceful realm of Azeroth. ", R.drawable.ic_action_movie));
    data.add(new Data("Alice in Wonderland", "Alice in Wonderland: Through the Looking Glass ", R.drawable.ic_action_movie));

    return data;
}

You can find the icon files for the ic_action_movie button referenced above here.

This completes all the steps to set up a RecyclerView.

Animating recycles with ItemAnimator

So far I’ve explained all that’s needed to create your own RecyclerView and explained what each part of the RecyclerView structure does. Now lets make things more interesting with item animation.

RecyclerView.ItemAnimator is a class that defines the animations performed on items and will animate ViewGroup changes such as add/delete/select notified to the adapter. DefaultItemAnimator is a basic animation available by default with the RecyclerView.

To customize the DefaultItemAnimator add an item animator to the RecyclerView. This code slows down the process of adding and removing items from the RecyclerView.

Add this after our last code in the onCreate method:

RecyclerView.ItemAnimator itemAnimator = new DefaultItemAnimator();
itemAnimator.setAddDuration(1000);
itemAnimator.setRemoveDuration(1000);
recyclerView.setItemAnimator(itemAnimator);

Add view

Remove view

Another approach to animating RecyclerView items is to use Android Interpolators. According to developer.android.com, an interpolator defines the rate of change of an animation.

The following examples represent two animations I implemented using these interpolators. Save the files in /anim/ in the project resources folder.

anticipate_overshoot_interpolator.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
   android:interpolator="@android:anim/anticipate_overshoot_interpolator">
   <translate
       android:fromYDelta="-50%p"
       android:toYDelta="0"
       android:duration="2000"
       />
</set>

bounce_interpolator.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
   android:interpolator="@android:anim/bounce_interpolator">
   <translate
       android:duration="1500"
       android:fromYDelta="-150%p"
       android:toYDelta="0"
       />
</set>

Now let’s return to the RecyclerView. Inside the RecyclerView Adapter class add the following function.

public void animate(RecyclerView.ViewHolder viewHolder) {
   final Animation animAnticipateOvershoot = AnimationUtils.loadAnimation(context, R.anim.bounce_interpolator);
   viewHolder.itemView.setAnimation(animAnticipateOvershoot);
}

If you take a closer look at the loadAnimation() method, it requires a context parameter and that’s why the Adapter’s constructor was modified in the first place. Now we can animate the RecyclerView. Inside the onBindViewHolder() method, call the animate() function and pass the holder parameter of the View_Holder class.

Run the app and test the animation. Here is how the bounce_interpolator animation works.

bounce animation

Next let’s test the anticipate_overshoot_interpolator animation, but first I’ll set the RecyclerView’s layout manager to StaggeredGrid in the Main Activity’s onCreate() method with a 2 column span and vertical scrolling.

recyclerView.setLayoutManager(new StaggeredGridLayoutManager(2,StaggeredGridLayoutManager.VERTICAL));

anticipate_overshoot animation

Using this method of animation in the RecyclerView is easy. If you want new animations, just change the interpolation type in the examples above, set the parameters to your preference and you will have a new animation style.

Having trouble setting onItemClickListener to RecyclerView items?

Even though displaying elements in RecyclerView is better, in terms of performance, than its predecessors, ListView and GridView. These views have the ability to add item click listeners to intercept which element of the list was clicked.
Here is how it looks on a ListView:

ListView listView = ...;
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

    }
});

Unfortunately, there is no such thing for RecyclerViews, but the following solution might provide with a solution to this.

Crete a Java interface like the following.

public interface RecyclerViewItemClickListener {
    public void onClick(View view, int position);

    public void onLongClick(View view, int position);
}

As you can see from this interface the idea is to provide a solution similar to that of the ListView for both onClick and onLongClick of the RecyclerViews items.

To detect the item of the RecyclerView which is clicked we need a helper class.

public class CustomRVItemTouchListener implements RecyclerView.OnItemTouchListener {

    //GestureDetector to intercept touch events
    GestureDetector gestureDetector;
    private RecyclerViewItemClickListener clickListener;

    public CustomRVItemTouchListener(Context context, final RecyclerView recyclerView, final RecyclerViewItemClickListener clickListener) {
        this.clickListener = clickListener;
        gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {

            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                return true;
            }

            @Override
            public void onLongPress(MotionEvent e) {
                //find the long pressed view
                View child = recyclerView.findChildViewUnder(e.getX(), e.getY());
                if (child != null && clickListener != null) {
                    clickListener.onLongClick(child, recyclerView.getChildLayoutPosition(child));
                }
            }
        });
    }

    @Override
    public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent e) {

        View child = recyclerView.findChildViewUnder(e.getX(), e.getY());
        if (child != null && clickListener != null && gestureDetector.onTouchEvent(e)) {
            clickListener.onClick(child, recyclerView.getChildLayoutPosition(child));
        }
        return false;
    }

    @Override
    public void onTouchEvent(RecyclerView rv, MotionEvent e) {

    }

    @Override
    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {

    }
}

Basically what this class does is, detect the RecyclerView element under the (X, Y) position where the screen was clicked. This class is helpful for both click types created by the interface.

Finally, here is how this new click listener is implemented for the RecyclerView.

recyclerView.addOnItemTouchListener(new CustomRVItemTouchListener(this, recyclerView, new RecyclerViewItemClickListener() {
    @Override
    public void onClick(View view, int position) {

    }

    @Override
    public void onLongClick(View view, int position) {

    }
}));

As you can see the custom touch listener provides a reference of the RecyclerView item which was clicked and its position in the list.

Conclusion

The RecyclerView is a flexible pattern for view recycling eliminating the imitations of ListView and GridView by making item animations easy to set and reuse. Getting used to development with RecyclerView may need some work at first, but it’s worth your time.

If you have any questions or comments please let me know below.

For more on Android, watch our tutorial Outputting Data to Your Android App’s User Interface.

Sponsors
Login or Create Account to Comment
Login Create Account