RecyclerView

Visually representing collections of items is an important aspect of many mobile apps. The classic Android implementation of this was the AdapterView family of widgets: ListView, GridView, Spinner, and so on. However, they had their limitations, particularly with respect to advanced capabilities like animating changes in the list contents.

In 2014, Google released RecyclerView, via the Android Support package. Developers can add the recyclerview-v7 to their projects and use RecyclerView as a replacement for most of the AdapterView family. RecyclerView was written from the ground up to be a more flexible container, with lots of hooks and delegation to allow behaviors to be plugged in.

This had two major impacts:

  1. RecyclerView is indeed much more powerful than its AdapterView counterparts
  2. RecyclerView, out of the box, is nearly useless, and wiring together enough stuff to even replicate basic ListView/GridView functionality takes quite a bit of code

In this chapter, we will review RecyclerView from the ground up, starting with basic operation. Many of the ListView samples from elsewhere in the book will be replicated here, to see how to pull off the same things with RecyclerView. And, we will explore some of the additional capabilities that make RecyclerView perhaps worth the effort on high-end Android applications.

Prerequisites

Understanding this chapter requires that you have read the core chapters, particularly the one on AdapterView and adapters.

One section involves the use of custom XML drawables. Another section demonstrates using content pulled from the MediaStore ContentProvider.

This chapter also covers things like action modes and other advanced ListView techniques.

AdapterView and its Discontents

AdapterView, and particularly its ListView and GridView subclasses, serve important roles in Android application development. And, for basic scenarios, they work reasonably well.

However, there are issues.

Perhaps the biggest tactical issue is that updating an AdapterView tends to be an all-or-nothing affair. If there is a change to the model data — new rows added, existing rows removed, or data changes that might affect the AdapterView presentation — the only well-supported solution is to call notifyDataSetChanged() and have the AdapterView rebuild itself. This is slow and can have impacts on things like choice states. And, if you wanted to get really elaborate about your changes, and use animated effects to show where rows got added or removed, that was halfway to impossible.

Strategically, AdapterView, AbsListView (the immediate parent of ListView and GridView), and ListView are large piles of code that resemble pasta to many outsiders. There are so many responsibilities piled into these classes that maintainability was a challenge for Google and extensibility was a dream more than a reality.

Enter RecyclerView

RecyclerView is designed to correct those sorts of flaws.

RecyclerView, on its own, does very little other than help manage view recycling (e.g., row recycling of a vertical list). It delegates almost everything else to other classes, such as:

This is on top of the adapters and view holders that were the hallmarks of conventional AdapterView usage.

Because things like layout managers are handled via abstract classes and replaceable concrete implementations, third-party developers can contribute options for developers to use, just as Google does. Later in this chapter, we will explore some of these contributions.

On the flip side, though, RecyclerView does much less “out of the box” than does ListView or GridView. Not everything that is missing is supplied anywhere in the recyclerview-v7 library, requiring that you either roll a bunch of code yourself or rely upon those third-party libraries to get anything much done.

A Trivial List

Back in the original chapter on AdapterView and adapters, we had the Selection/Dynamic sample app. This app would display a list of 25 Latin words, each with the word’s length and an accompanying icon (different for short and long words):

The Dynamic Sample Application
Figure 543: The Dynamic Sample Application

Here, we will review the RecyclerView/SimpleList sample project, which is a first pass at porting the Selection/Dynamic demo over to use RecyclerView.

The Dependency

Any project that wishes to use RecyclerView needs to have access to the recyclerview-v7 library from the Android Support package. Android Studio users can simply have a reference to it in the top-level dependencies closure:

apply plugin: 'com.android.application'

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

android {
    compileSdkVersion 22
    buildToolsVersion "22.0.1"
}

(from RecyclerView/SimpleList/app/build.gradle)

However, if you are using recyclerview-v7, you want to use version 23 or higher of that library. There are changes to ART – the Android runtime used on Android 5.0+ — that apparently will break the older versions of recyclerview-v7 when running on Android 6.0+ devices.

A RecyclerViewActivity

With ListView, we could use ListActivity, to isolate some of the ListView-management code. There is no RecyclerViewActivity in the recyclerview-v7 library… but we can create one:

package com.commonsware.android.recyclerview.simplelist;

import android.app.Activity;
import android.support.v7.widget.RecyclerView;

public class RecyclerViewActivity extends Activity {
  private RecyclerView rv=null;

  public void setAdapter(RecyclerView.Adapter adapter) {
    getRecyclerView().setAdapter(adapter);
  }

  public RecyclerView.Adapter getAdapter() {
    return(getRecyclerView().getAdapter());
  }

  public void setLayoutManager(RecyclerView.LayoutManager mgr) {
    getRecyclerView().setLayoutManager(mgr);
  }

  public RecyclerView getRecyclerView() {
    if (rv==null) {
      rv=new RecyclerView(this);
      rv.setHasFixedSize(true);
      setContentView(rv);
    }

    return(rv);
  }
}

(from RecyclerView/SimpleList/app/src/main/java/com/commonsware/android/recyclerview/simplelist/RecyclerViewActivity.java)

The important part is the getRecyclerView() method. Here, if we have not already initialized the RecyclerView, we create an instance of it and set it as the activity’s content view via setContentView(). Along the way, we call setHasFixedSize(true) on the RecyclerView, to tell it that its size should not be changing based upon the contents of the adapter. This knowledge can help RecyclerView operate more efficiently.

The RecyclerViewActivity also has getAdapter() and setAdapter() analogues for their ListActivity counterparts. We will explore the differences in the adapter classes later in this section. We also have a setLayoutManager() convenience method, that just calls setLayoutManager() on the underlying RecyclerView — we will see what a layout manager is in the context of RecyclerView in the next section.

There are other features of ListActivity that are not mirrored here in RecyclerViewActivity, just to keep RecyclerViewActivity short. Notably, ListActivity supports either inflating a custom layout that contains the ListView or creating its own. RecyclerViewActivity does not support this, though it could with some minor extensions.

The LayoutManager

The “real” activity of the project is MainActivity, which consists of a single method: onCreate()

  @Override
  public void onCreate(Bundle icicle) {
    super.onCreate(icicle);

    setLayoutManager(new LinearLayoutManager(this));
    setAdapter(new IconicAdapter());
  }

(from RecyclerView/SimpleList/app/src/main/java/com/commonsware/android/recyclerview/simplelist/MainActivity.java)

After chaining to the superclass, the first thing we do is call setLayoutManager(), which will associate a RecyclerView.LayoutManager with our RecyclerView. Specifically, we are using a LinearLayoutManager.

ListView has the notion of a vertically-scrolling list of rows “baked into” its implementation. Similarly, GridView has the notion of a two-dimensional vertically-scrolling grid “baked into” its implementation. RecyclerView, on the other hand, knows absolutely nothing about how to lay out its children. That work is delegated to a RecyclerView.LayoutManager, so that different approaches can be plugged in as needed.

There are three concrete subclasses of the abstract RecyclerView.LayoutManager base class that ship with recyclerview-v7:

In addition, it is eminently possible to create your own RecyclerView.LayoutManager, or use ones from third-party libraries.

In this example, though, we stick with a simple LinearLayoutManager, as we are attempting to replicate the functionality of a ListView.

The Adapter

Our onCreate() method also calls setAdapter(), to associate an RecyclerView.Adapter with our RecyclerView (specifically, a revised version of our IconicAdapter from the original Selection/Dynamic sample app). As with the AdapterView family, RecyclerView uses an adapter to help convert our model data into visual representations. However, the implementation of a RecyclerView.Adapter is substantially different from a classic ListAdapter for use with ListView or GridView.

Reminiscent of ArrayAdapter, a RecyclerView.Adapter uses generics, and we declare what sort of stuff we are adapting. However, ArrayAdapter uses the generic to describe the model data. RecyclerView.Adapter instead uses the generic to identify a ViewHolder that will be responsible for doing the work to actually tie model data to row widgets:

  class IconicAdapter extends RecyclerView.Adapter<RowHolder> {
    @Override
    public RowHolder onCreateViewHolder(ViewGroup parent, int viewType) {
      return(new RowHolder(getLayoutInflater()
                            .inflate(R.layout.row, parent, false)));
    }

    @Override
    public void onBindViewHolder(RowHolder holder, int position) {
      holder.bindModel(items[position]);
    }

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

(from RecyclerView/SimpleList/app/src/main/java/com/commonsware/android/recyclerview/simplelist/MainActivity.java)

In our case, IconicAdapter is using a RowHolder class that we will examine in the next section.

A RecyclerView.Adapter has three abstract methods that need to be implemented.

One is getItemCount(), which fills the same role as does getCount() with a ListAdapter, indicating how many items there will be in the RecyclerView. In the case of IconicAdapter, this is based on the length of the items static array of String objects, same as it was with IconicAdapter in the Selection/Dynamic sample app:

  private static final String[] items={"lorem", "ipsum", "dolor",
          "sit", "amet",
          "consectetuer", "adipiscing", "elit", "morbi", "vel",
          "ligula", "vitae", "arcu", "aliquet", "mollis",
          "etiam", "vel", "erat", "placerat", "ante",
          "porttitor", "sodales", "pellentesque", "augue", "purus"};

(from RecyclerView/SimpleList/app/src/main/java/com/commonsware/android/recyclerview/simplelist/MainActivity.java)

The other two methods are onCreateViewHolder() and onBindViewHolder(). These are a bit reminiscent of the newView() and bindView() methods that are used by a CursorAdapter. However, rather than working directly with views, onCreateViewHolder() and onBindViewHolder() work with ViewHolder objects, as a formalization of the view holder pattern seen originally in the chapter on selection widgets.

onCreateViewHolder(), as the name suggests, needs to create, configure, and return a ViewHolder for a particular row of our list. It is passed two parameters:

The IconicAdapter implementation inflates our row view (R.layout.row) and passes it to the RowHolder constructor, returning the resulting RowHolder.

onBindViewHolder() is responsible for updating a ViewHolder based upon the model data for a certain position. IconicAdapter handles this by passing the model into a private bindModel() method implemented on RowHolder.

There are many other methods you could override on RecyclerView.Adapter, and we will see a few of those later in this chapter. But, for a simple list, these three will suffice.

The ViewHolder

The RecyclerView.ViewHolder is responsible for binding data as needed from our model into the widgets for a row in our list:

  static class RowHolder extends RecyclerView.ViewHolder {
    TextView label=null;
    TextView size=null;
    ImageView icon=null;
    String template=null;

    RowHolder(View row) {
      super(row);

      label=(TextView)row.findViewById(R.id.label);
      size=(TextView)row.findViewById(R.id.size);
      icon=(ImageView)row.findViewById(R.id.icon);

      template=size.getContext().getString(R.string.size_template);
    }

    void bindModel(String item) {
      label.setText(item);
      size.setText(String.format(template, item.length()));

      if (item.length()>4) {
        icon.setImageResource(R.drawable.delete);
      }
      else {
        icon.setImageResource(R.drawable.ok);
      }
    }
  }

(from RecyclerView/SimpleList/app/src/main/java/com/commonsware/android/recyclerview/simplelist/MainActivity.java)

However, other than needing to use the base class of RecyclerView.ViewHolder, there is no other particular protocol that is mandated between the adapter and the view holder. You can invent your own API. Here, we use the RowHolder constructor to pass in the row View, where the constructor retrieves the individual widgets and sets up our string resource template. Then, a private bindModel() method takes our model object (a String) and binds it to the row’s widgets, applying our business rules along the way.

The Results

As the project name suggests, this gives us a simple list:

SimpleList RecyclerView Demo
Figure 544: SimpleList RecyclerView Demo

As with ListView, RecyclerView (along with the RecyclerView.LayoutManager) handles the vertical scrolling through our available rows.

What’s Missing?

However, we are lacking two things that we had in the Selection/Dynamic edition of this sample that used a ListView.

First, there are no dividers between the rows. That may not be a huge issue for this particular row layout, but other layouts may need more assistance in visually separating one row from the next. We will explore ways of accomplishing this in the next section.

Second, we are missing click events. The user can tap on rows as much as she wants. Not only will the user not get any visual feedback from those taps, but we have no setOnItemClickListener() to find out about those taps. We will explore how to fill in this gap later in the chapter.

RecyclerView also lacks a variety of other things that we could get from a ListView, that we happen to not be using in this sample, such as:

We will explore some of those and how to address them in this chapter.

Divider Options

There are two main approaches for visually separating items in a RecyclerView:

  1. Ensure that this is handled via the layout itself, such as using a CardView
  2. Use a RecyclerView.ItemDecoration to apply a common divider between items

Both of these techniques will be covered in this chapter.

CardView

Cards are a popular visual metaphor in mobile development. Dividing content collections (or aspects of a larger piece of content) into cards makes it clearer how you can reorganize that content to fit various screen sizes and orientations. In some cases, you might have a single column of cards, while in other cases, you have cards arranged more laterally.

In 2014, Google released cardview-v7, another library in the Android Support package, that offers a CardView. CardView is a simple subclass of FrameLayout, designed to provide a card UI, consisting of a rounded rectangle and a drop shadow. In particular, CardView will use Android 5.0’s default drop shadows based on widget elevation, while offering emulated drop shadows on earlier Android releases. This way, you can get a reasonably consistent look going back to API Level 7.

To use this, you will have to add the cardview-v7 library to your app project. Android Studio users can just add a dependency on the cardview-v7 artifact in the Android Support repository, as seen in the RecyclerView/CardViewList sample project:

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

(from RecyclerView/CardViewList/app/build.gradle)

Then, you can wrap your row layout in a CardView (or, more accurately, in an android.support.v7.widget.CardView):

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:cardview="http://schemas.android.com/apk/res-auto"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:layout_margin="4dp"
  cardview:cardCornerRadius="4dp">

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

    <ImageView
      android:id="@+id/icon"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_gravity="center_vertical"
      android:padding="2dip"
      android:src="@drawable/ok"
      android:contentDescription="@string/icon"/>

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

      <TextView
        android:id="@+id/label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="25sp"
        android:textStyle="bold"/>

      <TextView
        android:id="@+id/size"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="15sp"/>
    </LinearLayout>

  </LinearLayout>
</android.support.v7.widget.CardView>
(from RecyclerView/CardViewList/app/src/main/res/layout/row.xml)

With no other code changes from the original RecyclerView/SimpleList sample, we get this:

CardViewList RecyclerView Demo
Figure 545: CardViewList RecyclerView Demo

Note that drop shadows from CardView may not show up on Android 5.0+ emulators, particularly if you have Host GPU mode disabled in the emulator AVD. The CardView itself will work fine, just without the drop shadow effect.

Manual

A CardView may not be an appropriate visual approach for your list. Perhaps you want a regular divider, like we had with ListView. While that is possible, it is not especially straightforward.

RecyclerView considers things like dividers to be “item decorations”. There is a RecyclerView.ItemDecoration abstract class that you can extend to handle item decoration, and you can attach such a decoration to a RecyclerView via addItemDecoration(). As the name suggests, you can have more than one decorator if needed.

However, Google did not bother to provide any concrete implementation of such a decoration.

A few enterprising developers experimented with this, leading to solutions like this one, published as a GitHub gist. The RecyclerView/ManualDividerList sample project demonstrates the use of such a decoration.

First, we will need a drawable resource for the divider itself:

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
       android:shape="rectangle">

  <size
    android:width="1dp"
    android:height="1dp" />

  <solid android:color="@color/divider" />

</shape>
(from RecyclerView/ManualDividerList/app/src/main/res/drawable/item_divider.xml)

This is a ShapeDrawable, as is covered in the chapter on drawables. The big thing is the solid fill, here pointing to a color resource for the color to use for that fill:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <color name="divider">#ffaaaaaa</color>
</resources>
(from RecyclerView/ManualDividerList/app/src/main/res/values/colors.xml)

The ShapeDrawable is given a size of 1dp square. In reality, it will be resized on the fly by the decorator to fill the width of the RecyclerView.

Note that there is nothing especially magic about using this particular drawable. You could have a gradient fill to have the divider taper off towards the ends and be solid in the middle. Or, you could use a nine-patch PNG file, a VectorDrawable on Android 5.0+, or anything else that will resize well.

Next, we need a RecyclerView.ItemDecoration implementation, such as the sample project’s HorizontalDividerItemDecoration:

package com.commonsware.android.recyclerview.manualdivider;

import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.RecyclerView;
import android.view.View;

// inspired by https://gist.github.com/polbins/e37206fbc444207c0e92

public class HorizontalDividerItemDecoration extends RecyclerView.ItemDecoration {
  private Drawable divider;

  public HorizontalDividerItemDecoration(Drawable divider) {
    this.divider=divider.mutate();
  }

  @Override
  public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
    int left=parent.getPaddingLeft();
    int right=parent.getWidth()-parent.getPaddingRight();

    int childCount=parent.getChildCount();

    for (int i=0; i<childCount-1; i++) {
      View child=parent.getChildAt(i);
      RecyclerView.LayoutParams params=
          (RecyclerView.LayoutParams)child.getLayoutParams();

      int top=child.getBottom()+params.bottomMargin;
      int bottom=top+divider.getIntrinsicHeight();

      divider.setBounds(left, top, right, bottom);
      divider.draw(c);
    }
  }
}
(from RecyclerView/ManualDividerList/app/src/main/java/com/commonsware/android/recyclerview/manualdivider/HorizontalDividerItemDecoration.java)

