Android swipe menu with RecyclerView

Artur Grzybowski
codeburst
Published in
9 min readJul 2, 2017

--

Android swipe menu with RecyclerView

Very simple solution to create swipe menu with RecyclerView without any additional libraries — using ItemTouchHelper Callback for swiping items and ItemDecoration for drawing on canvas.

Code from this tutorial is available on my github.

Create RecyclerView application

First, let’s create a tiny application which displays some cool data. To do this, we need to download some data eg. from the kaggle.com — let’s download Fifa 2017 players data set. It’s a lot of data — 17.6K rows (it will also show how RecyclerView is optimized). Unpack this database and save into assets/players.csv

If you are familiar with RecyclerView just go to the second chapter

RecyclerView in action

Before we start to implement RecyclerView we need to add few dependencies into the project:

compile 'com.android.support:appcompat-v7:25.3.1'
compile 'com.android.support:recyclerview-v7:25.3.1'
compile 'com.android.support:cardview-v7:25.3.1'

Next step is to add layout (activity_main.xml) for our MainActivity:

<?xml version="1.0" encoding="utf-8"?>
<!-- activity_main.xml -->
<FrameLayout
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="pl.fanfatal.swipecontrollerdemo.MainActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:clipToPadding="false"
android:overScrollMode="never" />

Our application is about to display players data so we need to create Player class with getter/setter methods:

// Player.java
package pl.fanfatal.swipecontrollerdemo;

public class Player {

private String name, nationality, club;
private Integer rating, age;

public String getName() { return name; }
public void setName(String name) { this.name = name; }

public String getNationality() { return nationality; }
public void setNationality(String nationality) { this.nationality = nationality; }

public String getClub() { return club; }
public void setClub(String club) { this.club = club; }

public Integer getRating() { return rating; }
public void setRating(Integer rating) { this.rating = rating; }

public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
}

Adapter for RecyclerView is the most important class here. We need to override 3 methods to make things works:

getItemCount — returns how many items we want to display
onCreateViewHolder — creates viewholder and inflate the view, it’s only called when the RecyclerView needs to create the new view
onBindViewHolder — is binding data into a viewholder, it’s called when the RecyclerView needs to fill proper data into a view

// PlayersDataAdapter.java
package pl.fanfatal.swipecontrollerdemo;

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

import java.util.List;

class PlayersDataAdapter extends RecyclerView.Adapter<PlayersDataAdapter.PlayerViewHolder> {
private List<Player> players;

public class PlayerViewHolder extends RecyclerView.ViewHolder {
private TextView name, nationality, club, rating, age;

public PlayerViewHolder(View view) {
super(view);
name = (TextView) view.findViewById(R.id.name);
nationality = (TextView) view.findViewById(R.id.nationality);
club = (TextView) view.findViewById(R.id.club);
rating = (TextView) view.findViewById(R.id.rating);
age = (TextView) view.findViewById(R.id.age);
}
}

public PlayersDataAdapter(List<Player> players) {
this.players = players;
}

@Override
public PlayerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.player_row, parent, false);

return new PlayerViewHolder(itemView);
}

@Override
public void onBindViewHolder(PlayerViewHolder holder, int position) {
Player player = players.get(position);
holder.name.setText(player.getName());
holder.nationality.setText(player.getNationality());
holder.club.setText(player.getClub());
holder.rating.setText(player.getRating().toString());
holder.age.setText(player.getAge().toString());
}

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

Then, we need to create simple player_row.xml using CardView to display all player data.

<?xml version="1.0" encoding="utf-8"?>
<!-- player_row.xml -->
<android.support.v7.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:clickable="true"
android:layout_margin="8dp"
android:background="?android:attr/selectableItemBackground">

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

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

<TextView
android:id="@+id/name"
android:textSize="16sp"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Robert Lewandowski"/>

<TextView
android:id="@+id/nationality"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="textEnd"
tools:text="Poland" />

</LinearLayout>

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

<TextView
android:id="@+id/club"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Bayern Munchen"/>

</LinearLayout>

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

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Age: "/>

<TextView
android:id="@+id/age"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="26"/>

<TextView
android:id="@+id/rating"
android:textSize="20sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="textEnd"
tools:text="90" />

</LinearLayout>

</LinearLayout>

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

Finaly it’s time for MainActivity class. In our example, we read CSV database and pass it into an adapter. Proper work of RecyclerView needs setting up two things: first — is mentioned above adapter, second — layout manager, it tells view how to display data. Here we use LinearLayout.

// MainActivity.java
package pl.fanfatal.swipecontrollerdemo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {

private PlayersDataAdapter mAdapter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

setPlayersDataAdapter();
setupRecyclerView();
}

