29

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?

1
  • "What is the best way to do this?" -- an undo bar or snackbar. "And how to restore swiped item, if user chooses to undo removing?" -- update your model, then call the appropriate notify...() method on the RecyclerView.Adapter to indicate what changed in your model. Commented Jun 15, 2015 at 16:45

4 Answers 4

24

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:

  • changed hard-coded position as @Sourabh suggested in comments
  • for the complete example of adapter and fragment with RV see this gist
12
  • I liked you idea. Some apps show undo button on background after swiping foreground view. But it's very difficult to implement that. Your approach is enough. There are more important things to focus.
    – Robasan
    Commented Dec 27, 2015 at 11:50
  • On a side note, instead of using a hard coded position, you should use ViewHolder.getAdapterPosition
    – Sourabh
    Commented Dec 27, 2015 at 14:11
  • @Sourabh I'm not sure if I get your point. I'm using the getAdapterPosition in the onSwiped method. In this method I call the method of the adapter, with the position as argument. Where is the hard coded position?
    – JirkaV
    Commented Jan 4, 2016 at 20:12
  • Here: photos.add(adapterPosition, mPhoto);. Before clicking Undo, the adapter position might change, you should pass holder to onItemRemove()
    – Sourabh
    Commented Jan 5, 2016 at 18:01
  • 1
    Thanks but your method is throwing java.lang.IndexOutOfBoundsException: Invalid index -1, size is 4
    – X09
    Commented Sep 12, 2016 at 0:56
14

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);
}
6
  • Hi what is callbacks in adapter class?
    – user4571931
    Commented Sep 21, 2015 at 12:20
  • A listener interface, this is the usual way to send an event in Android (all system listeners like OnClickListener work that way). But it can be any call you need to make the actual deletion.
    – Gábor
    Commented Sep 21, 2015 at 15:09
  • 4
    I've tried to implement that solution but when showing the undo button, any click calls the onSwiped() method again instead of the onClick(). It's like any touch event on the undo view is interpreted as a swipe as long as the row is not fully deleted. Any ideas?
    – Biniou
    Commented Oct 2, 2015 at 19:23
  • 1
    Before you actually do your swiping work in onSwiped(), you could simply hide the whole undo layout with its setVisibility().
    – Gábor
    Commented Oct 2, 2015 at 21:38
  • It does in my app... :-)
    – Gábor
    Commented Nov 2, 2015 at 21:27
4

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);
    }

How it works

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.

2

I've figured out much simpler way to do a deletion confirmation dialog working:

@Override
        public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
            int itemPosition = viewHolder.getAdapterPosition();

            new AlertDialog.Builder(YourActivity.this)
                    .setMessage("Do you want to delete: \"" + mRecyclerViewAdapter.getItemAtPosition(itemPosition).getName() + "\"?")
                    .setPositiveButton("Delete", (dialog, which) -> mYourActivityViewModel.removeItem(itemPosition))
                    .setNegativeButton("Cancel", (dialog, which) -> mRecyclerViewAdapter.notifyItemChanged(itemPosition))
                    .setOnCancelListener(dialogInterface -> mRecyclerViewAdapter.notifyItemChanged(itemPosition))
                    .create().show();
        }

Note that:

  • deletion is delegated to ViewModel which updates the mRecyclerViewAdapter upon success.
  • for the item to "return" you just have to call mRecyclerViewAdapter.notifyItemChanged
  • cancelListener and negativeButtonListener perform the same thing. You can opt to use .setCanclable(false) if you don't want the user to tap outside the dialog

Not the answer you're looking for? Browse other questions tagged or ask your own question.