This class takes the Drawable that is the divider as input, so it can be used for different dividers as needed. HorizontalDividerItemDecoration calls mutate() on the Drawable to get a Drawable that can be changed independently of any original instance of the Drawable. This is important when using Drawable resources, as the Drawable instances get reused for other references to the same resource, so changing the core Drawable itself (e.g., via a setBounds() call) is unsafe.

The main logic of HorizontalDividerItemDecoration resides in the onDrawOver() method. This will be called to let us draw over top of the items in the RecyclerView. Here we:

Using that bit of magic, then, is merely a matter of attaching our HorizontalDividerItemDecoration to our RecyclerView, done here in onCreate() of MainActivity:

  @Override
  public void onCreate(Bundle icicle) {
    super.onCreate(icicle);

    setLayoutManager(new LinearLayoutManager(this));

    Drawable divider=getResources().getDrawable(R.drawable.item_divider);

    getRecyclerView().addItemDecoration(new HorizontalDividerItemDecoration(divider));
    setAdapter(new IconicAdapter());
  }

(from RecyclerView/ManualDividerList/app/src/main/java/com/commonsware/android/recyclerview/manualdivider/MainActivity.java)

The rest of the sample project is a clone of the original SimpleList sample project from the beginning of this chapter.

The result is that we have a divider drawn between the children:

ManualDividerList RecyclerView Demo
Figure 546: ManualDividerList RecyclerView Demo

If the idea of having to do all of this yourself irritates you, there are third-party libraries that offer item decorations that you can use “out of the box”. We will examine one such library later in this chapter.

Handling Click Events

However, having nice dividers does not address the larger problem: responding to input.

The RecyclerView vision, overall, is that RecyclerView itself has nothing much to do with input, other than scrolling. Anything having to do with users clicking things and triggering some sort of response is the responsibility of the views inside the RecyclerView, such as the rows in a list-style RecyclerView.

This has its benefits. Clickable widgets, like a RatingBar, in a ListView row had long been in conflict with click events on rows themselves. Getting rows that can be clicked, with row contents that can also be clicked, gets a bit tricky at times. With RecyclerView, you are in more explicit control over how this sort of thing gets handled… because you are the one setting up all of the on-click handling logic.

Of course, that does not help the users much. Users do not care what bit of code is responsible for input. Users simply want to provide the input. If you present them with a vertically-scrolling list-style UI, they will attempt to click on rows in the list and will expect some sort of outcome.

The RecyclerView approach, though, means that you are largely on your own for handling that input. This requires yet more code that, in an ideal world, would be offered as an “out of the box” option by RecyclerView.

Responding to Clicks

At its core, responding to clicks is a matter of setting an OnClickListener on the appropriate Views.

So, for example, the RecyclerView/CardClickList sample project is a clone of the CardViewList sample, where we call setOnClickListener() on the row View in the RecyclerView.ViewHolder, now renamed RowController:

package com.commonsware.android.recyclerview.cardclicklist;

import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

class RowController extends RecyclerView.ViewHolder
    implements View.OnClickListener {
  TextView label=null;
  TextView size=null;
  ImageView icon=null;
  String template=null;

  RowController(View row) {
    super(row);

    label=(TextView)row.findViewById(R.id.label);
    size=(TextView)row.findViewById(R.id.size);
    icon=(ImageView)row.findViewById(R.id.icon);

    template=size.getContext().getString(R.string.size_template);

    row.setOnClickListener(this);
  }

  @Override
  public void onClick(View v) {
    Toast.makeText(v.getContext(),
        String.format("Clicked on position %d", getAdapterPosition()),
        Toast.LENGTH_SHORT).show();
  }

  void bindModel(String item) {
    label.setText(item);
    size.setText(String.format(template, item.length()));

    if (item.length()>4) {
      icon.setImageResource(R.drawable.delete);
    }
    else {
      icon.setImageResource(R.drawable.ok);
    }
  }
}

(from RecyclerView/CardClickList/app/src/main/java/com/commonsware/android/recyclerview/cardclicklist/RowController.java)

In this sample, all the onClick() method does is show a Toast. However, you could:

In this case, since none of the widgets in the row are interactive and might consume click events themselves, the user can tap anywhere on the row, and the Toast will appear. If you have more complex scenarios — such as a checklist where you have a CheckBox in the rows — you can decide for yourself how to handle click events on different parts of the row. We will see checklists in action later in this chapter.

Visual Impact of Clicks

However, if you run the CardClickList sample, you will notice one major remaining flaw: there is no visual feedback to the user about the click event. Yes, the Toast appears, but users are used to seeing some sort of transient state change in the row itself on a click, such as a flash of color. Once again, we have the ability to control this as we see fit… by having the responsibility to make it happen at all.

There are a few approaches to this problem, such as the ones outlined in this section.

Option #1: Translucent Selector on Top

An approach that Mark Allison suggested in his Styling Android blog mimics the drawSelectorOnTop approach available to ListView. Using something like a FrameLayout, you layer a translucent selector atop the rows, where the selector implements the click feedback.

The RecyclerView/CardRippleList sample project is a clone of CardClickList that takes Mr. Allison’s approach. The revised row.xml takes advantage of the fact that CardView is a subclass of FrameLayout, so it layers a plain View atop the LinearLayout that is the core content of the row:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:cardview="http://schemas.android.com/apk/res-auto"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:layout_margin="4dp"
  cardview:cardCornerRadius="4dp">

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

    <ImageView
      android:id="@+id/icon"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_gravity="center_vertical"
      android:padding="2dip"
      android:src="@drawable/ok"
      android:contentDescription="@string/icon"/>

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

      <TextView
        android:id="@+id/label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="25sp"
        android:textStyle="bold"/>

      <TextView
        android:id="@+id/size"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="15sp"/>
    </LinearLayout>

  </LinearLayout>

  <View
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="?android:attr/selectableItemBackground" />

</android.support.v7.widget.CardView>
(from RecyclerView/CardRippleList/app/src/main/res/layout/row.xml)

The background of that View is the selectableItemBackground from the current theme. On apps using Theme, this will give you an orange flash. On apps using Theme.Holo, this will give you a blue flash. On apps using Theme.Material, this will give you a ripple animation. And, of course, you can supply your own override value for selectableItemBackground to use your own StateListDrawable instead.

The downsize of this approach is that the View is higher on the Z axis than is the rest of the row content. In this case, since the rest of the row content is non-interactive, this is not a problem. However, if we elect to put interactive widgets in the rows — such as CheckBox widgets to implement a checklist — now our View will prevent the user from interacting with those widgets.

Option #2: Background Selector

Another approach would be to apply the selectableItemBackground to our existing row content, rather than to some separate selector widget that overlays the row content. This is the approach taken in the RecyclerView/CardRippleList2 sample project. Here, the selectableItemBackground is applied to the LinearLayout inside of the CardView:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:cardview="http://schemas.android.com/apk/res-auto"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:layout_margin="4dp"
  cardview:cardCornerRadius="4dp">

  <LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:background="?android:attr/selectableItemBackground">

    <ImageView
      android:id="@+id/icon"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_gravity="center_vertical"
      android:padding="2dip"
      android:src="@drawable/ok"
      android:contentDescription="@string/icon"/>

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

      <TextView
        android:id="@+id/label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="25sp"
        android:textStyle="bold"/>

      <TextView
        android:id="@+id/size"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="15sp"/>
    </LinearLayout>

  </LinearLayout>

</android.support.v7.widget.CardView>
(from RecyclerView/CardRippleList2/app/src/main/res/layout/row.xml)

For non-interactive widgets, like our TextViews and ImageView, touch events will get propagated to the LinearLayout, which will trigger the changes in the state of the StateListDrawable that is the LinearLayout background. Yet, if we change the rows to have interactive widgets, those widgets will still be able to process their own touch events, as we will see later in this chapter.

However, particularly for this sample app, the visual effect is largely the same as with CardRippleList: the user will get click feedback based upon the selectableItemBackground in use given the activity’s theme.

Option #3: Controlled Ripple Emanation Point

There is one problem with both click event implementations, though: the ripples on Android 5.0+ start in the center of each row.

According to the Material Design rules, the ripples should start where the touch event occurs, so they seem to flow outward from the finger.

To do this, you need to use the setHotspot() method, added to Drawable in API Level 21. setHotspot() provides to the drawable a “hot spot”, and RippleDrawable apparently uses this as the emanation point for the ripple effect. setHotspot() takes a pair of float values, presumably with an eye towards using setHotspot() inside of an OnTouchListener, as the MotionEvent reports X/Y positions of the touch event with float values.

The RecyclerView/CardRippleList3 sample project is a clone of CardRipple2 that adds this feature.

The row layout is the same as before. However, in RowController, when setting up the row, we register an OnTouchListener, to find out the low-level MotionEvent of when the user touches our row:

  RowController(View row) {
    super(row);

    label=(TextView)row.findViewById(R.id.label);
    size=(TextView)row.findViewById(R.id.size);
    icon=(ImageView)row.findViewById(R.id.icon);

    template=size.getContext().getString(R.string.size_template);

    row.setOnClickListener(this);

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
      row.setOnTouchListener(new View.OnTouchListener() {
        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        @Override
        public boolean onTouch(View v, MotionEvent event) {
          v
            .findViewById(R.id.row_content)
            .getBackground()
            .setHotspot(event.getX(), event.getY());

          return(false);
        }
      });
    }
  }

(from RecyclerView/CardRippleList3/app/src/main/java/com/commonsware/android/recyclerview/cardripplelist3/RowController.java)

We only bother registering this listener on API Level 21+, as there is no setHotspot() method on prior versions of Android and therefore no need for the listener. However, if we are on an Android 5.0+ device, we intercept the touch event, pass it along to setHotspot() on the background Drawable, and return false to ensure that regular touch event processing proceeds.

The effect is subtle and may be difficult for you to discern. But, if you look at the touch events in slow motion (e.g., screen record a session, then examine the resulting video frame-by-frame), you will see that the ripple effect appears to emanate from the touch point, rather than from the row’s center as before. And, since this logic is only used on API Level 21+, older devices are unaffected.

What About Cursors?

So far, our model data has been a simple static array. Often times, though, we need to be working with model data culled from a database or ContentProvider. It may be that, for other reasons, we want to convert the Cursor we get back from queries into an array of ordinary Java objects. However, there is nothing stopping us from using a Cursor more directly as the model for a RecyclerView.

The RecyclerView.Adapter is responsible for teaching the RecyclerView.ViewHolder the model data to bind against. The RecyclerView.Adapter base class is oblivious to how that model data is organized: array, ArrayList, Cursor, JSONArray, etc. And the actual bind-the-data logic for the ReyclerView.ViewHolder is our responsibility — again, the base class is oblivious to where the data is coming from. Hence, we can create our own protocol for passing the model data for the needed position from the RecyclerView.Adapter to the RecyclerView.ViewHolder. If we want to use a Cursor as the vehicle for doing this, we are welcome to do so.

This is illustrated in the RecyclerView/VideoList sample project, which is a clone of the VideoList project introduced in the chapter on the MediaStore ContentProvider. In the original sample, the list was a ListView; in this sample, the list is a RecyclerView.

The core “plumbing” of the app is akin to the previous RecyclerView samples, such as using RecyclerViewActivity for handling getting the RecyclerView on the screen. However, our row layout is now based on the original VideoList row:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:orientation="horizontal"
  android:padding="8dp"
  android:background="?android:attr/selectableItemBackground">

  <ImageView
    android:id="@+id/thumbnail"
    android:layout_width="64dp"
    android:layout_height="64dp"
    android:contentDescription="@string/thumbnail"/>

  <TextView
    android:id="@android:id/text1"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="8dp"
    android:layout_gravity="center_vertical"
    android:textSize="24sp"/>

</LinearLayout>
(from RecyclerView/VideoList/app/src/main/res/layout/row.xml)

However, as we will now be accessing media, we need the READ_EXTERNAL_STORAGE permission, so we request that in the manifest:

<?xml version="1.0"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.commonsware.android.recyclerview.videolist"
          android:versionCode="1"
          android:versionName="1.0">

  <supports-screens
    android:anyDensity="true"
    android:largeScreens="true"
    android:normalScreens="true"
    android:smallScreens="true"/>

  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

  <application
    android:icon="@drawable/ic_launcher"
    android:theme="@style/Theme.Apptheme">
    <activity
      android:name=".MainActivity"
      android:label="@string/app_name">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>

        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>
  </application>

</manifest>

(from RecyclerView/VideoList/app/src/main/AndroidManifest.xml)

And our app/build.gradle file gives us a targetSdkVersion of 23, requiring us to deal with runtime permissions on Android 6.0+:

apply plugin: 'com.android.application'

dependencies {
    compile 'com.android.support:recyclerview-v7:23.4.0'
    compile 'com.squareup.picasso:picasso:2.5.2'
}

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.2"

    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 23
    }
}

(from RecyclerView/VideoList/app/build.gradle)

onCreate() sets up the empty RecyclerView with a LinearLayoutManager and a VideoAdapter (that we will examine shortly). However, we also confirm whether we have READ_EXTERNAL_STORAGE already — if yes, we call loadVideos() to get the videos. If we do not have permission, and we are not in the middle of requesting permission, we ask for permission using requestPermissions():

  private static final String STATE_IN_PERMISSION="inPermission";
  private static final int REQUEST_PERMS=137;
  private boolean isInPermission=false;

  @Override
  public void onCreate(Bundle icicle) {
    super.onCreate(icicle);

    setLayoutManager(new LinearLayoutManager(this));
    setAdapter(new VideoAdapter());

    if (icicle!=null) {
      isInPermission=
        icicle.getBoolean(STATE_IN_PERMISSION, false);
    }

    if (hasFilesPermission()) {
      loadVideos();
    }
    else if (!isInPermission) {
      isInPermission=true;

      ActivityCompat.requestPermissions(this,
        new String[] {Manifest.permission.READ_EXTERNAL_STORAGE},
        REQUEST_PERMS);
    }
  }

(from RecyclerView/VideoList/app/src/main/java/com/commonsware/android/recyclerview/videolist/MainActivity.java)

hasFilesPermission() just uses checkSelfPermission() to see whether we can read external storage:

  private boolean hasFilesPermission() {
    return(ContextCompat.checkSelfPermission(this,
      Manifest.permission.READ_EXTERNAL_STORAGE)==
      PackageManager.PERMISSION_GRANTED);
  }

(from RecyclerView/VideoList/app/src/main/java/com/commonsware/android/recyclerview/videolist/MainActivity.java)

We then call loadVideos() once we have permission, plus keep track of whether or not we are in the process of requesting permissions (so we do not raise the permission dialog again if we undergo a configuration change while the permission dialog is already on-screen):

  @Override
  protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);

    outState.putBoolean(STATE_IN_PERMISSION, isInPermission);
  }

  @Override
  public void onRequestPermissionsResult(int requestCode,
                                         String[] permissions,
                                         int[] grantResults) {
    isInPermission=false;

    if (requestCode==REQUEST_PERMS) {
      if (hasFilesPermission()) {
        loadVideos();
      }
      else {
        finish(); // denied permission, so we're done
      }
    }
  }

