Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Confirmation and undo removing in RecyclerView

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?

like image 529
cheyuriy Avatar asked Jun 15 '15 16:06

cheyuriy


3 Answers

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
like image 104
JirkaV Avatar answered Nov 19 '22 00:11

JirkaV


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);
}
like image 27
Gábor Avatar answered Nov 19 '22 00:11

Gábor


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.

like image 4
X09 Avatar answered Nov 19 '22 00:11

X09