private void setPlayersDataAdapter() {
List<Player> players = new ArrayList<>();
try {
InputStreamReader is = new InputStreamReader(getAssets().open("players.csv"));

BufferedReader reader = new BufferedReader(is);
reader.readLine();
String line;
String[] st;
while ((line = reader.readLine()) != null) {
st = line.split(",");
Player player = new Player();
player.setName(st[0]);
player.setNationality(st[1]);
player.setClub(st[4]);
player.setRating(Integer.parseInt(st[9]));
player.setAge(Integer.parseInt(st[14]));
players.add(player);
}
} catch (IOException e) {

}

mAdapter = new PlayersDataAdapter(players);
}

private void setupRecyclerView() {
RecyclerView recyclerView = (RecyclerView)findViewById(R.id.recyclerView);

recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
recyclerView.setAdapter(mAdapter);
}
}

Starting with swipe using ItemTouchHelper

OK, so we want to add some actions to our static view. Using ItemTouchHelper Callback we need to implement three methods to make this work.
getMovementFlags tells helper what kind of actions RecyclerView should handle — in our example just swiping left and right
onMove, onSwiped what to do on given actions — we can ignore it in our example

// SwipeController.java
package pl.fanfatal.swipecontrollerdemo;

import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper.Callback;

import static android.support.v7.widget.helper.ItemTouchHelper.*;

class SwipeController extends Callback {

@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
return makeMovementFlags(0, LEFT | RIGHT);
}

@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
return false;
}

@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {

}
}

Next, we need to attach helper to RecyclerView. To do this we are adding it to the MainActivity class using attachToRecyclerView method:

// MainActivity.java
SwipeController swipeController = new SwipeController();
ItemTouchHelper itemTouchhelper = new ItemTouchHelper(swipeController);
itemTouchhelper.attachToRecyclerView(recyclerView);

Now swipe should be enabled in our project.

RecyclerView with swipe

Of course, as you have noticed, we are able to swipe out the items — we have to block it somehow. For this, we need to override convertToAbsoluteDirection method in ItemTouchHelper.Callback:

// SwipeController.java
@Override
public int convertToAbsoluteDirection(int flags, int layoutDirection) {
if (swipeBack) {
swipeBack = false;
return 0;
}
return super.convertToAbsoluteDirection(flags, layoutDirection);
}

OK, so where the swipeBack came from? We just need to set onTouchListener on RecyclerView — and after finishing swiping we set swipeBack to true:

// SwipeController.java
@Override
public void onChildDraw(Canvas c,
RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder,
float dX, float dY,
int actionState, boolean isCurrentlyActive) {

if (actionState == ACTION_STATE_SWIPE) {
setTouchListener(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}

private void setTouchListener(Canvas c,
RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder,
float dX, float dY,
int actionState, boolean isCurrentlyActive) {

recyclerView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
swipeBack = event.getAction() == MotionEvent.ACTION_CANCEL || event.getAction() == MotionEvent.ACTION_UP;
return false;
}
});
}

After this we get:

swipeBack in action

Before we start drawing our buttons let’s create some logic for showing them. To be honest, this is the most important part in here and most difficult to explain, so I decided to move it into the separate chapter.

Swipe Controller with buttons state

First, create properties buttonShowedState to keep information about what kind of button has been shown— if any.

// SwipeController.java
enum ButtonsState {
GONE,
LEFT_VISIBLE,
RIGHT_VISIBLE
}
class SwipeController extends Callback {
private boolean swipeBack = false;
private ButtonsState buttonShowedState = ButtonsState.GONE;
private static final float buttonWidth = 300;
...
}

We need some condition to set properly our state — in our touch listener we check how much to the left (or right) user swiped the item. If enough, we change state to show buttons:

// SwipeController.java
private void setTouchListener(final Canvas c,
final RecyclerView recyclerView,
final RecyclerView.ViewHolder viewHolder,
final float dX, final float dY,
final int actionState, final boolean isCurrentlyActive) {
recyclerView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
swipeBack = event.getAction() == MotionEvent.ACTION_CANCEL || event.getAction() == MotionEvent.ACTION_UP;
if (swipeBack) {
if (dX < -buttonWidth) buttonShowedState = ButtonsState.RIGHT_VISIBLE;
else if (dX > buttonWidth) buttonShowedState = ButtonsState.LEFT_VISIBLE;


if (buttonShowedState != ButtonsState.GONE) {
setTouchDownListener(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
setItemsClickable(recyclerView, false);

}
}
return false;
}
});
}

Now, if buttonShowedState is different than GONE we need to overwrite touch listener and simulate click on RecyclerView . Why simulate? Because we could already have onclick listener on items, so we need to disable it because of glitches.

// SwipeController.java
private void setTouchDownListener(final Canvas c,
final RecyclerView recyclerView,
final RecyclerView.ViewHolder viewHolder,
final float dX, final float dY,
final int actionState, final boolean isCurrentlyActive) {
recyclerView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
setTouchUpListener(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
return false;
}
});
}

