The RecyclerView class, delivered as part of the v7 support library suite, is an improved version of the ListView and the GridView classes provided by the Android framework.
With the help of ItemTouchHelper
class you can add swipe to dismiss, drag & drop support to RecyclerView
. Swiping the row will remove the row from the RecyclerView
, but it won’t refresh the data. You can see an empty row displayed on swiping the row. You have to take care of refreshing the list by removing the item from the adapter dataset.
ItemTouchHelper is a utility class added in Android Support Library V7. This class helps to add swipe to dismiss and drag & drop support to RecyclerView
.
ItemTouchHelper
class extends RecyclerView.ItemDecoration
and implements RecyclerView.OnChildAttachStateChangeListener
. In order to use ItemTouchHelper
, you need to create ItemTouchHelper.Callback
. This class lets you control the touch event like swipe and move by providing a callback for the same. It also provides the callback to override the default animation.
It has a SimpleCallback
class which configures what type of interactions are enabled and also receives events when user performs these actions (like swipe and drag).
ItemTouchHelper.SimpleCallback
provides certain callback methods like onMove()
, onChildDraw()
, onSwiped()
when the row is swiped. Showing the background view, removing the item from adapter can be done using these callback methods.
To implement Swipe to Dismiss behaviour in RecyclerView
we have to override following methods of ItemTouchHelper.Callback
.
The first method we are going to discuss is getMovementFlags(RecyclerView, ViewHolder)
.
It takes parameters:
RecyclerView
: the RecyclerView
to which ItemTouchHelper
is attached.ViewHolder
: the ViewHolder
for which the movement information is necessary.and returns
flags
specifying which movements are allowed on this ViewHolder
.@Override public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) { int swipeFlags = Direction.RIGHT; return makeMovementFlags(0, swipeFlags); }
To specify in which direction you want to swipe, you have to override this method. makeMovementFlag(int, int)
method provides convenience to create Movement
flag.
The second method we are going to discuss is onMove(RecyclerView, ViewHolder, ViewHolder)
.
It takes parameters
RecyclerView
: the RecyclerView
to which ItemTouchHelper
is attached to.ViewHolder
: the ViewHolder
which is being dragged by the user.ViewHolder
: the ViewHolder
over which the currently active item is being dragged.and returns
true
if the ViewHolder
has been moved to the adapter position of the target.If you want to make any changes during dragging the RecyclerView
item then you have to override this method. If this method returns true
, ItemTouchHelper
assumes item has been moved to the adapter position of target place.
The final and important method for the swipe to dismiss is onSwiped(ViewHolder, int)
.
It takes parameters
ViewHolder
: the ViewHolder
which has been swiped by the user.int
(direction): the direction to which the ViewHolder
is swiped. It is one of UP, DOWN, LEFT or RIGHT. If your getMovementFlags(RecyclerView, ViewHolder)
method returned relative flags instead of LEFT; RIGHT direction will be relative as well (START or END).Layouts and Resources
I'm going to use Vector Asset for ic_add.xml, ic_edit.xml and ic_delete.xml resources.
For example, here is my ic_add.xml
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24.0" android:viewportHeight="24.0"> <path android:fillColor="#FFFFFFFF" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/> </vector>
The main layout (activity_main.xml) has a RecyclerView
with a FAB
in CoordinatorLayout
. The FAB
is used to add items to RecyclerView
list.
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout 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" android:fitsSystemWindows="true"> <android.support.v7.widget.RecyclerView android:id="@+id/rvItems" android:scrollbars="vertical" android:layout_width="match_parent" android:layout_height="match_parent"/> <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="@dimen/fab_margin" android:src="@drawable/ic_add" /> </android.support.design.widget.CoordinatorLayout>
The next layout is for RecyclerView
list item (row_layout.xml). It has only a TextView
to display item name.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_marginBottom="1dp" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/tvItem" android:layout_marginTop="15dp" android:layout_marginBottom="15dp" android:layout_gravity="center" android:textSize="18sp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textStyle="bold" /> </LinearLayout>
The following snippet is a RecyclerView.Adapter
. I added two methods addItem()
and removeItem()
to add and remove data from RecyclerView
. The notifyItemInserted()
notify the adapter that a new data is inserted. Similarly notifyItemRemoved()
notify about removal of data. And notifyItemRangeChanged()
is for range change.
public class DataAdapter extends RecyclerView.Adapter<DataAdapter.ViewHolder> { private ArrayList<String> items; public DataAdapter(ArrayList items) { this.items = items; } @Override public DataAdapter.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) { View view = LayoutInflater.from(viewGroup.getContext()) .inflate(R.layout.row_layout, viewGroup, false); return new ViewHolder(view); } @Override public void onBindViewHolder(ViewHolder viewHolder, int i) { viewHolder.tvItem.setText(items.get(i)); } @Override public int getItemCount() { return items.size(); } public void addItem() { int rnd = new Random().nextInt(100); String item = "Item " + rnd; items.add(item); notifyItemInserted(items.indexOf(item)); } public void removeItem(int position) { items.remove(position); notifyItemRemoved(position); notifyItemRangeChanged(position, items.size()); } public void moveItem(int oldPos, int newPos) { String item = items.get(oldPos); items.remove(oldPos); items.add(newPos, item); notifyItemMoved(oldPos, newPos); } public class ViewHolder extends RecyclerView.ViewHolder{ public TextView tvItem; public ViewHolder(View view) { super(view); tvItem = (TextView)view.findViewById(R.id.tvItem); } } }
Simple swipe and drag n drop
The following activity implements simple example of swipe and drag n drop.
public class MainActivity extends AppCompatActivity implements View.OnClickListener{ private ArrayList<String> items = new ArrayList<>(); private DataAdapter adapter; private RecyclerView recyclerView; private Activity activity = MainActivity.this; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(this); recyclerView = (RecyclerView)findViewById(R.id.rvItems); recyclerView.setHasFixedSize(true); RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getApplicationContext()); recyclerView.setLayoutManager(layoutManager); adapter = new DataAdapter(items); recyclerView.setAdapter(adapter); items.add("Item 1"); items.add("Item 2"); items.add("Item 3"); adapter.notifyDataSetChanged(); ItemTouchHelper itemTouchHelper = new ItemTouchHelper(createItemTouchHelper()); itemTouchHelper.attachToRecyclerView(recyclerView); } private ItemTouchHelper.Callback createItemTouchHelper() { ItemTouchHelper.Callback simpleCallback = new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) { @Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { adapter.moveItem(viewHolder.getAdapterPosition(), target.getAdapterPosition()); return true; } @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { adapter.removeItem(viewHolder.getAdapterPosition()); } @Override public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { float alpha = 1 - (Math.abs(dX) / recyclerView.getWidth()); viewHolder.itemView.setAlpha(alpha); } super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); } }; return simpleCallback; } @Override public void onClick(View v) { switch (v.getId()){ case R.id.fab: adapter.addItem(); break; } } }
Result
Custom view for swipe
In this section we'll build extended reaction to swipe (see screenshot below). In our Activity the AlertDialog
is initialized using initDialog()
method. Then we implement the custom Swipe view using the ItemTouchHelper.SimpleCallback
class.
Following is the layout (dialog_layout.xml) for Alert Dialog which is used to get input data to add and edit data. It has only a EditText
widget.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <EditText android:id="@+id/etItem" android:layout_marginLeft="10dp" android:layout_marginRight="10dp" android:layout_marginTop="10dp" android:layout_marginBottom="10dp" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout>
In following snippet the onChildDraw()
method is used to draw custom view under the RecyclerView
list item when swiped. Then we obtain the View
object itemView
from the RecyclerView.ViewHolder
object. We can obtain the coordinates of top, bottom, right, left as float points using getTop()
, getBottom()
, getRight()
, getLeft()
methods. First we draw a rectange on Canvas
which covers the whole item view in prefered color. Then we draw a VectorDrawable
icon over that in specified location. The two vector asset are ic_edit.xml and ic_delete.xml.
If swiped left you will see red background with delete icon. If swiped right you will see green background with edit icon. On left swipe the adapter removeItem()
method is called. On right swipe the AlertDialog
is displayed to edit the data. Similarly when the FAB is pressed the AlertDialog
is displayed to add data to RecyclerView
list. These swipe process is defined in initSwipe()
method. The ItemTouchHelper
is attached to RecyclerView
using the attachToRecyclerView()
method.
The AlertDialog
layout is inflated using LayoutInflator
. If the dialog is displayed for second time the existing view is removed using the removeView()
method.
public class MainActivity extends AppCompatActivity implements View.OnClickListener{ private ArrayList<String> items = new ArrayList<>(); private DataAdapter adapter; private RecyclerView recyclerView; private AlertDialog.Builder alertDialog; private EditText etItem; private int editPosition; private View view; private boolean add = false; private Paint p = new Paint(); private Activity activity = MainActivity.this; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initViews(); initDialog(); } private void initViews(){ FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(this); recyclerView = (RecyclerView)findViewById(R.id.rvItems); recyclerView.setHasFixedSize(true); RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getApplicationContext()); recyclerView.setLayoutManager(layoutManager); adapter = new DataAdapter(items); recyclerView.setAdapter(adapter); items.add("Item 1"); items.add("Item 2"); items.add("Item 3"); adapter.notifyDataSetChanged(); initSwipe(); } private void initSwipe(){ ItemTouchHelper.SimpleCallback simpleItemTouchCallback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) { @Override public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { return false; } @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { int position = viewHolder.getAdapterPosition(); if (direction == ItemTouchHelper.LEFT){ adapter.removeItem(position); } else { removeView(); editPosition = position; alertDialog.setTitle("Edit item"); etItem.setText(items.get(position)); alertDialog.show(); } } @Override public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { Bitmap icon; if(actionState == ItemTouchHelper.ACTION_STATE_SWIPE){ View itemView = viewHolder.itemView; float height = (float) itemView.getBottom() - (float) itemView.getTop(); float width = height / 3; if(dX > 0){ p.setColor(Color.parseColor("#388E3C")); RectF background = new RectF((float) itemView.getLeft(), (float) itemView.getTop(), dX,(float) itemView.getBottom()); c.drawRect(background,p); //icon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_edit); float left = (float) itemView.getLeft() + width; float top = (float) itemView.getTop() + width; float right = (float) itemView.getLeft() + 2 * width; float bottom = (float)itemView.getBottom() - width; icon = getBitmapFromVectorDrawable(activity, R.drawable.ic_edit); RectF iconDest = new RectF(left, top, right, bottom); c.drawBitmap(icon,null,iconDest,p); } else if (dX < 0) { p.setColor(Color.parseColor("#D32F2F")); RectF background = new RectF((float) itemView.getRight() + dX, (float) itemView.getTop(),(float) itemView.getRight(), (float) itemView.getBottom()); c.drawRect(background,p); //icon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_delete); icon = getBitmapFromVectorDrawable(activity, R.drawable.ic_delete); float left = (float) itemView.getRight() - 2*width; float top = (float) itemView.getTop() + width; float right = (float) itemView.getRight() - width; float bottom = (float)itemView.getBottom() - width; RectF iconDest = new RectF(left, top, right,bottom); c.drawBitmap(icon,null,iconDest,p); } } super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); } }; ItemTouchHelper itemTouchHelper = new ItemTouchHelper(simpleItemTouchCallback); itemTouchHelper.attachToRecyclerView(recyclerView); } private void removeView(){ if(view.getParent()!=null) { ((ViewGroup) view.getParent()).removeView(view); } } private void initDialog(){ alertDialog = new AlertDialog.Builder(this); view = getLayoutInflater().inflate(R.layout.dialog_layout,null); alertDialog.setView(view); alertDialog.setPositiveButton("Save", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if(add) { add = false; adapter.addItem(etItem.getText().toString()); dialog.dismiss(); } else { items.set(editPosition, etItem.getText().toString()); adapter.notifyDataSetChanged(); dialog.dismiss(); } } }); etItem = (EditText) view.findViewById(R.id.etItem); } @Override public void onClick(View v) { switch (v.getId()){ case R.id.fab: removeView(); add = true; alertDialog.setTitle("Add item"); etItem.setText(""); alertDialog.show(); break; } } public static Bitmap getBitmapFromVectorDrawable(Context context, int drawableId) { Drawable drawable = ContextCompat.getDrawable(context, drawableId); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { drawable = (DrawableCompat.wrap(drawable)).mutate(); } Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); return bitmap; } }