android - make - recyclerview dependency
ConfirmaciĆ³n y deshacer la eliminaciĆ³n en RecyclerView (4)
El enfoque habitual es no eliminar el elemento inmediatamente al deslizar. Coloque un mensaje (podría ser un snackbar o, como en Gmail, un mensaje superpuesto al elemento que acaba de pasar) y proporcione un botón de tiempo de espera y un botón de deshacer para el mensaje.
Si el usuario presiona el botón Deshacer mientras el mensaje está visible, simplemente debe descartar el mensaje y volver al procesamiento normal. Elimine el elemento real solo si el tiempo de espera transcurre sin que el usuario presione el botón de deshacer.
Básicamente, algo a lo largo de estas líneas:
@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);
}
}
Esto supone la existencia de un diseño de undo
en el visor del elemento, normalmente invisible, con dos elementos, un texto (que dice Eliminado o similar) y un botón Deshacer .
<?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>
Al tocar el botón simplemente se elimina el mensaje. Opcionalmente, tocar el texto confirma la eliminación y elimina el elemento inmediatamente llamando a la devolución de llamada apropiada en su código. No olvide volver a llamar al notifyItemRemoved()
su adaptador:
public void onDismiss(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, int position) {
//TODO delete the actual item in your data source
adapter.notifyItemRemoved(position);
}
Tengo una lista de elementos simples en RecyclerView. Usando ItemTouchHelper fue muy fácil implementar el comportamiento de "deslizar para eliminar".
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?
}
}
}
Funciona bien. Sin embargo, también necesito implementar alguna acción de deshacer o confirmación. ¿Cuál es la mejor manera de hacer esto?
¿La primera pregunta es cómo insertar otra vista en lugar de eliminar con el cuadro de diálogo de confirmación? ¿Y cómo restaurar el elemento deslizado, si el usuario elige deshacer la eliminación?
Estoy de acuerdo con @Gabor en que es mejor eliminar los elementos y mostrar el botón Deshacer.
Sin embargo estoy usando Snackbar para mostrar la UNDO. Fue más fácil de implementar para mí.
Estoy pasando el adaptador y la instancia RecyclerView a mi devolución de llamada ItemTouchHelper. Mi onSwiped es simple y la mayor parte del trabajo lo realiza el adaptador.
Aquí está mi código ( editado el 01/01/2016 ):
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
mAdapter.onItemRemove(viewHolder, mRecyclerView);
}
El onItemRemove methos del adaptador es:
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);
}
El photosToDelete es un campo ArrayList de myAdapter. Estoy haciendo la eliminación real de esos elementos en el método onPause () del fragmento de host recyclerView.
Nota de edición 2016/01/10 :
- cambió la posición de código fijo como @Sourabh sugirió en los comentarios
- Para ver el ejemplo completo de adaptador y fragmento con RV, consulte esta idea.
He descubierto una forma mucho más sencilla de hacer un cuadro de diálogo de confirmación de eliminación:
@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();
}
Tenga en cuenta que:
- la eliminación se delega a ViewModel que actualiza el mRecyclerViewAdapter en caso de éxito.
- para que el elemento "devuelva" solo tiene que llamar a mRecyclerViewAdapter.notifyItemChanged
- cancelListener y negativeButtonListener realizan lo mismo. Puede optar por usar .setCanclable (falso) si no desea que el usuario toque fuera del cuadro de diálogo
Probé la solución de JirkaV , pero estaba lanzando una IndexOutOfBoundsException
. Pude modificar su solución para que funcione para mí. Por favor, inténtalo y hazme saber si te encuentras con problemas.
@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);
}
Cómo funciona
Cuando el usuario desliza, se muestra un snackbar y el elemento se elimina del conjunto de datos, por lo tanto esto:
snackbar.show();
BookItems.remove(adapterPosition);
mBookAdapter.notifyItemRemoved(adapterPosition);
Dado que los datos utilizados para rellenar el recyclerView provienen de una base de datos SQL, en este momento el elemento marcado no se elimina de la base de datos.
Cuando el usuario hace clic en el botón "DESHACER", el elemento deslizado se devuelve y el recyclerView se desplaza a la posición del elemento que se acaba de volver a agregar. De ahí esto:
mBookItems.add(adapterPosition, bookItem);
mBookAdapter.notifyItemInserted(adapterPosition);
mRecyclerView.scrollToPosition(adapterPosition);
Luego, cuando la barra de aperitivos se cierra, verifico si la barra de aperitivos fue descartada por el usuario haciendo clic en el botón "DESHACER". Si no, elimino el elemento de la base de datos en este punto.
Probablemente haya problemas de rendimiento con esta solución, no he encontrado ninguno. Por favor, si nota alguno, deje su comentario.