private void setTouchUpListener(final Canvas c,
final RecyclerView recyclerView,
final RecyclerView.ViewHolder viewHolder,
final float dX, final float dY,
final int actionState, final boolean isCurrentlyActive) {
recyclerView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
SwipeController.super.onChildDraw(c, recyclerView, viewHolder, 0F, dY, actionState, isCurrentlyActive);
recyclerView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return false;
}
});
setItemsClickable(recyclerView, true);
swipeBack = false;
buttonShowedState = ButtonsState.GONE;

}
return false;
}
});
}

private void setItemsClickable(RecyclerView recyclerView,
boolean isClickable) {
for (int i = 0; i < recyclerView.getChildCount(); ++i) {
recyclerView.getChildAt(i).setClickable(isClickable);
}
}
swipeBack with button states

So what happens here? After user clicks on RecyclerView we just reset state of our SwipeController and redraw everything back.

Let’s draw some buttons!

We have access to canvas in onChildDraw method so we can use it here. We need to assign buttonInstance property for future usage.

// SwipeController.java
@Override
public void onChildDraw(Canvas c,
RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder,
float dX, float dY,
int actionState, boolean isCurrentlyActive) {
// ...
drawButtons(c, viewHolder);
}
private void drawButtons(Canvas c, RecyclerView.ViewHolder viewHolder) {
float buttonWidthWithoutPadding = buttonWidth - 20;
float corners = 16;

View itemView = viewHolder.itemView;
Paint p = new Paint();

RectF leftButton = new RectF(itemView.getLeft(), itemView.getTop(), itemView.getLeft() + buttonWidthWithoutPadding, itemView.getBottom());
p.setColor(Color.BLUE);
c.drawRoundRect(leftButton, corners, corners, p);
drawText("EDIT", c, leftButton, p);

RectF rightButton = new RectF(itemView.getRight() - buttonWidthWithoutPadding, itemView.getTop(), itemView.getRight(), itemView.getBottom());
p.setColor(Color.RED);
c.drawRoundRect(rightButton, corners, corners, p);
drawText("DELETE", c, rightButton, p);

buttonInstance = null;
if (buttonShowedState == ButtonsState.LEFT_VISIBLE) {
buttonInstance = leftButton;
}
else if (buttonShowedState == ButtonsState.RIGHT_VISIBLE) {
buttonInstance = rightButton;
}
}

private void drawText(String text, Canvas c, RectF button, Paint p) {
float textSize = 60;
p.setColor(Color.WHITE);
p.setAntiAlias(true);
p.setTextSize(textSize);

float textWidth = p.measureText(text);
c.drawText(text, button.centerX()-(textWidth/2), button.centerY()+(textSize/2), p);
}
drawing buttons with onChildDraw method

As you may notice, it almost works, except buttons disappear after scrolling up/down our view. This is because onChildDraw method is being triggered only when swiping or moving items (unfortunately not on scrolling view). To avoid this we can use ItemDecoration instead, to be sure our buttons have been drawn correctly.

// MainActivity.java
private void setupRecyclerView() {
// ...
recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
swipeController.onDraw(c);
}
});
}

We just need to implement public onDraw method in our swipe controller. Then in onChildDraw method we need to assign currentItemViewHolder property instead of calling onDraw method directly.

// SwipeController.java
private RecyclerView.ViewHolder currentItemViewHolder = null;
if (buttonShowedState == ButtonsState.GONE) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
currentItemViewHolder = viewHolder;
}// PlayersSwipeController.java
private RecyclerView.ViewHolder currentItemViewHolder = null;
drawing buttons with ItemDecoration class

Buttons actions — callbacks

Right now we have almost everything — except the fact, that our buttons do nothing. We need to create an abstract class SwipeControllerActions with two methods to implement onLeftClicked and onRightClicked.

// SwipeControllerActions.java
package pl.fanfatal.swipecontrollerdemo;

public abstract class SwipeControllerActions {

public void onLeftClicked(int position) {}

public void onRightClicked(int position) {}
}

Now, we need to pass our action to controller:

// SwipeController.java
public SwipeController(SwipeControllerActions buttonsActions) {
this.buttonsActions = buttonsActions;
}

And in our touchUpListener we need to check if our button has been clicked or not:

// SwipeController.java
if (buttonsActions != null && buttonInstance != null && buttonInstance.contains(event.getX(), event.getY())) {
if (buttonShowedState == ButtonsState.LEFT_VISIBLE) {
buttonsActions.onLeftClicked(viewHolder.getAdapterPosition());
}
else if (buttonShowedState == ButtonsState.RIGHT_VISIBLE) {
buttonsActions.onRightClicked(viewHolder.getAdapterPosition());
}
}

In our MainActivity class we can pass callback this way:

// MainActivity.java
swipeController = new SwipeController(new SwipeControllerActions() {
@Override
public void onRightClicked(int position) {
mAdapter.players.remove(position);
mAdapter.notifyItemRemoved(position);
mAdapter.notifyItemRangeChanged(position, mAdapter.getItemCount());
}
});
Swipe buttons with action callback

Final code

This is it —as you can see it’s very simple to create swipe menu with RecyclerView without any additional libraries.

Code from this tutorial is available on my github.

--

--