(from RecyclerView/VideoList/app/src/main/java/com/commonsware/android/recyclerview/videolist/MainActivity.java)

loadVideos() just calls initLoader() to request that we load the videos from the MediaStore:

  private void loadVideos() {
    getLoaderManager().initLoader(0, null, this);
  }

(from RecyclerView/VideoList/app/src/main/java/com/commonsware/android/recyclerview/videolist/MainActivity.java)

The CursorLoader logic, for getting details about videos from the MediaStore, is pretty much the same as before, other than providing the Cursor to the VideoAdapter when it is ready:

  @Override
  public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
    return(new CursorLoader(this,
                            MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
                            null, null, null,
                            MediaStore.Video.Media.TITLE));
  }

  @Override
  public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
    ((VideoAdapter)getAdapter()).setVideos(c);
  }

  @Override
  public void onLoaderReset(Loader<Cursor> loader) {
    ((VideoAdapter)getAdapter()).setVideos(null);
  }

(from RecyclerView/VideoList/app/src/main/java/com/commonsware/android/recyclerview/videolist/MainActivity.java)

VideoAdapter is another subclass of RecyclerView.Adapter, this time with smarts for dealing with a Cursor as the source of model data:

  class VideoAdapter extends RecyclerView.Adapter<RowController> {
    Cursor videos=null;

    @Override
    public RowController onCreateViewHolder(ViewGroup parent, int viewType) {
      return(new RowController(getLayoutInflater()
                                 .inflate(R.layout.row, parent, false)));
    }

    void setVideos(Cursor videos) {
      this.videos=videos;
      notifyDataSetChanged();
    }

    @Override
    public void onBindViewHolder(RowController holder, int position) {
      videos.moveToPosition(position);
      holder.bindModel(videos);
    }

    @Override
    public int getItemCount() {
      if (videos==null) {
        return(0);
      }

      return(videos.getCount());
    }
  }

(from RecyclerView/VideoList/app/src/main/java/com/commonsware/android/recyclerview/videolist/MainActivity.java)

Specifically:

Also note that we have a setVideos() method that is used to associate our Cursor of video information with the adapter. This also triggers a call to notifyDataSetChanged(), to ensure that the RecyclerView knows that our model has changed and it should re-render its contents.

The RowController constructor retrieves the necessary widgets from the row and setting up an OnClickListener:

  RowController(View row) {
    super(row);

    title=(TextView)row.findViewById(android.R.id.text1);
    thumbnail=(ImageView)row.findViewById(R.id.thumbnail);

    row.setOnClickListener(this);
  }

(from RecyclerView/VideoList/app/src/main/java/com/commonsware/android/recyclerview/videolist/RowController.java)

The bindModel() method invoked by onBindViewHolder() on VideoAdapter uses the same basic logic from the original VideoList sample to populate the row widgets, plus holds onto the Uri and MIME type of the video in data members for the current row:

  void bindModel(Cursor row) {
    title.setText(row.getString(
      row.getColumnIndex(MediaStore.Video.Media.TITLE)));

    videoUri=
        ContentUris.withAppendedId(
          MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
          row.getInt(row.getColumnIndex(MediaStore.Video.Media._ID)));

    Picasso.with(thumbnail.getContext())
      .load(videoUri.toString())
      .fit().centerCrop()
      .placeholder(R.drawable.ic_media_video_poster)
      .into(thumbnail);

    int uriColumn=row.getColumnIndex(MediaStore.Video.Media.DATA);
    int mimeTypeColumn=
        row.getColumnIndex(MediaStore.Video.Media.MIME_TYPE);

    videoMimeType=row.getString(mimeTypeColumn);
  }

(from RecyclerView/VideoList/app/src/main/java/com/commonsware/android/recyclerview/videolist/RowController.java)

The onClick() method uses those saved Uri and MIME type values for starting up the activity to play the selected video:

  @Override
  public void onClick(View v) {
    Intent i=new Intent(Intent.ACTION_VIEW);

    i.setDataAndType(videoUri, videoMimeType);
    title.getContext().startActivity(i);
  }

(from RecyclerView/VideoList/app/src/main/java/com/commonsware/android/recyclerview/videolist/RowController.java)

Other than the lack of dividers, the UI is very similar to the original VideoList.

This sample app is used as the basis for many other samples in this book, such as the drag-and-drop examples.

Grids

So far, we have focused on one visual representation of our collection of model data: a vertically-scrolling list. In the AdapterView family, a given AdapterView subclass has a specific visual representation (ListView for a vertically-scrolling list, GridView for a two-dimensional grid, etc.). With RecyclerView, the choice of layout manager determines most of the visual representation, and so switching from a list to a grid can be as simple as a single-line change to our code.

The key, though, is the word can in the previous sentence. Depending upon what you want to do, a grid-styled RecyclerView can be more complicated, simply because you now have two dimensions’ worth of power and configuration to play with.

A Simple Grid

Making a RecyclerView use a grid is a matter of swapping out LinearLayoutManager for GridLayoutManager. In the RecyclerView/Grid sample project, you will see a clone of the CardRippleList3 sample app, where we are now using GridLayoutManager in onCreate() of MainActivity:

  @Override
  public void onCreate(Bundle icicle) {
    super.onCreate(icicle);

    setLayoutManager(new GridLayoutManager(this, 2));
    setAdapter(new IconicAdapter());
  }

(from RecyclerView/Grid/app/src/main/java/com/commonsware/android/recyclerview/grid/MainActivity.java)

GridLayoutManager takes a number of “spans”, as well as a Context, as constructor parameters. In the simple case, as is with this app, “spans” will equate to “columns”: each item returned by the RecyclerView.Adapter will go into a single-row, single-span cell.

In our case, we requested two spans, and so our result resides in two columns:

Grid RecyclerView Demo
Figure 547: Grid RecyclerView Demo

In this case, this is a “true” grid, with rows and columns of cells. Hence, the height of a row is determined by the tallest cell in that row. The “amet” cell in the left column of the third row is taller than required because of the word-wrap of the “consectetuer” cell in the right column of the same row, for example. Later in this chapter, we will examine yet another option, StaggeredGridLayoutManager, where cells do not necessarily line up neatly in rows.

Choosing the Number of Columns

If we rotate the screen for the above sample, you will see that the cells fit a bit better, since they are really repurposed list-style rows:

Grid RecyclerView Demo, Landscape
Figure 548: Grid RecyclerView Demo, Landscape

However, some apps may have smaller per-cell content. Plus, we have tablets to consider, and perhaps even televisions. It may be that you want to determine how many spans to use based on screen size and orientation.

One approach for doing that would be to use integer resources. You could have a res/values/ints.xml file with <integer> elements, giving the integer a name (name attribute) and value (text of the <integer> node). You could also have res/values-w600dp/ints.xml or other variations of the resource, where you provide different values to use for different screen sizes. Then, at runtime, call getResources().getInteger() to retrieve the correct value of the resource to use for the current device, and use that in your GridLayoutManager constructor. Now, you are in control over how many columns there are, by controlling how many spans are supplied to the constructor.

Another approach, suggested by Chiu-Ki Chan, is to create a subclass of RecyclerView, on which you provide a custom attribute for a desired approximate column width. Then, in your subclass’ onMeasure() method, you can calculate the number of spans to use to give you the desired column width.

Of course, another way to take advantage of screen space is to grow the cells. By default, they will grow evenly, as each cell takes up one span, and the spans are evenly sizes. However, you can change that behavior, by attaching a GridLayoutManager.SpanSizeLookup to the GridLayoutManager. The GridLayoutManager.SpanSizeLookup is responsible for indicating, for a given item’s position, how many spans it should take up in the grid. We will examine how this works later in this chapter.

Varying the Items

So far, all of the items in the RecyclerView have had the same basic structure, just with varying content in the widgets in those items. But, it is entirely possible that we will want to have some items be more substantively different, based on different layouts. ListView and kin handle this via getViewTypeCount() and getItemViewType() in the ListAdapter. RecyclerView and RecyclerView.Adapter offer a similar mechanism, including their own variant of the getItemViewType() method. In this section, we will examine how this works, both with lists and grids.

A List with Headers

There are many cases where we want to have a list with some sort of section headers. The look of the headers usually is substantially different than the look of the rest of the rows, and therefore the best way to handle this is to teach the adapter about multiple row types.

This can be seen in the RecyclerView/HeaderList sample project. This is a clone of a similar project for ListView, where we want to put the 25 Latin words into 5 groups of 5 words each, with each group getting its own header.

Hence, our model data is now a two-dimensional String array:

  private static final String[][] items= {
      { "lorem", "ipsum", "dolor", "sit", "amet" },
      { "consectetuer", "adipiscing", "elit", "morbi", "vel" },
      { "ligula", "vitae", "arcu", "aliquet", "mollis" },
      { "etiam", "vel", "erat", "placerat", "ante" },
      { "porttitor", "sodales", "pellentesque", "augue", "purus" } };

(from RecyclerView/HeaderList/app/src/main/java/com/commonsware/android/recyclerview/headerlist/MainActivity.java)

Our getItemCount() method now needs to take into account the headers, as well as the regular rows. There is one header row per batch of items, and so getItemCount() sums up the sizes of the batches with the extra header rows:

    @Override
    public int getItemCount() {
      int count=0;

      for (String[] batch : items) {
        count+=1 + batch.length;
      }

      return(count);
    }

(from RecyclerView/HeaderList/app/src/main/java/com/commonsware/android/recyclerview/headerlist/MainActivity.java)

In order to teach RecyclerView about our different rows, we need to implement getItemViewType(). Unlike its counterpart on ListAdapter, getItemViewType() can return any int value, so long as it is unique for the row type. In fact, the recommendation is to use dedicated ID resources to ensure that uniqueness.

To that end, we define two ID resources, in a res/values/ids.xml file:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <item type="id" name="header"/>
  <item type="id" name="detail"/>
</resources>
(from RecyclerView/HeaderList/app/src/main/res/values/ids.xml)

Then, getItemViewType() can return R.id.header or R.id.detail to identify the two row types, and specifically which row type corresponds to the supplied position:

    @Override
    public int getItemViewType(int position) {
      if (getItem(position) instanceof Integer) {
        return(R.id.header);
      }

      return(R.id.detail);
    }

    private Object getItem(int position) {
      int offset=position;
      int batchIndex=0;

      for (String[] batch : items) {
        if (offset == 0) {
          return(Integer.valueOf(batchIndex));
        }

        offset--;

        if (offset < batch.length) {
          return(batch[offset]);
        }

        offset-=batch.length;
        batchIndex++;
      }

      throw new IllegalArgumentException("Invalid position: "
          + String.valueOf(position));
    }

(from RecyclerView/HeaderList/app/src/main/java/com/commonsware/android/recyclerview/headerlist/MainActivity.java)

This leverages a copy of the getItem() method from the original ListView version of this sample, which returns an Integer for a header item (identifying which header it is) and a String for detail item (identifying what Latin word to use). Note that getItem() is not part of the RecyclerView.Adapter protocol, but you are certainly welcome to have one if you want it.

In onCreateViewHolder(), we can now start paying attention to the second parameter, which we have been studiously ignoring until now. That value, viewType, will be a value that we returned from getItemViewType(), and it indicates what sort of RecyclerView.ViewHolder we should return. In our case, there are only two possibilities, and so we just inflate the appropriate layout and use a dedicated controller class (HeaderController for headers, RowController for detail):

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
      if (viewType==R.id.detail) {
        return(new RowController(getLayoutInflater()
                     .inflate(R.layout.row, parent, false)));
      }

      return(new HeaderController(getLayoutInflater()
                    .inflate(R.layout.header, parent, false)));
    }

(from RecyclerView/HeaderList/app/src/main/java/com/commonsware/android/recyclerview/headerlist/MainActivity.java)

Similarly, our binding logic in onBindViewHolder() needs to route the right sort of model information to the proper controller:

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
      if (holder instanceof RowController) {
        ((RowController)holder).bindModel((String)getItem(position));
      }
      else {
        ((HeaderController)holder).bindModel((Integer)getItem(position));
      }
    }

(from RecyclerView/HeaderList/app/src/main/java/com/commonsware/android/recyclerview/headerlist/MainActivity.java)

RowController is the same sort of setup as we have had in past examples. HeaderController is too, though it is far simpler, as we have only one widget needing to be updated (a TextView named label) and we do not care about click events:

package com.commonsware.android.recyclerview.headerlist;

import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.TextView;

class HeaderController extends RecyclerView.ViewHolder {
  TextView label=null;
  String template=null;

  HeaderController(View row) {
    super(row);

    label=(TextView)row.findViewById(R.id.label);

    template=label.getContext().getString(R.string.header_template);
  }

  void bindModel(Integer headerIndex) {
    label.setText(String.format(template, headerIndex.intValue()+1));
  }
}

(from RecyclerView/HeaderList/app/src/main/java/com/commonsware/android/recyclerview/headerlist/HeaderController.java)

The results are header rows with one look-and-feel, and detail rows with a different look-and-feel:

HeaderList RecyclerView Demo
Figure 549: HeaderList RecyclerView Demo

A Grid-Style Table

In the discussion of RecyclerView grids, we saw that one way to take advantage of larger screens is to have more cells, in part by having more spans across the screen.

Another way to take advantage of screen space is to grow the cells. By default, they will grow evenly, as each cell takes up one span, and the spans are evenly sizes. However, you can change that behavior, by attaching a GridLayoutManager.SpanSizeLookup to the GridLayoutManager. The GridLayoutManager.SpanSizeLookup is responsible for indicating, for a given item’s position, how many spans it should take up in the grid.

One way of employing a GridLayoutManager.SpanSizeLookup is to make a table. If you want a table, but the user should only be able to select rows, that would be a matter of using a LinearLayoutManager and setting up the rows with “cells” that are of consistent size per row. For example, each row could be a horizontal LinearLayout, where the “column” widths are determined using android:layout_weight. But sometimes you want a table where individual cells can be clicked upon (or selected via a five-way navigation option, like a trackball). In this case, GridLayoutManager.SpanSizeLookup will let you indicate, for a “column” of your output, how many spans the cell should take up. By using a consistent number of spans for each column, you can get the same sort of weighted column width that you might get with LinearLayout-based rows in a LinearLayoutManager-powered RecyclerView.

And that will make a lot more sense (hopefully) when you see an example.

The RecyclerView/VideoTable sample project is a clone of the VideoList sample project from earlier in the chapter, with a few changes:

The two columns that will hold text (title and video duration) will use the following layout:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:orientation="horizontal"
  android:padding="8dp"
  android:background="?android:attr/selectableItemBackground">

  <TextView
    android:id="@android:id/text1"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginLeft="8dp"
    android:layout_gravity="center_vertical"
    android:textSize="24sp"/>
</LinearLayout>
(from RecyclerView/VideoTable/app/src/main/res/layout/label.xml)

The LinearLayout root element may seem superfluous, but we are using it for the selectableItemBackground, to provide a response when the cell is clicked upon.

Similarly, we have a layout dedicated to the thumbnail:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:orientation="horizontal"
  android:padding="8dp"
  android:background="?android:attr/selectableItemBackground">

  <ImageView
    android:id="@+id/thumbnail"
    android:layout_width="96dp"
    android:layout_height="72dp"
    android:contentDescription="@string/thumbnail"/>

</LinearLayout>
(from RecyclerView/VideoTable/app/src/main/res/layout/thumbnail.xml)

onCreate() of MainActivity is largely the same as before. This time, though, we are creating an instance of a ColumnWeightSpanSizeLookup class and using it for two things:

  1. Calling its getTotalSpans() to tell the GridLayoutManager how many spans to use
  2. Using it as a GridLayoutManager.SpanSizeLookup, attaching it to the GridLayoutManager via setSpanSizeLookup():

    ColumnWeightSpanSizeLookup spanSizer=new ColumnWeightSpanSizeLookup(COLUMN_WEIGHTS);
    GridLayoutManager mgr=new GridLayoutManager(this, spanSizer.getTotalSpans());

    mgr.setSpanSizeLookup(spanSizer);
    setLayoutManager(mgr);
    setAdapter(new VideoAdapter());

