blog/content/posts/recyclerview-basics.md
2025-12-12 15:30:04 +11:00

1175 lines
36 KiB
Markdown

+++
template = "article.html"
title = "RecyclerView basics"
date = 2015-01-18T19:37:00+01:00
description = "An introduction to RecyclerView in Android, covering adapters, view holders, and layout managers for efficient list displays."
[taxonomies]
tags = ["android"]
+++
## Introduction to RecyclerView
`RecyclerView` has been introduced with Android 5, in the support-v7 package. It
allows to display a collection of items in an arbitrary disposition (think of a
`ListView`, but much more flexible). As the name of the support package
indicates, it's available from the API level 7 (Android 2.1).
Its name comes from the way it works: when an item is hidden, instead of being
destroyed and a new one being created for each newly displayed item, hidden ones
are _recycled_: they are reused, with new data bound on them.
<!--more-->
A `RecyclerView` is split into 6 main components:
- an `Adapter`, providing data (similar to `ListView`'s)
- an `ItemAnimator`, responsible on animations to play when items are
modified, added, removed or moved
- an `ItemDecoration`, which can add drawings or change the layout of an item
(e.g. adding dividers)
- a `LayoutManager`, which specifies how items are laid out (grid, list…)
- a `ViewHolder`, the base class for each item's view
- the `RecyclerView` itself, binding everything together
Default implementations are bundled in the support-v7 package for some of these
components. You have an `ItemAnimator` and three `LayoutManager`s (a linear one,
similar to a `ListView`, a static grid, and a staggered grid). The
`RecyclerView` doesn't need to be modified, and the `ItemDecoration` is
optional. This leaves us the `Adapter` and the `ViewHolder`.
## Display a RecyclerView
### 1. Prepare your project
To use a `RecyclerView`, you need a specific module of the support-v7 package.
If you use Gradle, add to your dependencies:
`compile 'com.android.support:recyclerview-v7:21.0.3'`
This post will also use `CardView`s, so we reference them too:
`compile 'com.android.support:cardview-v7:21.0.3'`
That's it!
### 2. The base item
We'll write a very simple list, containing items with a title and a subtitle.
{{ filename(body="Item.java") }}
```java
public class Item {
private String title;
private String subtitle;
Item(String title, String subtitle) {
this.title = "title;"
this.subtitle = "subtitle;"
}
public String getTitle() {
return title;
}
public String getSubtitle() {
return subtitle;
}
}
```
### 3. Item layout
As mentionned earlier, our items will be displayed on a `CardView`. A `CardView`
is just a `FrameLayout` with some decorations, hence having two `TextView`s to
display is pretty simple:
{{ filename(body="layout/item.xml") }}
```xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:contentPadding="8dp"
app:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
style="@style/Base.TextAppearance.AppCompat.Headline" />
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
style="@style/Base.TextAppearance.AppCompat.Subhead" />
</LinearLayout>
</android.support.v7.widget.CardView>
```
### 4. The adapter
The first step is to define our `ViewHolder` class. It must extend
`RecyclerView.ViewHolder`, and should store references to the views you'll need
when binding your data on the holder. Here, we have our two `TextView`s:
{{ filename(body="Adapter.java") }}
```java
public class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> {
@SuppressWarnings("unused")
private static final String TAG = Adapter.class.getSimpleName();
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView title;
TextView subtitle;
public ViewHolder(View itemView) {
super(itemView);
title = "(TextView) itemView.findViewById(R.id.title);"
subtitle = "(TextView) itemView.findViewById(R.id.subtitle);"
}
}
}
```
Now, what's the simplest way to store a collection of objects? Well, a
`Collection`. Sometimes, even Java gets it right. For the simplicity of this
example, we'll store our items in an `ArrayList` in our `Adapter`:
{{ filename(body="Adapter.java") }}
```java
public class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> {
@SuppressWarnings("unused")
private static final String TAG = Adapter.class.getSimpleName();
private static final int ITEM_COUNT = 50;
private List<Item> items;
public Adapter() {
super();
// Create some items
items = new ArrayList<>();
for (int i = 0; i < ITEM_COUNT; ++i) {
items.add(new Item("Item " + i, "This is the item number " + i));
}
}
// ViewHolder definition omitted
}
```
Then we should implement the actual `RecyclerView.Adapter` methods:
- `onCreateViewHolder(ViewGroup parent, int viewType)` should create the view,
and return a matching `ViewHolder`,
- `onBindViewHolder(ViewHolder holder, int position)` should fill the
`ViewHolder` with data from item at position `position`,
- `getItemCount()` should give the number of elements in the `Adapter`
underlying data collection.
The implementation of these methods is pretty straightforward in our case:
{{ filename(body="Adapter.java") }}
```java
public class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> {
// Attributes and constructor omitted
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false);
return new ViewHolder(v);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
final Item item = items.get(position);
holder.title.setText(item.getTitle());
holder.subtitle.setText(item.getSubtitle());
}
@Override
public int getItemCount() {
return items.size();
}
// ViewHolder definition omitted
}
```
### 5. Bind everything together
We defined everything we needed. Now, it's time to give everything to a
`RecyclerView`, and watch the magic happen! First step, add a `RecyclerView` to
an `Activity`:
{{ filename(body="layout/activity_main.xml") }}
```xml
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
```
We will use the simplest layout manager for now: `LinearLayoutManager`. We will
also use the `DefaultItemAnimator`.
{{ filename(body="MainActivity.java") }}
```java
public class MainActivity extends ActionBarActivity {
@SuppressWarnings("unused")
private static final String TAG = MainActivity.class.getSimpleName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
recyclerView.setAdapter(new Adapter());
recyclerView.setItemAnimator(new DefaultItemAnimator());
recyclerView.setLayoutManager(new LinearLayoutManager(this));
}
}
```
Compile, run, and…
{{ img(src="/images/articles/recyclerview-basics/recyclerview-1.png", caption="Our first RecyclerView") }}
Now that we have a basic `RecyclerView` displayed, let's see what we can do with
it.
## Different kind of views on the same RecyclerView
Let's say you have two types of items you want to display. For example, you
display a remote music collection, and only some albums are available offline.
You can do specific actions on them, and display some specific information too.
For our example, we will add an `active` property to our items.
{{ filename(body="Item.java") }}
```java
public class Item {
private String title;
private String subtitle;
private boolean active;
Item(String title, String subtitle, boolean active) {
this.title = "title;"
this.subtitle = "subtitle;"
this.active = active;
}
public String getTitle() {
return title;
}
public String getSubtitle() {
return subtitle;
}
public boolean isActive() {
return active;
}
}
```
We change our items creation to have some active ones, and change the subtitle
to add an active/inactive indication:
{{ filename(body="Adapter.java") }}
```java
public class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> {
// Attributes omitted
public Adapter() {
super();
// Create some items
Random random = new Random();
items = new ArrayList<>();
for (int i = 0; i < ITEM_COUNT; ++i) {
items.add(new Item("Item " + i, "This is the item number " + i, random.nextBoolean()));
}
}
// onCreateViewHolder omitted
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
final Item item = items.get(position);
holder.title.setText(item.getTitle());
holder.subtitle.setText(item.getSubtitle() + ", which is " + (item.isActive() ? "active" : "inactive"));
}
// …
}
```
Displaying a different string is a good start, but we need more. When we were
writing the adapter, you may have noticed an argument that we didn't use in
`onCreateViewHolder(ViewGroup parent, int viewType)`. This `viewType` is here to
achieve exactly what we need: alter the `ViewHolder` creation. We must tell the
`Adapter` how to determine the type of an item. We do this by overriding a new
method, `getItemViewType(int position)`:
{{ filename(body="Adapter.java") }}
```java
public class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> {
// …
private static final int TYPE_INACTIVE = 0;
private static final int TYPE_ACTIVE = 1;
// …
@Override
public int getItemViewType(int position) {
final Item item = items.get(position);
return item.isActive() ? TYPE_ACTIVE : TYPE_INACTIVE;
}
// …
}
```
Now, you have multiple possibilities depending on your needs: create a different
`ViewHolder` for each view type, inflate a different layout but use the same
`ViewHolder`… To keep things simple here, we will use the same `ViewHolder`, but
a different layout. We will keep using the present layout for inactive items,
and create a new for active ones:
{{ filename(body="layout/item_active.xml") }}
```xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:contentPadding="8dp"
app:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:textColor="@color/material_deep_teal_500"
style="@style/Base.TextAppearance.AppCompat.Headline" />
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:textColor="@color/material_blue_grey_900"
style="@style/Base.TextAppearance.AppCompat.Subhead" />
</LinearLayout>
</android.support.v7.widget.CardView>
```
Last but not least: we have to inflate a different layout depending on
`viewType` in `onCreateViewHolder`:
{{ filename(body="Adapter.java") }}
```java
public class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> {
// …
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final int layout = viewType == TYPE_INACTIVE ? R.layout.item : R.layout.item_active;
View v = LayoutInflater.from(parent.getContext()).inflate(layout, parent, false);
return new ViewHolder(v);
}
// …
}
```
Now, we can distinguish active items from inactive ones:
{{ img(src="/images/articles/recyclerview-basics/recyclerview-active.png", caption="Displaying different layouts in the same RecyclerView") }}
## Layout managers
### LinearLayoutManager
This is the one we used. This manager replicates the `ListView` behaviour. It
takes up to three parameters: a `Context` (mandatory), an orientation (vertical,
which is the default, or horizontal), and a `boolean` allowing to reverse the
layout.
This is what happens with a reversed, horizontal linear layout manager:
{{ img(src="/images/articles/recyclerview-basics/recyclerview-linear-horizontal-reversed.png", caption="LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, true)") }}
Note that when `reverseLayout` is set to `true`, the view is automaticaly
displayed at the end without the need for scrolling.
### GridLayoutManager
This one is similar to the `GridView`. It takes up to four parameters: a
`Context` (mandatory), a span count (mandatory), an orientation (vertical, which
is the default too, or horizontal), and a `reverseLayout` option.
Here's a `GridLayoutManager` with a span count set to 3, vertical orientation,
not reversed:
{{ img(src="/images/articles/recyclerview-basics/recyclerview-grid-3-vertical.png", caption="GridLayoutManager(this, 3)") }}
Note that the `reverseLayout` can be surprising when working with a grid. It
reverses the layout in the direction you gave it, but not on the other one. With
a vertical orientation, the items are reversed in vertical order, but not in
horizontal:
{{ img(src="/images/articles/recyclerview-basics/recyclerview-grid-3-vertical-reversed.png", caption="GridLayoutManager(this, 3, GridLayoutManager.VERTICAL, true)") }}
### StaggeredGridLayoutManager
The `StaggeredGridLayoutManager` is a `GridLayoutManager` on steroids. And as
steroids may have bad effects on your body, they have bad effects on the
`StaggeredGridLayoutManager`, which at the time of writing of this post
(support-v7 21.0.0.3), has some pretty
[annoying](https://code.google.com/p/android/issues/detail?id=93156)
[bugs](https://code.google.com/p/android/issues/detail?id=93711). It seems
that this layout manager was finished in a hurry, and isn't in par with the
other ones. We can note this in its parameters: it doesn't need a `Context`, but
the orientation is mandatory. It also needs a span count, like the
`GridLayoutManager`. The code allows to reverse it, but there's no parameter in
the constructor to do this.
This layout is a grid, with a fixed span. However, we can have items spanning on
the whole line or column. Let's see how it works. Using our active/inactive
items from earlier, we'll make active items fully spanning. This is done in the
`Adapter`, when binding an item.
{{ filename(body="Adapter.java") }}
```java
public class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> {
// …
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
final Item item = items.get(position);
holder.title.setText(item.getTitle());
holder.subtitle.setText(item.getSubtitle() + ", which is " + (item.isActive() ? "active" : "inactive"));
// Span the item if active
final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
if (lp instanceof StaggeredGridLayoutManager.LayoutParams) {
StaggeredGridLayoutManager.LayoutParams sglp = (StaggeredGridLayoutManager.LayoutParams) lp;
sglp.setFullSpan(item.isActive());
holder.itemView.setLayoutParams(sglp);
}
}
// …
}
```
Here, we must check if the layout manager is a `StaggeredGridLayoutManager`
(line 13). If it's the case, we can modify the layout params accordingly.
The mandatory screenshot:
{{ img(src="/images/articles/recyclerview-basics/recyclerview-staggered-grid.png", caption="StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL)") }}
## Respond to clicks on items
The `RecyclerView` doesn't provide any high-level API to handle item clicks as
the `ListView` does. However, it's still pretty simple to achieve.
Let's think about it: we want to listen to click and long-click events on each
item. Each item is represented by a `ViewHolder`. Each `ViewHolder` is
initialized from its root `View`. Well, that's perfect: `View` as callbacks for
click and long-click events. The last thing we need is mapping each `ViewHolder`
to its position. `RecyclerView.ViewHolder` does all the work for us: the method
`getPosition()` returns the position of the currently bound item.
{{ filename(body="Adapter.java") }}
```java
public class Adapter extends RecyclerView.Adapter<Adapter.ViewHolder> {
// …
public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
@SuppressWarnings("unused")
private static final String TAG = ViewHolder.class.getSimpleName();
TextView title;
TextView subtitle;
public ViewHolder(View itemView) {
super(itemView);
title = "(TextView) itemView.findViewById(R.id.title);"
subtitle = "(TextView) itemView.findViewById(R.id.subtitle);"
itemView.setOnClickListener(this);
itemView.setOnLongClickListener(this);
}
@Override
public void onClick(View v) {
Log.d(TAG, "Item clicked at position " + getPosition());
}
@Override
public boolean onLongClick(View v) {
Log.d(TAG, "Item long-clicked at position " + getPosition());
return true;
}
}
}
```
## Selection handling
A frequent pattern when long-clicking on a list item is to trigger a selection
mode. Once again, the `RecyclerView` doesn't help us with this, but it's pretty
simple to do. It can be split in three steps:
- maintain a selection state,
- update the view of the selected items,
- start the selection mode.
To illustrate this part, we will add a way to select items, then remove this
selection.
### 1. Selection state
We need to modify our `Adapter` to keep a list of selected elements. Here's a
list of what the `Adapter` has to provide:
- list of selected elements,
- change selection state of a given element.
We can add bonus methods:
- check if a specific element is selected,
- clear the whole selection,
- give the number of selected elements.
I didn't chose these methods randomly, we will need them for the next parts.
We can notice one thing with these five methods: none of them is
`Item`-specific. We can write them in a generic way, and reuse our `Adapter`
behaviour.
Once we got all of this prepared, the code is pretty simple:
{{ filename(body="SelectableAdapter.java") }}
```java
public abstract class SelectableAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
@SuppressWarnings("unused")
private static final String TAG = SelectableAdapter.class.getSimpleName();
private SparseBooleanArray selectedItems;
public SelectableAdapter() {
selectedItems = new SparseBooleanArray();
}
/**
* Indicates if the item at position position is selected
* @param position Position of the item to check
* @return true if the item is selected, false otherwise
*/
public boolean isSelected(int position) {
return getSelectedItems().contains(position);
}
/**
* Toggle the selection status of the item at a given position
* @param position Position of the item to toggle the selection status for
*/
public void toggleSelection(int position) {
if (selectedItems.get(position, false)) {
selectedItems.delete(position);
} else {
selectedItems.put(position, true);
}
notifyItemChanged(position);
}
/**
* Clear the selection status for all items
*/
public void clearSelection() {
List<Integer> selection = getSelectedItems();
selectedItems.clear();
for (Integer i : selection) {
notifyItemChanged(i);
}
}
/**
* Count the selected items
* @return Selected items count
*/
public int getSelectedItemCount() {
return selectedItems.size();
}
/**
* Indicates the list of selected items
* @return List of selected items ids
*/
public List<Integer> getSelectedItems() {
List<Integer> items = new ArrayList<>(selectedItems.size());
for (int i = 0; i < selectedItems.size(); ++i) {
items.add(selectedItems.keyAt(i));
}
return items;
}
}
```
Last change needed: our `Adapter` must extend `SelectableAdapter`. Its code
doesn't change:
{{ filename(body="Adapter.java") }}
```java
public class Adapter extends SelectableAdapter<Adapter.ViewHolder> {
// …
}
```
### 2. Update the item views
To notify the user that an item is selected, we often see a colored overlay on
the selected views. That's what we'll do. On both `item.xml` and
`item_active.xml`, we add an invisible, colored `View`. As this `View` should
fill the whole `CardView` space, we need to make some change in the layout
(move the padding to the inner `LinearLayout` instead of the `CardView`). The
color should be transparent.
We can also add a nice touch feedback using the framework's
`selectableItemBackground` as a foreground on the `CardView`. On Android 5, this
background displays a ripple, and a simple grey overlay on previous Android
versions.
{{ filename(body="item.xml (same changes go for item_active.xml)") }}
```xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foreground="?android:attr/selectableItemBackground"
app:cardUseCompatPadding="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="8dp" >
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
style="@style/Base.TextAppearance.AppCompat.Headline" />
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
style="@style/Base.TextAppearance.AppCompat.Subhead" />
</LinearLayout>
<View
android:id="@+id/selected_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/selected_overlay"
android:visibility="invisible" />
</android.support.v7.widget.CardView>
```
The next step is to decide when to display this overlay. The right place to do
it seems pretty obvious: `Adapter`'s `onBindViewHolder()`. We also need to add a
reference to the overlay in the`ViewHolder`.
{{ filename(body="Adapter.java") }}
```java
public class Adapter extends SelectableAdapter<Adapter.ViewHolder> {
// …
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
final Item item = items.get(position);
holder.title.setText(item.getTitle());
holder.subtitle.setText(item.getSubtitle() + ", which is " + (item.isActive() ? "active" : "inactive"));
// Span the item if active
final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
if (lp instanceof StaggeredGridLayoutManager.LayoutParams) {
StaggeredGridLayoutManager.LayoutParams sglp = (StaggeredGridLayoutManager.LayoutParams) lp;
sglp.setFullSpan(item.isActive());
holder.itemView.setLayoutParams(sglp);
}
// Highlight the item if it's selected
holder.selectedOverlay.setVisibility(isSelected(position) ? View.VISIBLE : View.INVISIBLE);
}
// …
public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
@SuppressWarnings("unused")
private static final String TAG = ViewHolder.class.getSimpleName();
TextView title;
TextView subtitle;
View selectedOverlay;
public ViewHolder(View itemView) {
super(itemView);
title = "(TextView) itemView.findViewById(R.id.title);"
subtitle = "(TextView) itemView.findViewById(R.id.subtitle);"
selectedOverlay = itemView.findViewById(R.id.selected_overlay);
itemView.setOnClickListener(this);
itemView.setOnLongClickListener(this);
}
// …
}
}
```
### 3. Start the selection mode
This last step will be a little more complex, but nothing really hard. First, we
need to route click and long-click events back to our `Activity`. To achieve
this, our `ViewHolder`s will expose a listener. We will pass it through the
`Adapter`:
{{ filename(body="Adapter.java") }}
```java
public class Adapter extends SelectableAdapter<Adapter.ViewHolder> {
// …
private ViewHolder.ClickListener clickListener;
public Adapter(ViewHolder.ClickListener clickListener) {
super();
this.clickListener = clickListener;
// Create some items
Random random = new Random();
items = new ArrayList<>();
for (int i = 0; i < ITEM_COUNT; ++i) {
items.add(new Item("Item " + i, "This is the item number " + i, random.nextBoolean()));
}
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final int layout = viewType == TYPE_INACTIVE ? R.layout.item : R.layout.item_active;
View v = LayoutInflater.from(parent.getContext()).inflate(layout, parent, false);
return new ViewHolder(v, clickListener);
}
// …
public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
@SuppressWarnings("unused")
private static final String TAG = ViewHolder.class.getSimpleName();
TextView title;
TextView subtitle;
View selectedOverlay;
private ClickListener listener;
public ViewHolder(View itemView, ClickListener listener) {
super(itemView);
title = "(TextView) itemView.findViewById(R.id.title);"
subtitle = "(TextView) itemView.findViewById(R.id.subtitle);"
selectedOverlay = itemView.findViewById(R.id.selected_overlay);
this.listener = listener;
itemView.setOnClickListener(this);
itemView.setOnLongClickListener(this);
}
@Override
public void onClick(View v) {
if (listener != null) {
listener.onItemClicked(getPosition());
}
}
@Override
public boolean onLongClick(View v) {
if (listener != null) {
return listener.onItemLongClicked(getPosition());
}
return false;
}
public interface ClickListener {
public void onItemClicked(int position);
public boolean onItemLongClicked(int position);
}
}
}
```
To distinguish the selection mode from the normal mode, we will use an
`ActionMode`, allowing us to display a different `ActionBar` while the selection
is active. To achieve this, we have to implement a basic `ActionMode.Callback`.
For simplicity, our `Activity` will implement this interface in an inner class.
It will also implement our new click listener interface,
`Adapter.ViewHolder.ClickListener`. We will need access to our `Adapter` from
the callback class, so we move it as an attribute in the `Activity`.
Let's summarize the click handlers logic. On a click, if there's no current
selection, we do nothing. If there is something selected, we toggle the
selection state of the clicked item. On a long click, if there is no current
selection, we start the selection and toggle the selection state of the clicked
item. If there is already something selected, we toggle the selection state too.
Our `MainActivity` becomes a little more complex:
{{ filename(body="MainActivity.java") }}
```java
public class MainActivity extends ActionBarActivity implements Adapter.ViewHolder.ClickListener {
@SuppressWarnings("unused")
private static final String TAG = MainActivity.class.getSimpleName();
private Adapter adapter;
private ActionModeCallback actionModeCallback = new ActionModeCallback();
private ActionMode actionMode;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
adapter = new Adapter(this);
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
recyclerView.setAdapter(adapter);
recyclerView.setItemAnimator(new DefaultItemAnimator());
recyclerView.setLayoutManager(new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL));
}
@Override
public void onItemClicked(int position) {
if (actionMode != null) {
toggleSelection(position);
}
}
@Override
public boolean onItemLongClicked(int position) {
if (actionMode == null) {
actionMode = startSupportActionMode(actionModeCallback);
}
toggleSelection(position);
return true;
}
/**
* Toggle the selection state of an item.
*
* If the item was the last one in the selection and is unselected, the
* selection is stopped.
* Note that the selection must already be started (actionMode must not be
* null).
*
* @param position Position of the item to toggle the selection state
*/
private void toggleSelection(int position) {
adapter.toggleSelection(position);
int count = adapter.getSelectedItemCount();
if (count == 0) {
actionMode.finish();
} else {
actionMode.setTitle(String.valueOf(count));
actionMode.invalidate();
}
}
private class ActionModeCallback implements ActionMode.Callback {
@SuppressWarnings("unused")
private final String TAG = ActionModeCallback.class.getSimpleName();
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate (R.menu.selected_menu, menu);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_remove:
// TODO: actually remove items
Log.d(TAG, "menu_remove");
mode.finish();
return true;
default:
return false;
}
}
@Override
public void onDestroyActionMode(ActionMode mode) {
adapter.clearSelection();
actionMode = null;
}
}
}
```
And here is the result. The screenshot was taken while selecting the item "Item
5":
{{ img(src="/images/articles/recyclerview-basics/recyclerview-selection.png", caption="Selection mode") }}
We need to handle our "remove" event, which does nothing for now. Let's see how
to modify our dataset in the last part.
## Changing the dataset
In order to update the view, the `Adapter` must notify when a change occurs on
its data. On basic `ListView`s, adapters had a single method to achieve that:
`notifyDataSetChanged()`. However, this method is far from optimal: every view
must be refreshed, because we don't know exactly what changed. With the
`RecyclerView.Adapter`, we've got multiple methods:
- `notifyItemChanged(int position)`
- `notifyItemInserted(int position)`
- `notifyItemRemoved(int position)`
- `notifyItemMoved(int fromPosition, int toPosition)`
- `notifyItemRangeChanged(int positionStart, int itemCount)`
- `notifyItemRangeInserted(int positionStart, int itemCount)`
- `notifyItemRangeRemoved(int positionStart, int itemCount)`
- `notifyDataSetChanged()`
We can notify an item insertion, removal and change, same for a range of
items, an item move, or a full dataset change. Let's take the removal as an
example.
We will write two public methods in our `Adapter` to allow external classes to
remove either a single item, or a list of items. While the removal of a single
item is straightforward, we need to think a little more for the list.
If the user provides us with a list of `[5, 8, 9]` to remove, if we start by
removing the item 5, our list is an item shorter, _before_ 8 and 9. We should
remove `[5, 7, 7]` one after the other. We can handle that. But what happens if
the user provides `[8, 9, 5]`?
There's a pretty simple solution to this: sort our input list in the
reverse-order. This allows easy ranges detection too, which is something we will
need to make our calls to `notifyItemRangeRemoved()`.
{{ filename(body="Adapter.java") }}
```java
public class Adapter extends SelectableAdapter<Adapter.ViewHolder> {
// …
public void removeItem(int position) {
items.remove(position);
notifyItemRemoved(position);
}
public void removeItems(List<Integer> positions) {
// Reverse-sort the list
Collections.sort(positions, new Comparator<Integer>() {
@Override
public int compare(Integer lhs, Integer rhs) {
return rhs - lhs;
}
});
// Split the list in ranges
while (!positions.isEmpty()) {
if (positions.size() == 1) {
removeItem(positions.get(0));
positions.remove(0);
} else {
int count = 1;
while (positions.size() > count && positions.get(count).equals(positions.get(count - 1) - 1)) {
++count;
}
if (count == 1) {
removeItem(positions.get(0));
} else {
removeRange(positions.get(count - 1), count);
}
for (int i = 0; i < count; ++i) {
positions.remove(0);
}
}
}
}
private void removeRange(int positionStart, int itemCount) {
for (int i = 0; i < itemCount; ++i) {
items.remove(positionStart);
}
notifyItemRangeRemoved(positionStart, itemCount);
}
// …
}
```
And the final code chunk of this article: actually call these two methods. We
already set-up a "Remove" action in our contextual menu, we just need to call
`removeItems()` from it. To test the other one, let's say that a click on a view
will remove it:
{{ filename(body="MainActivity.java") }}
```java
public class MainActivity extends ActionBarActivity implements Adapter.ViewHolder.ClickListener {
// …
@Override
public void onItemClicked(int position) {
if (actionMode != null) {
toggleSelection(position);
} else {
adapter.removeItem(position);
}
}
// …
private class ActionModeCallback implements ActionMode.Callback {
// …
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_remove:
adapter.removeItems(adapter.getSelectedItems());
mode.finish();
return true;
default:
return false;
}
}
// …
}
}
```
And that's it. We can remove items. As a bonus from using the right notification
method instead of the generic `notifyDataSetChanged()`, we have a nice
animation!
{{ video(src="/images/articles/recyclerview-basics/recyclerview-remove.webm", caption="Removing items from the RecyclerView") }}
## Conclusion
While the `RecyclerView` is a little more difficult to set up than a `ListView`
or a `GridView`, it allows for an easier responsibility separation between the
view holders, the data source, the list itself… and a more polished
user-experience, thanks to the animations.
Regarding the performance, I think that in comparison to a well-used `ListView`,
implementing the `ViewHolder` pattern correctly, it should be pretty similar.
However, with the `RecyclerView`, you **have** to use the `ViewHolder` pattern.
It's not an option. And that's a good thing for both the user, who will have
better performance than without the `ViewHolder`, and for the developer, who
will have to write better code.
While the `RecyclerView` obviously still lacks some polish (yes, I'm looking at you,
`StaggeredGridLayoutManager`), it's perfectly usable right now, and is a
welcome addition in the SDK.
The full example code is available on
[GitHub](https://github.com/Kernald/recyclerview-sample). Note that the code has
been updated by [Shinil M S](https://github.com/shinilms12) (thanks again) since
the article was released, to follow the small API changes. The code written for
this article can be found at the tag
[article](https://github.com/Kernald/recyclerview-sample/tree/article).