【问题标题】:Confirmation and undo removing in RecyclerViewRecyclerView 中的确认和撤消删除
【发布时间】:2015-06-15 16:40:34
【问题描述】:

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

效果很好。但是我还需要执行一些撤消操作或确认。最好的方法是什么?

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

【问题讨论】:

  • “最好的方法是什么?” -- an undo bar 或小吃店。 “如果用户选择撤消删除,如何恢复刷过的项目?” -- 更新您的模型,然后在RecyclerView.Adapter 上调用适当的notify...() 方法来指示您的模型发生了什么变化。

标签: android android-recyclerview


【解决方案1】:

我同意@Gabor 的观点,即最好软删除项目并显示撤消按钮。

但是我使用 Snackbar 来显示 UNDO。对我来说实施起来更容易。

我将 Adapter 和 RecyclerView 实例传递给我的 ItemTouchHelper 回调。我的 onSwiped 很简单,大部分工作都是由适配器完成的。

这是我的代码(编辑于 2016/01/10):

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

适配器的onItemRemove方法是:

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

photosToDelete 是 myAdapter 的一个 ArrayList 字段。我正在 recyclerView 主机片段的 onPause() 方法中真正删除这些项目。

注意编辑 2016/01/10

  • 将硬编码位置更改为 cmets 中的 @Sourabh 建议
  • 有关带有 RV 的适配器和片段的完整示例,请参阅this gist

【讨论】:

  • 我喜欢你的想法。滑动前景视图后,某些应用程序在背景上显示撤消按钮。但是实现起来非常困难。你的方法就够了。还有更重要的事情需要关注。
  • 附带说明,您应该使用 ViewHolder.getAdapterPosition 而不是使用硬编码位置
  • @Sourabh 我不确定我是否明白你的意思。我在 onSwiped 方法中使用 getAdapterPosition。在这个方法中,我调用适配器的方法,以位置作为参数。硬编码位置在哪里?
  • 这里:photos.add(adapterPosition, mPhoto);。在点击Undo之前,适配器位置可能会发生变化,您应该将持有人传递给onItemRemove()
  • 谢谢,但你的方法是抛出java.lang.IndexOutOfBoundsException: Invalid index -1, size is 4
【解决方案2】:

通常的做法是不要在滑动后立即删除该项目。发布一条消息(它可以是一个快餐栏,或者在 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);
}

【讨论】:

  • 嗨,适配器类中的回调是什么?
  • 监听器接口,这是在 Android 中发送事件的常用方式(所有系统监听器如 OnClickListener 都以这种方式工作)。但它可以是您进行实际删除所需的任何调用。
  • 我尝试实现该解决方案,但是当显示撤消按钮时,任何单击都会再次调用 onSwiped() 方法而不是 onClick()。就像撤消视图上的任何触摸事件都被解释为滑动,只要该行没有被完全删除。有什么想法吗?
  • onSwiped() 中实际进行滑动之前,您可以简单地使用setVisibility() 隐藏整个撤消布局。
  • 它在我的应用程序中... :-)
【解决方案3】:

我尝试了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 数据库,因此此时不会从数据库中删除已刷过的项目。

当用户点击“UNDO”按钮时,滑动的item被简单的带回来,recyclerView滚动到刚刚重新添加的item的位置。因此:

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

然后,当小吃栏关闭时,我检查了用户点击“撤消”按钮是否关闭了小吃栏。如果不是,我此时从数据库中删除该项目。

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

【讨论】:

    【解决方案4】:

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

    @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
    • cancelListenernegativeButtonListener 执行相同的操作。如果您不希望用户在对话框外点击,您可以选择使用 .setCanclable(false)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-03-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多