(from RecyclerView/VideoTable/app/src/main/java/com/commonsware/android/recyclerview/videotable/MainActivity.java)

The latter point means that ColumnWeightSpanSizeLookup is a subclass of the abstract GridLayoutManager.SpanSizeLookup base class. The one method that you need to override in a GridLayoutManager.SpanSizeLookup subclass is getSpanSize(). Given an item’s position, getSpanSize() returns the number of spans that the item’s cell should… um… span.

(we overload the word “span” a lot in Android…)

ColumnWeightSpanSizeLookup handles this via a set of column weights, which it gets as an int array in the constructor. onCreate() referenced a COLUMN_WEIGHTS static data member for the weights:

  private static final int[] COLUMN_WEIGHTS={1, 4, 1};

(from RecyclerView/VideoTable/app/src/main/java/com/commonsware/android/recyclerview/videotable/MainActivity.java)

This int array tells us both how many columns there are and how wide each column should be, in terms of spans.

Converting the position to a column index is a matter of applying the modulo (%) operator, so the implementation of getSpanSize() on ColumnWeightSpanSizeLookup just returns the columnWeights value for the desired column:

package com.commonsware.android.recyclerview.videotable;

import android.support.v7.widget.GridLayoutManager;

class ColumnWeightSpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
  private final int[] columnWeights;

  ColumnWeightSpanSizeLookup(int[] columnWeights) {
    this.columnWeights=columnWeights;
  }

  @Override
  public int getSpanSize(int position) {
    return(columnWeights[position % columnWeights.length]);
  }

  int getTotalSpans() {
    int sum=0;

    for (int weight : columnWeights) {
      sum+=weight;
    }

    return(sum);
  }
}

(from RecyclerView/VideoTable/app/src/main/java/com/commonsware/android/recyclerview/videotable/ColumnWeightSpanSizeLookup.java)

getTotalSpans() is a convenience method, to sum all of the column weights. That is how many spans the GridLayoutManager will use overall, with each column getting its specific number of spans based upon the int array. Note that while we hard-coded the int array values in this case, there is nothing stopping us from using <integer-array> resources to pull these values out of the Java code, and perhaps even vary them by screen size or other configuration variations.

All of that will set up our grid with the correct number of spans and the right number of spans to use per column of the output. The combination will give us the row structure, as each row’s worth of columns uses all of the spans for that row, forcing GridLayoutManager to put subsequent items on the next row.

The rest of the project is focused on having different widgets for those different cells, using getItemViewType() and so on.

The VideoAdapter implementation of getItemViewType() simply returns the position modulo 3, to return a unique value (in this case, 0, 1, or 2):

    @Override
    public int getItemViewType(int position) {
      return(position % 3);
    }

(from RecyclerView/VideoTable/app/src/main/java/com/commonsware/android/recyclerview/videotable/MainActivity.java)

getItemCount() takes into account that there are three cells per video, and so the number of items being managed by this adapter is triple the number of videos:

    @Override
    public int getItemCount() {
      if (videos==null) {
        return(0);
      }

      return(videos.getCount()*3);
    }

(from RecyclerView/VideoTable/app/src/main/java/com/commonsware/android/recyclerview/videotable/MainActivity.java)

The onCreateViewHolder() and onBindViewHolder() methods take into account those three item types, using a VideoThumbnailController or a VideoTextController depending on the item type. Both of those classes will inherit from a BaseVideoController, which defines a bindModel() method that onBindViewHolder() can use:

    @Override
    public BaseVideoController onCreateViewHolder(ViewGroup parent, int viewType) {
      BaseVideoController result=null;

      switch(viewType) {
        case 0:
          result=new VideoThumbnailController(getLayoutInflater()
                                                 .inflate(R.layout.thumbnail,
                                                          parent, false));
          break;

        case 1:
          int cursorColumn=videos.getColumnIndex(MediaStore.Video.VideoColumns.DISPLAY_NAME);

          result=new VideoTextController(getLayoutInflater()
                                            .inflate(R.layout.label,
                                                parent, false),
                                          android.R.id.text1,
                                          cursorColumn);
          break;

        case 2:
          cursorColumn=videos.getColumnIndex(MediaStore.Video.VideoColumns.DURATION);

          result=new VideoTextController(getLayoutInflater()
                                            .inflate(R.layout.label,
                                                parent, false),
                                          android.R.id.text1,
                                          cursorColumn);
          break;
      }

      return(result);
    }

    @Override
    public void onBindViewHolder(BaseVideoController holder, int position) {
      videos.moveToPosition(position/3);
      holder.bindModel(videos);
    }

(from RecyclerView/VideoTable/app/src/main/java/com/commonsware/android/recyclerview/videotable/MainActivity.java)

BaseVideoController handles click events on the cell, along with collecting the Uri and MIME type of the video to use on click events:

package com.commonsware.android.recyclerview.videotable;

import android.content.ContentUris;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;
import android.support.v7.widget.RecyclerView;
import android.view.View;

abstract class BaseVideoController extends RecyclerView.ViewHolder
    implements View.OnClickListener {
  private Uri videoUri=null;
  private String videoMimeType=null;

  BaseVideoController(View cell) {
    super(cell);

    cell.setOnClickListener(this);
  }

  @Override
  public void onClick(View v) {
    Intent i=new Intent(Intent.ACTION_VIEW);

    i.setDataAndType(videoUri, videoMimeType);
    itemView.getContext().startActivity(i);
  }

  void bindModel(Cursor row) {
    int mimeTypeColumn=
        row.getColumnIndex(MediaStore.Video.Media.MIME_TYPE);

    videoUri=ContentUris.withAppendedId(
      MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
      row.getInt(row.getColumnIndex(MediaStore.Video.Media._ID)));
    videoMimeType=row.getString(mimeTypeColumn);
  }
}

(from RecyclerView/VideoTable/app/src/main/java/com/commonsware/android/recyclerview/videotable/BaseVideoController.java)

VideoTextController extends BaseVideoController and handles binding some column from the MediaStore Cursor to a TextView with some ID:

package com.commonsware.android.recyclerview.videotable;

import android.database.Cursor;
import android.view.View;
import android.widget.TextView;

class VideoTextController extends BaseVideoController {
  private TextView label=null;
  private int cursorColumn;

  VideoTextController(View cell, int labelId, int cursorColumn) {
    super(cell);
    this.cursorColumn=cursorColumn;

    label=(TextView)cell.findViewById(labelId);
  }

  @Override
  void bindModel(Cursor row) {
    super.bindModel(row);

    label.setText(row.getString(cursorColumn));
  }
}

(from RecyclerView/VideoTable/app/src/main/java/com/commonsware/android/recyclerview/videotable/VideoTextController.java)

VideoThumbnailController handles using Picasso to get the video thumbnail asynchronously and binding it to an ImageView in the inflated cell View:

package com.commonsware.android.recyclerview.videotable;

import android.content.ContentUris;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;
import android.view.View;
import android.widget.ImageView;
import com.squareup.picasso.Picasso;

class VideoThumbnailController extends BaseVideoController {
  private ImageView thumbnail=null;

  VideoThumbnailController(View cell) {
    super(cell);

    thumbnail=(ImageView)cell.findViewById(R.id.thumbnail);
  }

  @Override
  void bindModel(Cursor row) {
    super.bindModel(row);

    Uri video=
        ContentUris.withAppendedId(
          MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
          row.getInt(row.getColumnIndex(MediaStore.Video.Media._ID)));

    Picasso.with(thumbnail.getContext())
      .load(video.toString())
      .fit().centerCrop()
      .placeholder(R.drawable.ic_media_video_poster)
      .into(thumbnail);
  }
}

(from RecyclerView/VideoTable/app/src/main/java/com/commonsware/android/recyclerview/videotable/VideoThumbnailController.java)

The result is the same information as was in the original VideoList demo, but organized into a table, where each cell is clickable:

VideoTable RecyclerView Demo
Figure 550: VideoTable RecyclerView Demo

The duration is returned by MediaStore in milliseconds, which is not a great choice to present directly to the user. An improved version of this app might use a dedicated RecyclerView.ViewHolder that would convert the millisecond count into a duration measured in hours, minutes, and seconds (e.g., shown as HH:MM:SS to the user).

Also note that the cell sizes are purely driven by their weights, which will not necessarily handle all content in all configurations very well. The chosen weights barely work on a 10" tablet in portrait, for example:

VideoTable RecyclerView Demo, Portrait
Figure 551: VideoTable RecyclerView Demo, Portrait

Mutable Row Contents

So far, all of the items we have used have been display-only. At most, they might respond to click events, along the lines of clicking a ListView row or GridView cell.

But, what about choice modes?

ListView and GridView — by way of their common AbsListView ancestor – have the concept of choice modes, where the user can “check” and “uncheck” items, and the list or grid will keep track of those states.

Well, as with lots of other things involving RecyclerView, RecyclerView does not offer choice modes… though you can implement that yourself. The RecyclerView/ChoiceList sample project turns our list-style RecyclerView into a checklist, with CheckBox widgets in each row, where the RecyclerView.Adapter will keep track of the CheckBox checked states for us.

First, we need to add a CheckBox to the row:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:cardview="http://schemas.android.com/apk/res-auto"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:layout_margin="4dp"
  cardview:cardCornerRadius="4dp">

  <LinearLayout
    android:id="@+id/row_content"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:background="?android:attr/selectableItemBackground">

    <ImageView
      android:id="@+id/icon"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_gravity="center_vertical"
      android:padding="2dip"
      android:src="@drawable/ok"
      android:contentDescription="@string/icon"/>

    <LinearLayout
      android:layout_width="0dip"
      android:layout_height="wrap_content"
      android:layout_weight="1"
      android:orientation="vertical">

      <TextView
        android:id="@+id/label"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="25sp"
        android:textStyle="bold"/>

      <TextView
        android:id="@+id/size"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="15sp"/>
    </LinearLayout>

    <CheckBox
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:id="@+id/cb"
      android:layout_gravity="center_vertical"/>

  </LinearLayout>
</android.support.v7.widget.CardView>
(from RecyclerView/ChoiceList/app/src/main/res/layout/row.xml)

Our IconicAdapter is only slightly different than before:

  class IconicAdapter extends ChoiceCapableAdapter<RowController> {
    IconicAdapter() {
      super(new MultiChoiceMode());
    }

    @Override
    public RowController onCreateViewHolder(ViewGroup parent, int viewType) {
      return(new RowController(this, getLayoutInflater()
                                      .inflate(R.layout.row, parent, false)));
    }

    @Override
    public void onBindViewHolder(RowController holder, int position) {
      holder.bindModel(items[position]);
    }

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

(from RecyclerView/ChoiceList/app/src/main/java/com/commonsware/android/recyclerview/choicelist/MainActivity.java)

ChoiceCapableAdapter is simply a RecyclerView.Adapter that knows how to handle choice modes, as implemented via the ChoiceMode interface:

package com.commonsware.android.recyclerview.choicelist;

import android.os.Bundle;

public interface ChoiceMode {
  void setChecked(int position, boolean isChecked);
  boolean isChecked(int position);
  void onSaveInstanceState(Bundle state);
  void onRestoreInstanceState(Bundle state);
}

(from RecyclerView/ChoiceList/app/src/main/java/com/commonsware/android/recyclerview/choicelist/ChoiceMode.java)

A ChoiceMode is effectively a strategy class, responsible for tracking the checked states, not only for the current ChoiceCapableAdapter instance, but for future ones created as part of a configuration change. It requires four methods:

This project uses a MultiChoiceMode implementation of ChoiceMode:

package com.commonsware.android.recyclerview.choicelist;

import android.os.Bundle;

public class MultiChoiceMode implements ChoiceMode {
  private static final String STATE_CHECK_STATES="checkStates";
  private ParcelableSparseBooleanArray checkStates=new ParcelableSparseBooleanArray();

  @Override
  public void setChecked(int position, boolean isChecked) {
    checkStates.put(position, isChecked);
  }

  @Override
  public boolean isChecked(int position) {
    return(checkStates.get(position, false));
  }

  @Override
  public void onSaveInstanceState(Bundle state) {
    state.putParcelable(STATE_CHECK_STATES, checkStates);
  }

  @Override
  public void onRestoreInstanceState(Bundle state) {
    checkStates=state.getParcelable(STATE_CHECK_STATES);
  }
}

(from RecyclerView/ChoiceList/app/src/main/java/com/commonsware/android/recyclerview/choicelist/MultiChoiceMode.java)

MultiChoiceMode, in turn, is mostly handled by a ParcelableSparseBooleanArray. SparseBooleanArray is a class, supplied in the Android SDK, that is a space-efficient mapping of int values to boolean values, as opposed to using a HashMap and having to convert those primitives to Integer and Boolean objects. However, for inexplicable reasons, SparseBooleanArray was not implemented to be Parcelable, and therefore it cannot be stored in a Bundle. ParcelableSparseBooleanArray is a subclass of SparseBooleanArray that handles the Parcelable aspects:

package com.commonsware.android.recyclerview.choicelist;

import android.os.Parcel;
import android.os.Parcelable;
import android.util.SparseBooleanArray;

public class ParcelableSparseBooleanArray extends SparseBooleanArray
    implements Parcelable {
  public static Parcelable.Creator<ParcelableSparseBooleanArray> CREATOR
    =new Parcelable.Creator<ParcelableSparseBooleanArray>() {
    @Override
    public ParcelableSparseBooleanArray createFromParcel(Parcel source) {
      return(new ParcelableSparseBooleanArray(source));
    }

    @Override
    public ParcelableSparseBooleanArray[] newArray(int size) {
      return(new ParcelableSparseBooleanArray[size]);
    }
  };

  public ParcelableSparseBooleanArray() {
    super();
  }

  private ParcelableSparseBooleanArray(Parcel source) {
    int size=source.readInt();

    for (int i=0; i < size; i++) {
      put(source.readInt(), (Boolean)source.readValue(null));
    }
  }

  @Override
  public int describeContents() {
    return(0);
  }

  @Override
  public void writeToParcel(Parcel dest, int flags) {
    dest.writeInt(size());

    for (int i=0;i<size();i++) {
      dest.writeInt(keyAt(i));
      dest.writeValue(valueAt(i));
    }
  }
}

(from RecyclerView/ChoiceList/app/src/main/java/com/commonsware/android/recyclerview/choicelist/ParcelableSparseBooleanArray.java)

The net effect is that MultiChoiceMode, by means of ParcelableSparseBooleanArray, can track the checked/unchecked states of particular item position values.

ChoiceCapableAdapter, then, is a RecyclerView.ViewHolder that surfaces a ChoiceMode implementation:

package com.commonsware.android.recyclerview.choicelist;

import android.os.Bundle;
import android.support.v7.widget.RecyclerView;

abstract public class
    ChoiceCapableAdapter<T extends RecyclerView.ViewHolder>
    extends RecyclerView.Adapter<T> {
  private final ChoiceMode choiceMode;

  public ChoiceCapableAdapter(ChoiceMode choiceMode) {
    super();
    this.choiceMode=choiceMode;
  }

  void onChecked(int position, boolean isChecked) {
    choiceMode.setChecked(position, isChecked);
  }

  boolean isChecked(int position) {
    return(choiceMode.isChecked(position));
  }

  void onSaveInstanceState(Bundle state) {
    choiceMode.onSaveInstanceState(state);
  }

  void onRestoreInstanceState(Bundle state) {
    choiceMode.onRestoreInstanceState(state);
  }
}

(from RecyclerView/ChoiceList/app/src/main/java/com/commonsware/android/recyclerview/choicelist/ChoiceCapableAdapter.java)

The methods exposed by ChoiceCapableAdapter can then be used by outside parties. Specifically, MainActivity delegates onSaveInstanceState() and onRestoreInstanceState() to ChoiceCapableAdapter, so checked states can span configuration changes and the like. Plus, RowController can hook up on OnCheckedChangedListener and to update ChoiceCapableAdapter based on the state of checkbox changes:

package com.commonsware.android.recyclerview.choicelist;

import android.annotation.TargetApi;
import android.os.Build;
import android.support.v7.widget.RecyclerView;
import android.view.MotionEvent;
import android.view.View;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

class RowController extends RecyclerView.ViewHolder
    implements View.OnClickListener, CompoundButton.OnCheckedChangeListener {
  private ChoiceCapableAdapter adapter;
  private TextView label=null;
  private TextView size=null;
  private ImageView icon=null;
  private String template=null;
  private CheckBox cb=null;

  RowController(ChoiceCapableAdapter adapter, View row) {
    super(row);

    this.adapter=adapter;
    label=(TextView)row.findViewById(R.id.label);
    size=(TextView)row.findViewById(R.id.size);
    icon=(ImageView)row.findViewById(R.id.icon);
    cb=(CheckBox)row.findViewById(R.id.cb);

    template=size.getContext().getString(R.string.size_template);

    row.setOnClickListener(this);

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
      row.setOnTouchListener(new View.OnTouchListener() {
        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        @Override
        public boolean onTouch(View v, MotionEvent event) {
          v
              .findViewById(R.id.row_content)
              .getBackground()
              .setHotspot(event.getX(), event.getY());

          return(false);
        }
      });
    }

    cb.setOnCheckedChangeListener(this);
  }

  @Override
  public void onClick(View v) {
    Toast.makeText(v.getContext(),
        String.format("Clicked on position %d", getAdapterPosition()),
        Toast.LENGTH_SHORT).show();
  }

  @Override
  public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
    adapter.onChecked(getAdapterPosition(), isChecked);
  }

  void bindModel(String item) {
    label.setText(item);
    size.setText(String.format(template, item.length()));

    if (item.length()>4) {
      icon.setImageResource(R.drawable.delete);
    }
    else {
      icon.setImageResource(R.drawable.ok);
    }

    cb.setChecked(adapter.isChecked(getAdapterPosition()));
  }
}

