Подтвердить что ты не робот

Подтверждение и удаление в 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?
        }
    }
}

Это хорошо работает. Однако мне также нужно выполнить некоторые действия отмены или подтверждения. Каков наилучший способ сделать это?

Первый вопрос заключается в том, как вставить другой вид вместо удаленного с диалоговым окном подтверждения? И как восстановить swiped элемент, если пользователь решает отменить удаление?

4b9b3361

Ответ 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);
}

PhotoToDelete - это поле ArrayList myAdapter. Я делаю реальное удаление этих элементов в методе onPause() фрагмента хоста recyclerView.

Примечание изменить 2016/01/10:

  • изменилось жестко закодированное положение как @Sourabh, предложенное в комментариях
  • для полного примера адаптера и фрагмента с RV см. этот метод

Ответ 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);
}

Ответ 3

Я попробовал решение JirkaV, но он выбрал 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", пропущенный элемент просто возвращается, а recyclerView прокручивается до позиции только что добавленного элемента. Отсюда:

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

Затем, когда закусочная уволится, я проверил, была ли убранная закуска пользователем, нажав кнопку "UNDO". Если нет, я удаляю элемент из базы данных в этот момент.

Вероятно, в этом решении есть проблемы с производительностью, я не нашел их. Если вы заметили, напишите свой комментарий.

Ответ 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.
  • для элемента "return" вам просто нужно вызвать mRecyclerViewAdapter.notifyItemChanged
  • cancelListener и negativeButtonListener выполняют одно и то же. Вы можете использовать .setCanclable(false), если вы не хотите, чтобы пользователь выходил за пределы диалогового окна