I have got a list of simple items in RecyclerView. Using ItemTouchHelper it was very easy to implement "swipe-to-delete" behavior.
public class TripsAdapter extends RecyclerView.Adapter<TripsAdapter.VerticalItemHolder> {
private List<Trip> mTrips;
private Context mContext;
private RecyclerView mRecyclerView;
[...]
//Let adapter know his RecyclerView. Attaching ItemTouchHelper
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new TripItemTouchHelperCallback());
itemTouchHelper.attachToRecyclerView(recyclerView);
mRecyclerView = recyclerView;
}
[...]
public class TripItemTouchHelperCallback extends ItemTouchHelper.SimpleCallback {
public TripItemTouchHelperCallback (){
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.RIGHT);
}
@Override
public boolean onMove(RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
//some "move" implementation
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
//AND WHAT HERE?
}
}
}
It works well. However i also need to implement some undo action or confirmation. What is the best way to do this?
First question is how to insert another view in place of removed with confirmation dialog? And how to restore swiped item, if user chooses to undo removing?
I agree with @Gabor that it is better to soft delete the items and show the undo button.
However I'm using Snackbar for showing the UNDO. It was easier to implement for me.
I'm passing the Adapter and the RecyclerView instance to my ItemTouchHelper callback. My onSwiped is simple and most of the work is done by adapter.
Here is my code (edited 2016/01/10):
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
mAdapter.onItemRemove(viewHolder, mRecyclerView);
}
The onItemRemove methos of the adapter is:
public void onItemRemove(final RecyclerView.ViewHolder viewHolder, final RecyclerView recyclerView) {
final int adapterPosition = viewHolder.getAdapterPosition();
final Photo mPhoto = photos.get(adapterPosition);
Snackbar snackbar = Snackbar
.make(recyclerView, "PHOTO REMOVED", Snackbar.LENGTH_LONG)
.setAction("UNDO", new View.OnClickListener() {
@Override
public void onClick(View view) {
int mAdapterPosition = viewHolder.getAdapterPosition();
photos.add(mAdapterPosition, mPhoto);
notifyItemInserted(mAdapterPosition);
recyclerView.scrollToPosition(mAdapterPosition);
photosToDelete.remove(mPhoto);
}
});
snackbar.show();
photos.remove(adapterPosition);
notifyItemRemoved(adapterPosition);
photosToDelete.add(mPhoto);
}
The photosToDelete is an ArrayList field of myAdapter. I'm doing the real delete of those items in onPause() method of the recyclerView host fragment.
Note edit 2016/01/10:
The usual approach is not to delete the item immediately upon swipe. Put up a message (it could be a snackbar or, as in Gmail, a message overlaying the item just swiped) and provide both a timeout and an undo button for the message.
If the user presses the undo button while the message is visible, you simply dismiss the message and return to normal processing. Delete the actual item only if the timeout elapses without the user pressing the undo button.
Basically, something along these lines:
@Override
public void onSwiped(final RecyclerView.ViewHolder viewHolder, int direction) {
final View undo = viewHolder.itemView.findViewById(R.id.undo);
if (undo != null) {
// optional: tapping the message dismisses immediately
TextView text = (TextView) viewHolder.itemView.findViewById(R.id.undo_text);
text.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
callbacks.onDismiss(recyclerView, viewHolder, viewHolder.getAdapterPosition());
}
});
TextView button = (TextView) viewHolder.itemView.findViewById(R.id.undo_button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
recyclerView.getAdapter().notifyItemChanged(viewHolder.getAdapterPosition());
clearView(recyclerView, viewHolder);
undo.setVisibility(View.GONE);
}
});
undo.setVisibility(View.VISIBLE);
undo.postDelayed(new Runnable() {
public void run() {
if (undo.isShown())
callbacks.onDismiss(recyclerView, viewHolder, viewHolder.getAdapterPosition());
}
}, UNDO_DELAY);
}
}
This supposes the existence of an undo
layout in the item viewholder, normally invisible, with two items, a text (saying Deleted or similar) and an Undo button.
<?xml version="1.0" encoding="utf-8"?>
<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="wrap_content">
...
<LinearLayout
android:id="@+id/undo"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/darker_gray"
android:orientation="horizontal"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:visibility="gone">
<TextView
android:id="@+id/undo_text"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="2"
android:gravity="center|start"
android:text="Deleted"
android:textColor="@android:color/white"/>
<TextView
android:id="@+id/undo_button"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center|end"
android:text="UNDO"
android:textColor="?attr/colorAccent"
android:textStyle="bold"/>
</LinearLayout>
</FrameLayout>
Tapping the button simply removes the message. Optionally, tapping the text confirms the deletion and deletes the item immediately by calling the appropriate callback in your code. Don't forget to call back to your adapter's notifyItemRemoved()
:
public void onDismiss(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, int position) {
//TODO delete the actual item in your data source
adapter.notifyItemRemoved(position);
}
I tried JirkaV's solution, but it was throwing an IndexOutOfBoundsException
. I was able to modify his solution to work for me. Please try it and let me know if you run into problems.
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
final int adapterPosition = viewHolder.getAdapterPosition();
final BookItem bookItem = mBookItems.get(adapterPosition); //mBookItems is an arraylist of mBookAdpater;
snackbar = Snackbar
.make(mRecyclerView, R.string.item_removed, Snackbar.LENGTH_LONG)
.setAction(R.string.undo, new View.OnClickListener() {
@Override
public void onClick(View view) {
mBookItems.add(adapterPosition, bookItem);
mBookAdapter.notifyItemInserted(adapterPosition); //mBookAdapter is my Adapter class
mRecyclerView.scrollToPosition(adapterPosition);
}
})
.setCallback(new Snackbar.Callback() {
@Override
public void onDismissed(Snackbar snackbar, int event) {
super.onDismissed(snackbar, event);
Log.d(TAG, "SnackBar dismissed");
if (event != DISMISS_EVENT_ACTION) {
Log.d(TAG, "SnackBar not dismissed by click event");
//In my case I doing a database transaction. The items are only deleted from the database if the snackbar is not dismissed by click the UNDO button
mDatabase = mBookHelper.getWritableDatabase();
String whereClause = "_id" + "=?";
String[] whereArgs = new String[]{
String.valueOf(bookItem.getDatabaseId())
};
mDatabase.delete(BookDbSchema.BookEntry.NAME, whereClause, whereArgs);
mDatabase.close();
}
}
});
snackbar.show();
mBookItems.remove(adapterPosition);
mBookAdapter.notifyItemRemoved(adapterPosition);
}
When the user swipes, a snackbar is shown and the item is removed from the dataset, hence this:
snackbar.show();
BookItems.remove(adapterPosition);
mBookAdapter.notifyItemRemoved(adapterPosition);
Since the data used in populating the recyclerView is from an SQL database, the swiped item is not removed from the database at this point.
When the user clicks on the "UNDO" button, the swiped item is simply brought back and the recyclerView scrolls to the position of the just re-added item. Hence this:
mBookItems.add(adapterPosition, bookItem);
mBookAdapter.notifyItemInserted(adapterPosition);
mRecyclerView.scrollToPosition(adapterPosition);
Then when the snackbar dismisses, I checked if the snackbar was dismissed by the user clicking on the "UNDO" button. If no, I delete the item from the database at this point.
Probably there are performance issues with this solution, I haven''t found any. Please if you notice any, drop your comment.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With