(from RecyclerView/ChoiceList/app/src/main/java/com/commonsware/android/recyclerview/choicelist/RowController.java)

Here, bindModel() updates the CheckBox based upon the ChoiceCapableAdapter isChecked() value for the RecyclerView.ViewHolder position (obtained via getPosition()). And, onCheckedChanged() updates the ChoiceCapableAdapter to keep track of whether this position is checked or unchecked, to handle row recycling, configuration changes, etc.

The result is much as you would expect: a version of our same sort of UI as before, except that if the user clicks the CheckBox, instead of the rest of the row, the CheckBox toggles its checked state, and that state survives row recycling, configuration changes, and so on:

ChoiceList RecyclerView Demo
Figure 552: ChoiceList RecyclerView Demo

Note that since this sample is using Theme.Material on Android 5.0+ devices, and since the screenshot is from an Android 5.0 emulator, the CheckBox styling is based on the accent color, here shown as bright yellow.

Switching to the Activated Style

Also note that ChoiceCapableAdapter, MultiChoiceMode, and kin are oblivious to how the user is informed about what is checked and unchecked. RowController in the previous sample happens to use a CheckBox. RowController could use some other widget, like a Switch.

Another approach is to use the activated state. Once again, this is the sort of thing that is automatically handled for us by ListView and its choice modes, but with some minor tweaks, we can get our RowController to use this approach. This is shown in the RecyclerView/ActivatedList sample project.

First, we need to give our row a background that has a StateListDrawable that supports the activated state. The simplest approach — and the one traditionally used with ListView — is to set up an activated style with the stock theme-supplied background drawable, then apply that style to the row.

So, this sample app defines activated in res/values/styles.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>

  <style name="Theme.Apptheme" parent="@android:style/Theme.Holo.Light.DarkActionBar">
  </style>

  <style name="activated" parent="Theme.Apptheme">
    <item name="android:background">?android:attr/activatedBackgroundIndicator</item>
  </style>

</resources>
(from RecyclerView/ActivatedList/app/src/main/res/values/styles.xml)

Note that activated inherits from Theme.Apptheme. This means that we will get the Theme.Holo-flavored background normally, but on API Level 21+, we will get the Theme.Material-flavored background, courtesy of a res/values-v21/styles.xml override of Theme.Apptheme:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <style name="Theme.Apptheme" parent="android:Theme.Material.Light.DarkActionBar">
    <item name="android:colorPrimary">@color/primary</item>
    <item name="android:colorPrimaryDark">@color/primary_dark</item>
    <item name="android:colorAccent">@color/accent</item>
  </style>
</resources>
(from RecyclerView/ActivatedList/app/src/main/res/values-v21/styles.xml)

Our row layout now dumps the CardView (whose own background may conflict with the activated one) and applies the activated style to the root LinearLayout:

<?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="wrap_content"
  android:orientation="horizontal"
  style="@style/activated">

  <ImageView
    android:id="@+id/icon"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_vertical"
    android:padding="2dip"
    android:src="@drawable/ok"
    android:contentDescription="@string/icon"/>

  <LinearLayout
    android:layout_width="0dip"
    android:layout_height="wrap_content"
    android:layout_weight="1"
    android:orientation="vertical">

    <TextView
      android:id="@+id/label"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:textSize="25sp"
      android:textStyle="bold"/>

    <TextView
      android:id="@+id/size"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:textSize="15sp"/>
  </LinearLayout>

</LinearLayout>

(from RecyclerView/ActivatedList/app/src/main/res/layout/row.xml)

The row also no longer has the CheckBox, as it is no longer needed.

RowController now uses the OnClickListener interface to respond to clicks and use that to toggle the activated state for that row:

  @Override
  public void onClick(View v) {
    boolean isCheckedNow=adapter.isChecked(getAdapterPosition());

    adapter.onChecked(getAdapterPosition(), !isCheckedNow);
    row.setActivated(!isCheckedNow);
  }

(from RecyclerView/ActivatedList/app/src/main/java/com/commonsware/android/recyclerview/activatedlist/RowController.java)

setActivated(), applied to a View, indicates that it is (or is not) activated, affecting anything in that View that depends upon that state, such as the background.

Similarly, bindModel() uses setActivated() to update the activated state when binding our data:

  void bindModel(String item) {
    label.setText(item);
    size.setText(String.format(template, item.length()));

    if (item.length()>4) {
      icon.setImageResource(R.drawable.delete);
    }
    else {
      icon.setImageResource(R.drawable.ok);
    }

    row.setActivated(adapter.isChecked(getAdapterPosition()));

(from RecyclerView/ActivatedList/app/src/main/java/com/commonsware/android/recyclerview/activatedlist/RowController.java)

Everything else is the same as the original CheckBox version of the sample. But now, the “checked” state is indicated by the activated highlight:

ActivatedList RecyclerView Demo
Figure 553: ActivatedList RecyclerView Demo

And, since this demo is running on Android 5.0, the activated highlight color is the accent color, which in this case is set to be yellow.

But, What About Single-Choice?

Both of the preceding examples illustrate multiple-choice behavior. Sometimes, though, single-choice behavior is the better option. For example, in a master-detail structure, in dual-pane mode (e.g., tablets, where the master and the detail are both visible), you probably normally want single-choice mode.

That is certainly possible, though, once again, RecyclerView does not offer it. It also adds a wrinkle: how do we arrange to uncheck a previously-checked item, when the user checks another item? Like RadioButton widgets in a RadioGroup, we need to ensure that only one item at a time is checked, and that will require us to update the UI of the formerly-checked-but-now-unchecked item.

With some tweaks, the last sample project, where we used the activated state for a multiple-choice list, can be revised to limit the user to a single choice. Those tweaks are illustrated in the RecyclerView/SingleActivatedList sample project.

The ChoiceMode interface now has two new methods:

  1. isSingleChoice() will return true for a single-choice ChoiceMode strategy, false otherwise
  2. getCheckedPosition() will return the position of whatever the currently-checked item is

package com.commonsware.android.recyclerview.singleactivatedlist;

import android.os.Bundle;

public interface ChoiceMode {
  boolean isSingleChoice();
  int getCheckedPosition();
  void setChecked(int position, boolean isChecked);
  boolean isChecked(int position);
  void onSaveInstanceState(Bundle state);
  void onRestoreInstanceState(Bundle state);
}

(from RecyclerView/SingleActivatedList/app/src/main/java/com/commonsware/android/recyclerview/singleactivatedlist/ChoiceMode.java)

SingleChoiceMode is now our implementation of ChoiceMode:

package com.commonsware.android.recyclerview.singleactivatedlist;

import android.os.Bundle;

public class SingleChoiceMode implements ChoiceMode {
  private static final String STATE_CHECKED="checkedPosition";
  private int checkedPosition=-1;

  @Override
  public boolean isSingleChoice() {
    return(true);
  }

  @Override
  public int getCheckedPosition() {
    return(checkedPosition);
  }

  @Override
  public void setChecked(int position, boolean isChecked) {
    if (isChecked) {
      checkedPosition=position;
    }
    else if (isChecked(position)) {
      checkedPosition=-1;
    }
  }

  @Override
  public boolean isChecked(int position) {
    return(checkedPosition==position);
  }

  @Override
  public void onSaveInstanceState(Bundle state) {
    state.putInt(STATE_CHECKED, checkedPosition);
  }

  @Override
  public void onRestoreInstanceState(Bundle state) {
    checkedPosition=state.getInt(STATE_CHECKED, -1);
  }
}

(from RecyclerView/SingleActivatedList/app/src/main/java/com/commonsware/android/recyclerview/singleactivatedlist/SingleChoiceMode.java)

SingleChoiceMode tracks the currently-checked position, using -1 to indicate no position is checked. Of note, if a position was checked, then setChecked() unchecks it, SingleChoiceMode goes back to -1 and indicates that there is no currently-checked position.

ChoiceCapableAdapter also has a couple of modifications. First, it now accepts the RecyclerView itself as a constructor parameter, holding onto it in an rv data member. And, onChecked() needs to be modified to take care of removing the activated state from whatever item had been previously checked when some new item is checked:

package com.commonsware.android.recyclerview.singleactivatedlist;

import android.os.Bundle;
import android.support.v7.widget.RecyclerView;

abstract public class
    ChoiceCapableAdapter<T extends RecyclerView.ViewHolder>
    extends RecyclerView.Adapter<T> {
  private final ChoiceMode choiceMode;
  private final RecyclerView rv;

  public ChoiceCapableAdapter(RecyclerView rv,
                              ChoiceMode choiceMode) {
    super();
    this.rv=rv;
    this.choiceMode=choiceMode;
  }

  void onChecked(int position, boolean isChecked) {
    if (choiceMode.isSingleChoice()) {
      int checked=choiceMode.getCheckedPosition();

      if (checked>=0) {
        RowController row=
            (RowController)rv.findViewHolderForAdapterPosition(checked);

        if (row!=null) {
          row.setChecked(false);
        }
      }
    }

    choiceMode.setChecked(position, isChecked);
  }

  boolean isChecked(int position) {
    return(choiceMode.isChecked(position));
  }

  void onSaveInstanceState(Bundle state) {
    choiceMode.onSaveInstanceState(state);
  }

  void onRestoreInstanceState(Bundle state) {
    choiceMode.onRestoreInstanceState(state);
  }

  @Override
  public void onViewAttachedToWindow(T holder) {
    super.onViewAttachedToWindow(holder);

    if (holder.getAdapterPosition()!=choiceMode.getCheckedPosition()) {
      ((RowController)holder).setChecked(false);
    }
  }
}

(from RecyclerView/SingleActivatedList/app/src/main/java/com/commonsware/android/recyclerview/singleactivatedlist/ChoiceCapableAdapter.java)

To do that, onChecked() asks the ChoiceMode if it is single choice. If yes, it gets the last checked position. If that position is plausible (0 or higher), it gets the RecyclerView.ViewHolder for that position via findViewHolderForAdapterPosition(), called on the RecyclerView. If this returns something other than null, then it must be a RowController, and so onChecked() calls setChecked(false) on that row to remove the activated state.

findViewHolderForAdapterPosition() and findViewHolderForLayoutPosition() replace the now-deprecated findViewHolderForPosition() method. All three methods do the same basic thing: given a position, return the ViewHolder for that position, if any. findViewHolderForPosition() and findViewHolderForLayoutPosition() have the same implementation, at least at the present time. The primary thing that findViewHolderForAdapterPosition() does differently is it always returns null if the data has been changed (e.g., notifyDataSetChanged() was called on the adapter) but those changes have not yet been laid out. In this sample app, that difference is academic, but findViewHolderForAdapterPosition() probably is a safer choice for most use cases.

However, these find...() methods have a wrinkle: they only return a ViewHolder if the row is visible. If the ViewHolder is cached, but not visible, find...() will still not return it. This causes a problem where we need to de-select a row that is not visible (and so find...() does not work) but will not be re-bound using onBindViewHolder() (as the ViewHolder is already set up). This requires us to implement onViewAttachedToWindow() — called whenever a ViewHolder contents are actually attached as children to the RelativeLayout — and update the checked state there, as a fallback.

(and many thanks to Mahmoud Abou-Eita for reporting that problem)

setChecked() did not exist in the previous sample, as the activated state was handled purely internally to RowController. So, now RowController has a setChecked() method to toggle the activated state:

  void setChecked(boolean isChecked) {
    row.setActivated(isChecked);
  }

(from RecyclerView/SingleActivatedList/app/src/main/java/com/commonsware/android/recyclerview/singleactivatedlist/RowController.java)

MainActivity now must supply the RecyclerView to the IconicAdapter in onCreate():

  @Override
  public void onCreate(Bundle icicle) {
    super.onCreate(icicle);

    setLayoutManager(new LinearLayoutManager(this));
    adapter=new IconicAdapter(getRecyclerView());
    setAdapter(adapter);
  }

(from RecyclerView/SingleActivatedList/app/src/main/java/com/commonsware/android/recyclerview/singleactivatedlist/MainActivity.java)

…so that IconicAdapter() can supply it to the ChoiceCapableAdapter superclass constructor:

    IconicAdapter(RecyclerView rv) {
      super(rv, new SingleChoiceMode());
    }

(from RecyclerView/SingleActivatedList/app/src/main/java/com/commonsware/android/recyclerview/singleactivatedlist/MainActivity.java)

Visually, the results are identical to the previous example, except that at most only one item can be checked at a time. The key is the phrase “at most” — this implementation allows the user to tap on a checked item to uncheck it. This may be fine, as your app may simply hide the detail in this scenario, still allowing the user to interact with action bar items (e.g., a “create new model” item). If you wanted to prevent that, have SingleChoiceMode not set checkedPosition to -1 when the user taps on a previously-checked item, to leave the currently-checked position intact.

Keyboard Navigation

If you try using the RecyclerView/SingleActivatedList app — or any of the sample apps presented so far — on a device that has a physical keyboard or five-way navigation option (e.g., D-pad), you will find that RecyclerView has no built-in keyboard navigation. This is in contrast with standard AdapterView classes like ListView, where key events are handled automatically. Once again, if you want the behavior, you have to add it yourself.

The exact details of what you want to do when the user tries navigating with a keyboard will vary, based on lots of things:

The RecyclerView/SingleActivatedListKB sample project is a clone of the SingleActivatedList sample, except that we now support keyboard events. Specifically, the user can use the up and down arrow keys to change the selected row in the list. This is perhaps the simplest scenario:

And, the best part is that we only need to change ChoiceCapableAdapter — the rest of the app can remain unchanged.

First, we override another method on RecyclerView.Adapter: onAttachedToRecyclerView(). As the name suggests, this method is called when our adapter is assigned to a RecyclerView instance. Here, if we are in single-choice mode, we register an OnKeyListener on the RecyclerView itself, to find out when it receives key events:

  // inspired by http://stackoverflow.com/a/28838834/115145

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

    if (choiceMode.isSingleChoice()) {
      rv.setOnKeyListener(new View.OnKeyListener() {
        @Override
        public boolean onKey(View v, int keyCode, KeyEvent event) {
          if (event.getAction()==KeyEvent.ACTION_DOWN) {
            switch (keyCode) {
              case KeyEvent.KEYCODE_DPAD_DOWN:
                return(chooseNext());
              case KeyEvent.KEYCODE_DPAD_UP:
                return(choosePrevious());
            }
          }

          return(false);
        }
      });
    }
  }

(from RecyclerView/SingleActivatedListKB/app/src/main/java/com/commonsware/android/recyclerview/singleactivatedlist/ChoiceCapableAdapter.java)

