Подтверждение и удаление в 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 элемент, если пользователь решает отменить удаление?
Ответы
Ответ 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), если вы не хотите, чтобы пользователь выходил за пределы диалогового окна