在RecyclerView中确认和撤消删除

时间:2015-06-15 16:40:34

标签: android android-recyclerview

我在RecyclerView中有一个简单项目列表。使用ItemTouchHelper实现“轻扫到删除”行为非常容易。

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?
        }
    }
}

效果很好。但是我还需要实现一些撤消操作或确认。这样做的最佳方式是什么?

第一个问题是如何使用确认对话框插入另一个视图代替删除?如果用户选择撤消删除,如何恢复刷过的项目?

4 个答案:

答案 0 :(得分:21)

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):

New-WebsiteShortcut -URL "https://www.google.ch" -Location "E:\PowerShell\Ziel"

The onItemRemove methos of the adapter is:

@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
    mAdapter.onItemRemove(viewHolder, mRecyclerView);
}

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

答案 1 :(得分:13)

通常的做法是不要在刷卡时立即删除该项目。提出一条消息(它可能是一个小吃吧,或者像在Gmail中一样,覆盖刚刚刷过的项目的消息),并为消息提供超时和撤消按钮。

如果用户在消息可见时按下撤消按钮,您只需关闭该消息并返回正常处理。仅在超时过去而没有用户按下撤销按钮的情况下删除实际项目。

基本上,这些内容如下:

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

这假设项目视图中存在undo布局,通常是不可见的,包含两个项目,一个文本(说明已删除或类似)和一个撤消按钮。

<?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>

点击按钮只会删除该消息。 (可选)点击文本确认删除并通过调用代码中的相应回调立即删除项目。不要忘记回拨适配器的notifyItemRemoved()

public void onDismiss(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, int position) {
  //TODO delete the actual item in your data source
  adapter.notifyItemRemoved(position);
}

答案 2 :(得分:2)

我尝试了JirkaV's solution,但却抛出IndexOutOfBoundsException。我能够修改他的解决方案,为我工作。如果您遇到问题,请尝试并告诉我。

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

如何运作

当用户滑动时,会显示一个快餐栏,并从数据集中删除该项目,因此:

snackbar.show();
BookItems.remove(adapterPosition);
mBookAdapter.notifyItemRemoved(adapterPosition);

由于填充recyclerView时使用的数据来自SQL数据库,因此此时不会从数据库中删除刷过的项目。

当用户点击&#34; UNDO&#34;按钮,简单地将刷过的物品带回来,recyclelerView滚动到刚刚重新添加的物品的位置。因此:

 mBookItems.add(adapterPosition, bookItem);
 mBookAdapter.notifyItemInserted(adapterPosition); 
 mRecyclerView.scrollToPosition(adapterPosition);

然后当小吃店解散时,我检查了小吃店是否被用户点击&#34; UNDO&#34;按钮。如果不是,我此时从数据库中删除该项目。

这个解决方案可能存在性能问题,我还没有发现任何问题。如果您发现任何问题,请发表评论。

答案 3 :(得分:1)

我已经找到了更简单的方法来执行删除确认对话框:

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

请注意:

  • 删除委托给 ViewModel ,成功时会更新 mRecyclerViewAdapter
  • 对于要返回的项目,您只需要调用 mRecyclerViewAdapter.notifyItemChanged
  • cancelListener negativeButtonListener 执行相同的操作。如果您不希望用户在对话框外点击,您可以选择使用.setCanclable(false)