When the user presses down either the up or down arrow key (or equivalents on a D-pad), we call out to private choosePrevious() and chooseNext() methods, which will return true if we moved the selection in that direction, false otherwise. If the key event is not one of those, we return false to indicate that we are not consuming the key event.

The choosePrevious() and chooseNext() methods are responsible for determining what our next selection should be, assuming that the selection can change in the designated direction:

  private boolean chooseNext() {
    long now=System.currentTimeMillis();
    boolean result=false;

    if (lastDownKeyTime==-1 || now-lastDownKeyTime>KEY_TIME_DELTA) {
      lastDownKeyTime=now;
      lastUpKeyTime=-1L;

      int checked=choiceMode.getCheckedPosition();

      if (checked<0) {
        onChecked(0, true, true);
        result=true;
      }
      else if (checked<getItemCount()-1) {
        onChecked(checked+1, true, true);
        result=true;
      }
    }

    return(result);
  }

  private boolean choosePrevious() {
    long now=System.currentTimeMillis();
    boolean result=false;

    if (lastUpKeyTime==-1 || now-lastUpKeyTime>KEY_TIME_DELTA) {
      lastUpKeyTime=now;
      lastDownKeyTime=-1L;

      int checked=choiceMode.getCheckedPosition();

      if (checked>0) {
        onChecked(checked-1, true, true);
        result=true;
      }
      else if (checked<0) {
        onChecked(0, true, true);
        result=true;
      }
    }

    return(result);
  }

(from RecyclerView/SingleActivatedListKB/app/src/main/java/com/commonsware/android/recyclerview/singleactivatedlist/ChoiceCapableAdapter.java)

In both cases, we find out what the current selection is. If it is a negative number, we do not have a selection yet, so we select the first row. Otherwise, if we can still move in the desired direction, add or subtract one from the current selection.

However, in this crude implementation, we need to slow down how frequently we change the selection. Simply changing which row is highlighted is fast, but if the list has to scroll to uncover that row, doing too many of those too quickly results in a “smearing” effect. The simplest way to avoid that smearing is to limit how many consecutive identical key events we process (e.g., user holds down an arrow key).

The approach taken in this code is to track the last time we were called to process an arrow key event for each direction, in lastUpKeyTime and lastDownKeyTime fields. We use -1 to indicate that we have not just processed another one of that type. If, when we get a key event, either the related time value is -1 or is within KEY_TIME_DELTA of now, we go ahead and update the checked position (if needed), plus update the times. KEY_TIME_DELTA is defined as 250, limiting us to four updates per second.

We change the current selection, where needed, via a call to a new three-parameter onChecked() method:

  void onChecked(int position, boolean isChecked) {
    onChecked(position, isChecked, false);
  }

  void onChecked(int position, boolean isChecked, boolean updateUI) {
    if (choiceMode.isSingleChoice()) {
      int checked=choiceMode.getCheckedPosition();

      if (checked>=0) {
        RowController row=
            (RowController)rv.findViewHolderForAdapterPosition(checked);

        if (row!=null) {
          row.setChecked(false);
        }
      }
    }

    choiceMode.setChecked(position, isChecked);

    if (updateUI) {
      notifyItemChanged(position);
      rv.scrollToPosition(position);
    }
  }

(from RecyclerView/SingleActivatedListKB/app/src/main/java/com/commonsware/android/recyclerview/singleactivatedlist/ChoiceCapableAdapter.java)

The third parameter indicates if we need to update the UI or not. For touchscreen events, activating rows and such is enough to ensure that the UI is properly updated. With key events, we need to:

Action Modes

Another thing that ListView gave us was support for action modes. In particular, the “multiple-choice modal” setting would automatically start and finish an action mode for us.

And, once again, RecyclerView has no hooks for action modes, though you can do it yourself if desired. We have to manually start and destroy the action mode, in addition to responding to the user’s interaction with the action mode (tapping on items, or dismissing the action mode manually).

Where things get interesting is in the connection between checked items and the action mode. There are two UX rules:

  1. When there are no checked items, there should be no action mode
  2. When there is no action mode, there should be no checked items

You might think that those two rules are the same, and to some extent they are. They are phrased this way to emphasize the state changes that are involved:

Handling these transitions takes a bit of work, demonstrated in the RecyclerView/ActionModeList sample project. This is a clone of the ChoiceList sample from earlier, augmented with an action mode when 1+ items are checked. The action mode logic is largely cloned from one of the book’s action mode samples, where we want to allow the user to capitalize or remove the checked items.

Once again, we have some tweaks to ChoiceMode, adding two methods:

  1. getCheckedCount(), to return the number of checked items, which we will use for the subtitle of the action mode
  2. clearChecks(), to uncheck all checked items

package com.commonsware.android.recyclerview.actionmodelist;

import android.os.Bundle;

public interface ChoiceMode {
  void setChecked(int position, boolean isChecked);
  boolean isChecked(int position);
  void onSaveInstanceState(Bundle state);
  void onRestoreInstanceState(Bundle state);
  int getCheckedCount();
  void clearChecks();
}

(from RecyclerView/ActionModeList/app/src/main/java/com/commonsware/android/recyclerview/actionmodelist/ChoiceMode.java)

MultiChoiceMode implements those, plus adds a subtle change to setChecked(). Previous editions of MultiChoiceMode would simply put the checked state boolean into the ParceableSparseBooleanArray, with false as a default value for any position not in the array. Now, we specifically remove items that are unchecked, so the only items in the ParcelableSparseBooleanArray are those that are checked. This makes getCheckedCount() and clearChecks() very simple to implement:

package com.commonsware.android.recyclerview.actionmodelist;

import android.os.Bundle;

public class MultiChoiceMode implements ChoiceMode {
  private static final String STATE_CHECK_STATES="checkStates";
  private ParcelableSparseBooleanArray checkStates=new ParcelableSparseBooleanArray();

  @Override
  public void setChecked(int position, boolean isChecked) {
    if (isChecked) {
      checkStates.put(position, isChecked);
    }
    else {
      checkStates.delete(position);
    }
  }

  @Override
  public boolean isChecked(int position) {
    return(checkStates.get(position, false));
  }

  @Override
  public void onSaveInstanceState(Bundle state) {
    state.putParcelable(STATE_CHECK_STATES, checkStates);
  }

  @Override
  public void onRestoreInstanceState(Bundle state) {
    checkStates=state.getParcelable(STATE_CHECK_STATES);
  }

  @Override
  public int getCheckedCount() {
    return(checkStates.size());
  }

  @Override
  public void clearChecks() {
    checkStates.clear();
  }
}

(from RecyclerView/ActionModeList/app/src/main/java/com/commonsware/android/recyclerview/actionmodelist/MultiChoiceMode.java)

ChoiceCapableAdapter exposes the two new ChoiceMode capabilities to its subclasses:

package com.commonsware.android.recyclerview.actionmodelist;

import android.os.Bundle;
import android.support.v7.widget.RecyclerView;

abstract public class
    ChoiceCapableAdapter<T extends RecyclerView.ViewHolder>
    extends RecyclerView.Adapter<T> {
  private final ChoiceMode choiceMode;

  public ChoiceCapableAdapter(ChoiceMode choiceMode) {
    super();
    this.choiceMode=choiceMode;
  }

  void onChecked(int position, boolean isChecked) {
    choiceMode.setChecked(position, isChecked);
  }

  boolean isChecked(int position) {
    return(choiceMode.isChecked(position));
  }

  void onSaveInstanceState(Bundle state) {
    choiceMode.onSaveInstanceState(state);
  }

  void onRestoreInstanceState(Bundle state) {
    choiceMode.onRestoreInstanceState(state);
  }

  int getCheckedCount() {
    return(choiceMode.getCheckedCount());
  }

  void clearChecks() {
    choiceMode.clearChecks();
  }
}

(from RecyclerView/ActionModeList/app/src/main/java/com/commonsware/android/recyclerview/actionmodelist/ChoiceCapableAdapter.java)

IconicAdapter now not only extends ChoiceCapableAdapter, but it implements the ActionMode.Callback interface, and therefore will be responsible for managing the action mode:

  class IconicAdapter extends ChoiceCapableAdapter<RowController>
      implements ActionMode.Callback {

(from RecyclerView/ActionModeList/app/src/main/java/com/commonsware/android/recyclerview/actionmodelist/MainActivity.java)

IconicAdapter now overrides onChecked(), normally just handled by ChoiceCapableAdapter. In addition to chaining to the superclass for standard behavior, IconicAdapter manages the action mode:

    @Override
    void onChecked(int position, boolean isChecked) {
      super.onChecked(position, isChecked);

      if (isChecked) {
        if (activeMode==null) {
          activeMode=startActionMode(this);
        }
        else {
          updateSubtitle(activeMode);
        }
      }
      else if (getCheckedCount()==0 && activeMode!=null) {
        activeMode.finish();
      }
    }

(from RecyclerView/ActionModeList/app/src/main/java/com/commonsware/android/recyclerview/actionmodelist/MainActivity.java)

Because IconicAdapter implements ActionMode.Callback, it needs to implement the methods required by that interface. This includes:

    @Override
    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
      MenuInflater inflater=getMenuInflater();

      inflater.inflate(R.menu.context, menu);
      mode.setTitle(R.string.context_title);
      activeMode=mode;
      updateSubtitle(activeMode);

      return(true);
    }

    @Override
    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
      return(false);
    }

    @Override
    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
      // TODO: do something based on the action

      updateSubtitle(activeMode);

      return(true);
    }

    @Override
    public void onDestroyActionMode(ActionMode mode) {
      if (activeMode != null) {
        activeMode=null;
        clearChecks();
        notifyDataSetChanged();
      }
    }

(from RecyclerView/ActionModeList/app/src/main/java/com/commonsware/android/recyclerview/actionmodelist/MainActivity.java)

The updateSubtitle() method, referred to by some of the previous methods, just updates the subtitle of the action mode to reflect the current count of checked items:

    private void updateSubtitle(ActionMode mode) {
      mode.setSubtitle("(" + getCheckedCount() + ")");
    }

(from RecyclerView/ActionModeList/app/src/main/java/com/commonsware/android/recyclerview/actionmodelist/MainActivity.java)

The resulting app looks a lot like the original ChoiceList sample, until we check one or more items, at which point the action mode appears:

ActionModeList RecyclerView Demo
Figure 554: ActionModeList RecyclerView Demo

Changing the Contents

The obvious problem with the preceding sample is that we are not actually doing anything in response to user clicks on action mode items. We really should be capitalizing and/or removing words. However, this involves modifying the model data and how that model data is being visually displayed by the RecyclerView.

The less-obvious problem is that we are calling notifyDataSetChanged() when the action mode is dismissed, to force a full repaint of the RecyclerView contents. While this works, it is overkill, as probably only a subset of the visible items are checked. Ideally, we would only update the specific positions that were checked and now, with the action mode finished, are unchecked. We could find the affected RowController instances, using findViewHolderByPosition() on RecyclerView, as we did in the single-choice list sample. But, really, updating the checked state is just another manifestation of the same problem that capitalizing or removing words causes: we need to ensure that the RecyclerView depicts the current model state, ideally with minimum work.

So, let’s see how this is accomplished, by looking at the RecyclerView/ActionModeList2 sample project. As the name suggests, this is a clone of the ActionModeList shown in the preceding section. This time, we will fully implement onActionItemClicked() and allow our model data to be mutable.

Updating Existing Contents

With AdapterView and Adapter classes based on BaseAdapter, the only way we had to tell the AdapterView about model data changes was notifyDataSetChanged(). This would trigger a rebuild of the entire AdapterView, which is slow and expensive.

While RecyclerView.Adapter has its own notifyDataSetChanged(), that is really for total reloads of the model data, such as having gotten a fresh Cursor from a database and not knowing exactly what the changes are. If you are driving the changes yourself from the UI — and particularly if your model data is something like an ArrayList of model objects – you can use methods on RecyclerView.Adapter that are more fine-grained than is notifyDataSetChanged().

If an item was updated in place — such as a word now being capitalized – you can use notifyItemChanged() on RecyclerView.Adapter to point out the specific position that changed. Alternatives include:

ActionModeList2 uses notifyItemChanged() when the user capitalizes words, to get those items repainted, if needed. It may not be needed immediately, if one or more of those items are not presently visible within the RecyclerView.

However, so far, our model data has been a static String array, and now we need a mutable model. So, we take the same approach as the ListView action mode samples use, converting our model to be an ArrayList that happens to be populated by a static String array.

The items data member of MainActivity is now an ArrayList of String, with the static String array being converted into ORIGINAL_ITEMS:

  private static final String[] ORIGINAL_ITEMS={"lorem", "ipsum", "dolor",
          "sit", "amet",
          "consectetuer", "adipiscing", "elit", "morbi", "vel",
          "ligula", "vitae", "arcu", "aliquet", "mollis",
          "etiam", "vel", "erat", "placerat", "ante",
          "porttitor", "sodales", "pellentesque", "augue", "purus"};
  private ArrayList<String> items;

(from RecyclerView/ActionModeList2/app/src/main/java/com/commonsware/android/recyclerview/actionmodelist2/MainActivity.java)

Places that used to refer to items now use a private getItems() method, which lazy-instantiates the list if needed:

  private ArrayList<String> getItems() {
    if (items==null) {
      items=new ArrayList<String>();

      for (String s : ORIGINAL_ITEMS) {
        items.add(s);
      }
    }

    return(items);
  }

(from RecyclerView/ActionModeList2/app/src/main/java/com/commonsware/android/recyclerview/actionmodelist2/MainActivity.java)

We also need to ensure that we hold onto the items across configuration changes, since those items could be changed by the user. So, our onSaveInstanceState() and onRestoreInstanceState() methods on MainActivity now handle that chore, in addition to their original behavior of having the ChoiceCapableAdapter persist checked states:

  @Override
  public void onSaveInstanceState(Bundle state) {
    adapter.onSaveInstanceState(state);
    state.putStringArrayList(STATE_ITEMS, items);
  }

  @Override
  public void onRestoreInstanceState(Bundle state) {
    adapter.onRestoreInstanceState(state);
    items=state.getStringArrayList(STATE_ITEMS);
  }

(from RecyclerView/ActionModeList2/app/src/main/java/com/commonsware/android/recyclerview/actionmodelist2/MainActivity.java)

(here, STATE_ITEMS is a static data member, serving as the constant key for the Bundle entry)

In order to be able to capitalize or remove the checked words from the list, we need to know which ones are checked. Rather than expose that data directly, ChoiceMode now has a visitChecks() method, where we can supply a Visitor to be invoked for every checked position:

package com.commonsware.android.recyclerview.actionmodelist2;

import android.os.Bundle;

public interface ChoiceMode {
  void setChecked(int position, boolean isChecked);
  boolean isChecked(int position);
  void onSaveInstanceState(Bundle state);
  void onRestoreInstanceState(Bundle state);
  int getCheckedCount();
  void clearChecks();
  void visitChecks(Visitor v);

  public interface Visitor {
    void onCheckedPosition(int position);
  }
}

(from RecyclerView/ActionModeList2/app/src/main/java/com/commonsware/android/recyclerview/actionmodelist2/ChoiceMode.java)

MultiChoiceMode implements visitChecks() by iterating over a copy of the checkStates ParcelableSparseBooleanArray. That way, if the visitor modifies checkStates (e.g., unchecks a position), our loop is unaffected.

  @Override
  public void visitChecks(Visitor v) {
    SparseBooleanArray copy=checkStates.clone();

    for (int i=0;i<copy.size();i++) {
      v.onCheckedPosition(copy.keyAt(i));
    }
  }

(from RecyclerView/ActionModeList2/app/src/main/java/com/commonsware/android/recyclerview/actionmodelist2/MultiChoiceMode.java)

visitChecks() is also exposed by ChoiceCapableAdapter, as are all the other methods on ChoiceMode.

Now, IconicAdapter can capitalize the words, by using visitChecks():

        case R.id.cap:
          visitChecks(new ChoiceMode.Visitor() {
            @Override
            public void onCheckedPosition(int position) {
              String word=getItems().get(position);

              word=word.toUpperCase(Locale.ENGLISH);
              getItems().set(position, word);
              notifyItemChanged(position);
            }
          });
          break;

(from RecyclerView/ActionModeList2/app/src/main/java/com/commonsware/android/recyclerview/actionmodelist2/MainActivity.java)

Here, for each checked item, we capitalize the word, replace the original word with its capitalized equivalent, and call notifyItemChanged() to let the RecyclerView know that this position had its model data changed and therefore should be repainted, if needed.

We also use visitChecks() now in onDestroyActionMode(), to avoid the notifyDataSetChanged() call:

    @Override
    public void onDestroyActionMode(ActionMode mode) {
      if (activeMode != null) {
        activeMode=null;
        visitChecks(new ChoiceMode.Visitor() {
          @Override
          public void onCheckedPosition(int position) {
            onChecked(position, false);
            notifyItemChanged(position);
          }
        });
      }
    }

(from RecyclerView/ActionModeList2/app/src/main/java/com/commonsware/android/recyclerview/actionmodelist2/MainActivity.java)

Each item that was checked is unchecked, and we use notifyItemChanged() to ensure that the item is repainted if needed.

Now, checking some items and choosing “CAPITALIZE” from the action mode will capitalize those words:

ActionModeList2 RecyclerView Demo, with Capitalized Words
Figure 555: ActionModeList2 RecyclerView Demo, with Capitalized Words

Adding and Removing Items

There are also methods on RecyclerView.Adapter to specifically call out when you are adding or removing items from the adapter. Not only does this cause the RecyclerView to update itself, but it will animate the changes, if the relevant position(s) are visible.

Specifically, you can call:

The ActionModeList2 sample uses notifyItemRemoved() as part of its handling of the remove action mode item:

        case R.id.remove:
          final ArrayList<Integer> positions=new ArrayList<Integer>();

          visitChecks(new ChoiceMode.Visitor() {
            @Override
            public void onCheckedPosition(int position) {
              positions.add(position);
            }
          });

          Collections.sort(positions, Collections.reverseOrder());

          for (int position : positions) {
            getItems().remove(position);
            notifyItemRemoved(position);
          }

          clearChecks();
          activeMode.finish();
          break;

(from RecyclerView/ActionModeList2/app/src/main/java/com/commonsware/android/recyclerview/actionmodelist2/MainActivity.java)

Because items slide up to take over vacated positions, when removing items, it is important to remove the lowest items first and work your way up the roster. That is why this code:

The result is that when the user removes items, they rapidly fade out, then later items in the list slide up to occupy the now-vacated space. If you would prefer to use other animations, you can do so, by creating your own subclass of RecyclerView.ItemAnimator and attaching it to the RecyclerView with setItemAnimator().

The Order of Things

Version 22+ of recyclerview-v7 offers SortedList. On the surface, the class appears to be a regular List that offers sorting. However, it also has a callback interface designed to be tied into RecyclerView, so that changes made to the SortedList can be reflected in the RecyclerView itself, complete with animations, optional batched processing, and so on.

This is illustrated in the RecyclerView/SortedList sample project. Along the way, we will also see how to use RecyclerView in a fragment and how to populate RecyclerView from the background thread.

The Gradle Change

This project requires version 22 or higher of recyclerview-v7, as the original v21 release of recyclerview-v7 did not have SortedList. So, the Gradle build file requests something appropriate:

    compile 'com.android.support:recyclerview-v7:22.2.0'
    compile 'com.android.support:cardview-v7:22.2.0'

(from RecyclerView/SortedList/app/build.gradle)

Note that it pulls in the same version of cardview-v7. In general, it is best to try to keep Android Support package libraries in sync, at least in terms of major versions. Similarly, it is best to have the compileSdkVersion match the major library version, as the library may be conditionally using APIs made available in that version of Android. Hence, the project also has compileSdkVersion (and buildTools) set to pull from v22:

    compileSdkVersion 22
    buildToolsVersion "22.0.1"

(from RecyclerView/SortedList/app/build.gradle)

The RecyclerViewFragment

Prior samples in this chapter used a RecyclerViewActivity for basic RecyclerView setup. However, in this sample, we want to use a retained fragment for managing the AsyncTask, which suggests putting the RecyclerView in a fragment, rather than having it be managed directly by the activity.

So, this project has a reworking of RecyclerViewActivity into RecyclerViewFragment:

package com.commonsware.android.recyclerview.sorted;

import android.app.Fragment;
import android.os.Bundle;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class RecyclerViewFragment extends Fragment {
  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
                           Bundle savedInstanceState) {
    RecyclerView rv=new RecyclerView(getActivity());

    rv.setHasFixedSize(true);

    return(rv);
  }

  public void setAdapter(RecyclerView.Adapter adapter) {
    getRecyclerView().setAdapter(adapter);
  }

  public RecyclerView.Adapter getAdapter() {
    return(getRecyclerView().getAdapter());
  }

  public void setLayoutManager(RecyclerView.LayoutManager mgr) {
    getRecyclerView().setLayoutManager(mgr);
  }

  public RecyclerView getRecyclerView() {
    return((RecyclerView)getView());
  }
}

(from RecyclerView/SortedList/app/src/main/java/com/commonsware/android/recyclerview/sorted/RecyclerViewFragment.java)

Basically, what had been in onCreate() mostly moves into onCreateView(), where we set up the RecyclerView. The rest of the core API is unchanged.

The project has SortedFragment, which extends RecyclerViewFragment and handles loading of the data — we will examine more of it later in this chapter.

The revised MainActivity then just loads up SortedFragment via a FragmentTransaction:

package com.commonsware.android.recyclerview.sorted;

import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    if (getFragmentManager().findFragmentById(android.R.id.content) == null) {
      getFragmentManager().beginTransaction()
                          .add(android.R.id.content,
                               new SortedFragment()).commit();
    }
  }
}

(from RecyclerView/SortedList/app/src/main/java/com/commonsware/android/recyclerview/sorted/MainActivity.java)

The SortedFragment

Most of SortedFragment is reminiscent of the original AsyncTask demo from the chapter on threads, mashed up with one of the CardView/RecyclerView samples from earlier in this chapter. However, the SortedList gets weaved throughout.

The SortedList

The model in the original AsyncTask demo was a simple ArrayList. Now it is a SortedList, initialized in onCreate() of the SortedFragment:

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setRetainInstance(true);

    model=new SortedList<String>(String.class, sortCallback);

    task=new AddStringTask();
    task.execute();
  }

(from RecyclerView/SortedList/app/src/main/java/com/commonsware/android/recyclerview/sorted/SortedFragment.java)

The SortedList constructor takes two parameters:

There is an optional third parameter for the capacity, unused in this sample.

We will take a peek at the SortedList.Callback implementation, named sortCallback, shortly.

The IconicAdapter

The IconicAdapter from earlier RecyclerView samples worked directly off of the static array of String values. Now, we want it to work off of the model SortedList. Hence, onBindViewHolder() and getItemCount() need to be modified to refer to appropriate methods on the model:

  class IconicAdapter extends RecyclerView.Adapter<RowController> {
    @Override
    public RowController onCreateViewHolder(ViewGroup parent, int viewType) {
      return(new RowController(getActivity().getLayoutInflater()
                                .inflate(R.layout.row, parent, false)));
    }

    @Override
    public void onBindViewHolder(RowController holder, int position) {
      holder.bindModel(model.get(position));
    }

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

(from RecyclerView/SortedList/app/src/main/java/com/commonsware/android/recyclerview/sorted/SortedFragment.java)

Also note that when we create the adapter in onViewCreated(), that we hold onto it in an adapter data member of the fragment:

  @Override
  public void onViewCreated(View view, Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);

    setLayoutManager(new LinearLayoutManager(getActivity()));
    adapter=new IconicAdapter();
    setAdapter(adapter);
  }

(from RecyclerView/SortedList/app/src/main/java/com/commonsware/android/recyclerview/sorted/SortedFragment.java)

The SortedList.Callback

The job of the SortedList.Callback is to serve as the bridge between the SortedList and the RecyclerView.Adapter.

SortedList, as the name suggests, sorts its contents. That means that any change to the SortedList contents can have different impacts on the RecyclerView. For example, while an add() to an ArrayList would just add a new row to the end of the RecyclerView, an add() on SortedList might need to insert a row in the middle of the RecyclerView, to maintain the sorted order.

Hence, your SortedList.Callback is responsible for two things:

With that in mind, here is the sortedCB implementation of SortedList.Callback:

  private SortedList.Callback<String> sortCallback=new SortedList.Callback<String>() {
    @Override
    public int compare(String o1, String o2) {
      return o1.compareTo(o2);
    }

    @Override
    public boolean areContentsTheSame(String oldItem, String newItem) {
      return(areItemsTheSame(oldItem, newItem));
    }

    @Override
    public boolean areItemsTheSame(String oldItem, String newItem) {
      return(compare(oldItem, newItem)==0);
    }

    @Override
    public void onInserted(int position, int count) {
      adapter.notifyItemRangeInserted(position, count);
    }

    @Override
    public void onRemoved(int position, int count) {
      adapter.notifyItemRangeRemoved(position, count);
    }

    @Override
    public void onMoved(int fromPosition, int toPosition) {
      adapter.notifyItemMoved(fromPosition, toPosition);
    }

    @Override
    public void onChanged(int position, int count) {
      adapter.notifyItemRangeChanged(position, count);
    }
  };

(from RecyclerView/SortedList/app/src/main/java/com/commonsware/android/recyclerview/sorted/SortedFragment.java)

The first method is your standard sort of compare() comparison method, as you might implement on a Comparator. It should return zero if the two model objects are the same from a sorting standpoint, a negative number if the first parameter sorts before the second parameter, or a positive number if the first parameter sorts after the second parameter.

Then there are two similarly-named methods that serve as more-or-less replacements for the equals() that you might have on a Comparator: areContentsTheSame() and areItemsTheSame().

areItemsTheSame() should return true if the two passed-in values represent the same actual logical item. In the case of SortedFragment, that is simply whether or not the strings are equal. But, with a more complex data model, you might be comparing primary keys or some other form of immutable identifier.

areContentsTheSame() should return true if the visual representation of the items look the same, as this will be used to optimize the changes made to the RecyclerView.

For example, suppose a shopping cart fragment wanted to use SortedList. Further suppose that if you added three boxes of laundry detergent to the cart, rather than having one row in the list with “Quantity: 3”, you were representing them as three rows in the RecyclerView. In this case:

In many cases, areContentsTheSame() can simply invoke areItemsTheSame(), under the premise that different items probably have different visual representations. That is what is done in this sample, where areItemsTheSame() in turn uses compare() to see whether or not the items are the same.

Finally, there are four on...() methods that are simply forwarded along to their RecyclerView.Adapter counterparts, so changes to the SortedList make the corresponding changes to the RecyclerView contents.

Note that there is a SortedListAdapterCallback that takes a RecyclerView.Adapter as a constructor parameter and handles the on...() methods for you. However, since we want to retain the SortedList across configuration changes, and since SortedList does not allow us to change the SortedList.Callback object, we cannot readily switch the SortedList to the new fragment and new adapter after a configuration change.

The AsyncTask

The AddStringTask is the same as with the original AsyncTask sample, except that now it adds the words to the SortedList, which (via its Callback) will update the RecyclerView:

  private class AddStringTask extends AsyncTask<Void, String, Void> {
    @Override
    protected Void doInBackground(Void... unused) {
      for (String item : items) {
        if (isCancelled())
          break;

        publishProgress(item);
        SystemClock.sleep(400);
      }

      return(null);
    }

    @Override
    protected void onProgressUpdate(String... item) {
      if (!isCancelled()) {
        model.add(item[0]);
      }
    }

    @Override
    protected void onPostExecute(Void unused) {
      Toast.makeText(getActivity(), R.string.done, Toast.LENGTH_SHORT)
          .show();

      task=null;
    }
  }

(from RecyclerView/SortedList/app/src/main/java/com/commonsware/android/recyclerview/sorted/SortedFragment.java)

The Results

If you run this sample, you will see the words be added to the list, every 400ms. However, in the original ListView-based sample, new rows were appended to the end, and so you would not see new rows appear after the ListView space was filled. In this sample, the Latin words are sorted by SortedList, and you will see them animate into position at the appropriate spots as they are added. In the end, you get the same look as in earlier CardView-based RecyclerView implementations, except that the words are sorted:

SortedList RecyclerView Demo
Figure 556: SortedList RecyclerView Demo

Other Bits of Goodness

To quote the infamous American infomercial line: “But wait! There’s more!”

In addition to LinearLayoutManager and GridLayoutManager, there is StaggeredGridLayoutManager. With a vertically-scrolling GridLayoutManager, rows are all a consistent height, but the cell widths might vary. With a vertically-scrolling StaggeredGridLayoutManager, the columns are all the same width, but the cell heights might vary.

All three of the standard layout managers support horizontal operation as well, through a boolean on a constructor. In these cases, the content will scroll horizontally, rather than vertically. This eliminates the need for third-party horizontal ListView implementations and the like.

And, of course, you can implement your own RecyclerView.LayoutManager, eschewing any of the built-in ones.

Animating the Deltas Using DiffUtil

SortedList is interesting, but it is inflexible. For example, you cannot readily change the sort order, without completely replacing the SortedList.

Moreover, it assumes that the changes that you want to make are simply to keep a list in sorted order. There are plenty of other possible changes to your data set that might occur, such as from the results of some Web service call to synchronize your local data with that on a server. You would have to somehow perform your own “diff” on the data shown in your RecyclerView and the new roster of data, to determine what changed, what did not change, and how those changes affect things like item positions within the list or grid.

Fortunately, the 25.0.0+ version of recyclerview-v7 gives you another tool: DiffUtil. This handles everything cited in the last sentence of the previous paragraph. You hand it two collections (old and new), plus an object that can help determine what the changes are (one reminiscent of a SortedList.Callback). It gives you a results object that, in turn, can update the RecyclerView to affect those changes.

The Java8/VideoLambda sample project is yet another rendition of the “list of videos” sample app from earlier in this chapter. However, it shows the list in sorted order, but using DiffUtil rather than SortedList. This allows us to offer the user the ability to change the sort order (ascending or descending).

Model

The original version of this sample used a Cursor directly, wrapping it in a RecyclerView.Adapter. That is fine, but we cannot readily sort a Cursor. It is simpler to convert the Cursor into a list of model objects, so we can sort the list.

To that end, the VideoLambda sample app has a Video class, with a constructor that can populate itself from a Cursor positioned on a valid row:

package com.commonsware.android.recyclerview.videolist;

import android.content.ContentUris;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;

class Video implements Comparable<Video> {
  final String title;
  final Uri videoUri;
  final String mimeType;

  Video(Cursor row) {
    this.title=
      row.getString(row.getColumnIndex(MediaStore.Video.Media.TITLE));
    this.videoUri=ContentUris.withAppendedId(
      MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
      row.getInt(row.getColumnIndex(MediaStore.Video.Media._ID)));
    this.mimeType=
      row.getString(row.getColumnIndex(MediaStore.Video.Media.MIME_TYPE));
  }

  @Override
  public boolean equals(Object obj) {
    if (!(obj instanceof Video)) {
      return(false);
    }

    return(videoUri.equals(((Video)obj).videoUri));
  }

  @Override
  public int hashCode() {
    return(videoUri.hashCode());
  }

  @Override
  public int compareTo(Video video) {
    return(title.compareTo(video.title));
  }
}

(from Java8/VideoLambda/app/src/main/java/com/commonsware/android/recyclerview/videolist/Video.java)

Note that Video has an implementation of equals() considering two Video objects to be equal if they point to the same Uri. That will be important when we use DiffUtil, as DiffUtil (and our helper code) need to know when two Video objects logically represent the same video.

Video also implements Comparable and therefore has a compareTo() method, implemented by comparing the titles of the videos. This will be used as part of our sorting logic.

The VideoAdapter has a setVideos() method, taking a Cursor that we loaded from the MediaStore, as before. However, now VideoAdapter does not hold that Cursor in a field. Rather, it holds an ArrayList of Video objects as the videos field, with setVideos() handling the conversion of data from the Cursor into the Video objects:

    void setVideos(Cursor c) {
      if (c==null) {
        videos=null;
        notifyDataSetChanged();
      }
      else {
        ArrayList<Video> temp=new ArrayList<>();

        while (c.moveToNext()) {
          temp.add(new Video(c));
        }

        if (videos==null) {
          videos=new ArrayList<>();
        }

        sortAndApply(temp);
      }
    }

    @Override

(from Java8/VideoLambda/app/src/main/java/com/commonsware/android/recyclerview/videolist/MainActivity.java)

We will see the sortAndApply() method, and understand what the temp local variable is all about, a bit later in this section. For now, take it on faith that:

The Menu

The other time we need to sort the videos is if the user chooses to switch from ascending to descending sort (or back again).

The app has a menu resource with a checkable menu <item>, named sort, that the user will be able to use to toggle the sort order:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
  <item
    android:id="@+id/sort"
    android:checkable="true"
    android:checked="true"
    android:enabled="false"
    android:showAsAction="never"
    android:title="@string/sort_ascending" />
</menu>
(from Java8/VideoLambda/app/src/main/res/menu/actions.xml)

Note that it is disabled initially. When the app starts up, we do not yet have our videos, since they need to be loaded from the MediaStore. Hence, we keep the item disabled until the videos are ready.

In the activity’s onCreateOptionsMenu() method, we inflate the resource, get the sort MenuItem, hold onto it in a field (also named sort), and enable it if the VideoAdapter happens to already have some videos:

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.actions, menu);
    sort=menu.findItem(R.id.sort);
    sort.setEnabled(adapter.getItemCount()>0);

    return(super.onCreateOptionsMenu(menu));
  }

(from Java8/VideoLambda/app/src/main/java/com/commonsware/android/recyclerview/videolist/MainActivity.java)

Then, in onOptionsItemSelected(), if the user taps on this action bar item, we toggle its checked state, then tell the VideoAdapter to sort() based upon that state:

  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    if (item.getItemId()==R.id.sort) {
      item.setChecked(!item.isChecked());
      adapter.sort(item.isChecked());

      return(true);
    }

    return(super.onOptionsItemSelected(item));
  }

(from Java8/VideoLambda/app/src/main/java/com/commonsware/android/recyclerview/videolist/MainActivity.java)

sort(), in turn, keeps track of the current sort order, then calls the same sortAndApply() that we did in setVideos():

        sortAscending=checked;
        sortAndApply(new ArrayList<>(videos));
      }
    }
  }
}

(from Java8/VideoLambda/app/src/main/java/com/commonsware/android/recyclerview/videolist/MainActivity.java)

The Diff-ing

The job of sortAndApply() is to do what the name suggests: sort the videos and apply the sorted list to the RecyclerView. In principle, there are two possible scenarios:

  1. We are showing the videos for the very first time
  2. We are showing the videos again, after a re-sort, or perhaps after a new video was scanned by the MediaStore, triggering the Loader framework to hand us a fresh Cursor

Fortunately, we can handle both the same way:

        Collections.sort(newVideos,
          (one, two) -> one.compareTo(two));
      }
      else {
        Collections.sort(newVideos,
          (one, two) -> two.compareTo(one));
      }

      DiffUtil.Callback cb=new SimpleCallback<>(videos, newVideos);
      DiffUtil.DiffResult result=DiffUtil.calculateDiff(cb, true);

      videos=newVideos;
      result.dispatchUpdatesTo(this);
    }

    private void sort(boolean checked) {

(from Java8/VideoLambda/app/src/main/java/com/commonsware/android/recyclerview/videolist/MainActivity.java)

The new list of videos is the newVideos parameter to sortAndApply(). Based on the sortAscending value, we use Collections.sort() to sort the newVideos list… using a Java 8 lambda expression. Lambda expressions are covered elsewhere in the book. However, we could just sort using an anonymous inner class implementation of Comparator, such as this for sorting in ascending order:


Collections.sort(temp, new Comparator<Video>() {
  @Override
  public int compare(Video one, Video two) {
    return(one.compareTo(two));
  }
});

Given the newly-sorted list, we create an instance of a DiffUtil.Callback object, called SimpleCallback. We will see its implementation shortly. Its role is to help DiffUtil calculate the nature of the changes to list of videos, comparing what is in the RecyclerView now (videos) with what we want the RecyclerView to show (newVideos).

That DiffUtil.Callback object is passed to the calculateDiff() method on DiffUtil. The second parameter — true — indicates whether or not objects in the list may have moved. Sometimes, we know that the updates do not move objects around in the list, but merely insert new ones or remove existing ones. In such cases, calculateDiff() can optimize its tracking algorithm to run more efficiently. In our case, we most definitely are changing the positions of existing objects, and therefore we need to pass true.

In principle, calculateDiff() should be called on a background thread. For long lists and complicated comparisons, calculateDiff() could take long enough that you might exhibit some jank by taking up too much time on the main application thread. In this case, the comparisons are cheap, and hopefully you do not have too many videos on your test device.

The result of calculateDiff() is a DiffUtil.DiffResult object. After updating our video field to be the sorted newVideos collection, we call dispatchUpdatesTo() on the DiffResult, to have it apply the changes to the VideoAdapter. We need to update videos first because dispatchUpdatesTo() may trigger fresh onBindViewHolder() calls, and we need to make sure that we are using the newly-sorted list for those.

The SimpleCallback

SimpleCallback, as the name suggests, is a naive implementation of DiffUtil.Callback:

package com.commonsware.android.recyclerview.videolist;

import android.support.v7.util.DiffUtil;
import java.util.ArrayList;

class SimpleCallback<T extends Comparable> extends DiffUtil.Callback {
  private final ArrayList<T> oldItems;
  private final ArrayList<T> newItems;

  public SimpleCallback(ArrayList<T> oldItems,
                        ArrayList<T> newItems) {
    this.oldItems=oldItems;
    this.newItems=newItems;
  }

  @Override
  public int getOldListSize() {
    return(oldItems.size());
  }

  @Override
  public int getNewListSize() {
    return(newItems.size());
  }

  @Override
  public boolean areItemsTheSame(int oldItemPosition,
                                 int newItemPosition) {
    return(oldItems.get(oldItemPosition)
      .equals(newItems.get(newItemPosition)));
  }

  @Override
  public boolean areContentsTheSame(int oldItemPosition,
                                    int newItemPosition) {
    return(oldItems.get(oldItemPosition)
      .compareTo(newItems.get(newItemPosition))==0);
  }
}

(from Java8/VideoLambda/app/src/main/java/com/commonsware/android/recyclerview/videolist/SimpleCallback.java)

Any DiffUtil.Callback needs to implement four key abstract methods:

Those latter two methods basically fill the same roles as do methods of the same names on a SortedList.Callback.

In the case of SimpleCallback, areItemsTheSame() uses equals(). Since Video implements equals() to compare the video Uri values, areItemsTheSame() will return true if the two Video objects point to the same video.

areContentsTheSame() leverages compareTo(), which compares the titles of the videos. If compareTo() return 0, the titles are the same, and so areContentsTheSame() returns true to indicate that the visual representation of the videos is the same. This might not actually be the case, as we are showing the video thumbnails in the rows, and so it is possible that we have two videos that have the same title but different thumbnails. In that case, DiffUtil might not cause RecyclerView to redraw one or the other row. An alternative approach would be to have areContentsTheSame() simply return the value of areItemsTheSame(). This covers the thumbnails issue, at the expense of possibly doing some unnecessary shuffling of RecyclerView items, for cases where we have two videos with identical titles and thumbnails. It will be up to you, in your own app, to determine the best implementation of areContentsTheSame() based on your UI.

The March of the Libraries

By this point in time, you may be wailing in anguish and rending your garments over how much is involved in getting RecyclerView going.

(pro tip: do not rend your garments in public, to avoid running afoul of indecency laws)

There is little doubt that RecyclerView is the epitome of “some assembly required”. However, other developers have come to the forefront with libraries that can help fill in these gaps without you having to roll all the code yourself.

Note that the author has not tried many of these libraries, and listing them here neither is an endorsement of these libraries nor a knock on any libraries not listed here.

DynamicRecyclerView

The DynamicRecyclerView library offers:

Advanced RecyclerView

The Advanced RecyclerView library offers its own drag-and-drop and swipe-to-dismiss implementations. Rather than using OnItemTouchListener implementations, you implement certain interfaces on your RecyclerView.Adapter and RecyclerView.ViewHolder classes to support drag-and-drop and/or swipe-to-dismiss, plus work with “manager” classes to tie the support together.

It also supports an expandable item, where clicking on the item expands or collapses a set of views vertically beneath the item, offering an approach for using RecyclerView to replace ExpandableListView. And, it has a few item decorators, including a basic divider implementation.

SuperRecyclerView

The SuperRecyclerView library has its own swipe-to-dismiss implementation. It also supports the “swipe layout” pattern, where a horizontal swipe gesture slides out the main view and uncovers a set of contextual operations on the item.

It also offers:

However, this library requires that you inherit from a custom RecyclerView subclass.

FlexibleDivider

The FlexibleDivider library does just one thing: provides dividers. However, it offers deep support for dividers, where you can easily control all sorts of aspects, from color and width to margins and path effects (e.g., dashed lines versus solid lines).

The RecyclerView/FlexDividerList sample project is a clone of the ManualDividerList sample from earlier in the chapter, where the dividers are now provided by the FlexibleDivider library, which is loaded via the build.gradle file:

dependencies {
    compile 'com.android.support:recyclerview-v7:22.2.0'
    compile 'com.yqritc:recyclerview-flexibledivider:1.0.1'
}

(from RecyclerView/FlexDividerList/app/build.gradle)

Then, we use the library’s HorizontalDividerItemDecoration to set up our dividers:

  @Override
  public void onCreate(Bundle icicle) {
    super.onCreate(icicle);

    setLayoutManager(new LinearLayoutManager(this));

    RecyclerView.ItemDecoration decor=
        new HorizontalDividerItemDecoration.Builder(this)
          .color(getResources().getColor(R.color.primary))
          .build();

    getRecyclerView().addItemDecoration(decor);
    setAdapter(new IconicAdapter());
  }

(from RecyclerView/FlexDividerList/app/src/main/java/com/commonsware/android/recyclerview/flexdivider/MainActivity.java)

The results are a solid blue divider:

FlexDividerList RecyclerView Demo
Figure 557: FlexDividerList RecyclerView Demo

Of course, through the library, you can change a lot more about the divider than just its color, through its builder-style API.

Expandable Rows

A common pattern in vertically-scrolling lists is to have the rows expand and contract when clicked. This allows you to have more information inline in the list, without always taking up all of the vertical space that the information might require.

This might not sound very hard: just toggle the visibility of some widgets, perhaps using an animation.

However, what we really want is that when a row is expanded, that the entirety of the expanded row is visible, assuming that there is sufficient screen space for it. Otherwise, if the user happens to expand some row at the bottom of the list, the user might not realize that more information is available off the bottom of the screen.

Making this work requires knowing where the row is in the list, how much space will be required when it is expanded, whether the expanded row will fit given the RecyclerView size, and how to scroll the RecyclerView to make the row fit if needed.

That sounds complicated.

And so, we turn to a library: the ExpandableLayout provided by com.github.SilenceDut:ExpandableLayout. This library is demonstrated in the RecyclerView/ExpandableRow sample project. However, this sample project makes extensive use of the data binding framework, so you may wish to read that chapter before continuing with this section.

This app is another “list the recent questions on Stack Overflow” apps that have been profiled elsewhere in the book, starting with the chapter on Internet access. In this case, we are using a RecyclerView for the list, with the data binding framework populating the rows.

A Stack Overflow question has lots of possible pieces of data, far more than we would want to display in a RecyclerView row. Even showing a subset of this information would make for a really long list, as each row would be fairly large. So, instead, we will use ExpandableLayout to show the title, owner’s avatar, and question score all the time and show the tags, view count, and answer count after the user taps on a row. Since we are “stealing” the click event to expand and collapse the row, we cannot use it for anything else, so we also want a “View” button in the expanded area, to allow the user to view the Stack Overflow question on the Stack Overflow Web site.

Our build.gradle file pulls in libraries for accessing the Stack Exchange API (Retrofit and Picasso), libraries for displaying the results (recyclerview-v7 and cardview-v7), plus ExpandableLayout:

apply plugin: 'com.android.application'

repositories {
    maven { url "https://jitpack.io" }
}

dependencies {
    compile 'com.squareup.picasso:picasso:2.5.2'
    compile 'com.squareup.retrofit:retrofit:1.9.0'
    compile 'com.android.support:recyclerview-v7:24.1.1'
    compile 'com.android.support:cardview-v7:24.1.1'
    compile 'com.github.SilenceDut:ExpandableLayout:1.2.0'
}

android {
    compileSdkVersion 24
    buildToolsVersion "24.0.1"

    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 24
        versionCode 1
        versionName "1.0"
        applicationId "com.commonsware.android.databind.expandable"
    }

    dataBinding {
        enabled = true
    }
}


(from RecyclerView/ExpandableRow/app/build.gradle)

An ExpandableLayout contains two children. The first child will be shown all the time, while the second child represents the additional content to be shown when the ExpandableLayout is expanded. So, our res/layout/row.xml resource contains an ExpandableLayout, wrapping around the desired UI. The ExpandableLayout itself is wrapped in a CardView for formatting, and the whole thing is inside a <layout> for the data binding framework:

<?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">

  <data>

    <import type="android.text.Html" />

    <import type="android.text.TextUtils" />

    <variable
      name="question"
      type="com.commonsware.android.databind.basic.Question" />

    <variable
      name="controller"
      type="com.commonsware.android.databind.basic.QuestionController" />
  </data>

  <android.support.v7.widget.CardView
    xmlns:cardview="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="4dp"
    cardview:cardCornerRadius="4dp">

    <com.silencedut.expandablelayout.ExpandableLayout
      android:id="@+id/row_content"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:background="?android:attr/selectableItemBackground"
      android:onTouch="@{controller::onTouch}"
      app:expWithParentScroll="true">

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

        <ImageView
          android:id="@+id/icon"
          android:layout_width="@dimen/icon"
          android:layout_height="@dimen/icon"
          android:layout_gravity="center_vertical"
          android:contentDescription="@string/icon"
          android:padding="8dip"
          app:error="@{@drawable/owner_error}"
          app:imageUrl="@{question.owner.profileImage}"
          app:placeholder="@{@drawable/owner_placeholder}" />

        <TextView
          android:id="@+id/title"
          android:layout_width="0dp"
          android:layout_height="wrap_content"
          android:layout_gravity="left|center_vertical"
          android:layout_weight="1"
          android:text="@{Html.fromHtml(question.title)}"
          android:textSize="20sp" />

        <TextView
          android:id="@+id/score"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:layout_gravity="center_vertical"
          android:layout_marginLeft="8dp"
          android:layout_marginRight="8dp"
          android:text="@{Integer.toString(question.score)}"
          android:textSize="40sp"
          android:textStyle="bold" />

      </LinearLayout>

      <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="8dp">

        <LinearLayout
          android:layout_width="0dp"
          android:layout_height="wrap_content"
          android:layout_weight="1"
          android:orientation="vertical">

          <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:ellipsize="end"
            android:maxLines="1"
            android:text='@{@string/tags+" "+TextUtils.join(", ", question.tags)}' />

          <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text='@{@string/views+" "+question.viewCount}' />

          <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text='@{@string/answers+" "+question.answerCount}' />

        </LinearLayout>

        <Button
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:onClick="@{()->controller.showQuestion(context, question)}"
          android:text="@string/btn_view" />
      </LinearLayout>
    </com.silencedut.expandablelayout.ExpandableLayout>
  </android.support.v7.widget.CardView>
</layout>

(from RecyclerView/ExpandableRow/app/src/main/res/layout/row.xml)

We do not need to do anything to teach ExpandableLayout to expand and collapse based upon click events, as that is built into the class. However, we do opt into one optional feature, via app:expWithParentScroll="true". This indicates that we want to scroll the parent (here, referring to the RecyclerView) to allow the expanded ExpandableLayout to be fully visible where possible.

When the activity is launched, the rows are collapsed:

Expandable RecyclerView Rows, Collapsed
Figure 558: Expandable RecyclerView Rows, Collapsed

Tapping on one expands it to show the rest of the content, including scrolling the RecyclerView so it is visible:

Expandable RecyclerView Rows, One Row Expanded