如何延迟(使用撤消)滑动以删除使用 Room 和 LiveData 的 Recyclerview?
How can I have a delayed (with Undo) Swipe to Delete Recyclerview that uses Room and LiveData?
我使用以下示例作为指南在我的应用程序中实现了滑动删除功能 nemanja-kovacevic/recycler-view-swipe-to-delete。最初我使用的是一个简单的 SQLite 数据库 class 并且一切正常。
在尝试更新我的应用程序以利用 Android 的架构组件室和 LiveData 时,我遵循了 Google 的 Room with a view Codelab。更新代码后,它似乎可以工作,并且可以进行一次滑动。但是,如果您在撤消延迟完成之前滑动另一行,LiveData 会更新适配器缓存的列表副本,以便后续的待处理删除可运行对象无法找到它们应该在列表中移动的项目(位置=-1),这会使应用程序崩溃。
那是很多解释,这里是适配器代码:
public class DropsListAdapter extends RecyclerView.Adapter<DropsListAdapter.DropHolder> {
private final static int PENDING_REMOVAL_TIMEOUT = 3000; // 3sec
private final LayoutInflater inflater;
private Context context;
private Handler mHandler = new Handler();
private HashMap<DeadDrop, Runnable> pendingRunnables = new HashMap<>();
private List<DeadDrop> deadDrops;
private List<DeadDrop> dropsPendingRemoval;
DropsListAdapter(Context context) {
inflater = LayoutInflater.from(context);
this.context = context;
this.dropsPendingRemoval = new ArrayList<>();
}
@NonNull
@Override
public DropHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new DropHolder(inflater.inflate(R.layout.drop_list_item, parent, false));
}
@Override
public void onBindViewHolder(@NonNull DropHolder holder, int position) {
final DeadDrop deadDrop = deadDrops.get(position);
if (dropsPendingRemoval.contains(deadDrop)) {
holder.itemView.setBackgroundColor(Color.WHITE);
holder.undoIt.setVisibility(View.VISIBLE);
holder.rowWrapper.setVisibility(View.GONE);
} else {
holder.rowWrapper.setVisibility(View.VISIBLE);
holder.latitude.setText(Converts.latitudeToSexaString(deadDrop.getLatitude()));
holder.longitude.setText(Converts.longitudeToSexaString(deadDrop.getLongitude()));
holder.undoIt.setVisibility(View.GONE);
}
}
@Override
public int getItemCount() {
if (deadDrops != null)
return deadDrops.size();
else return 0;
}
void pendingRemoval(int position) {
final DeadDrop mDeadDrop = deadDrops.get(position);
if (!dropsPendingRemoval.contains(mDeadDrop)) {
dropsPendingRemoval.add(mDeadDrop);
notifyItemChanged(position);
Runnable pendingRemovalRunnable = new Runnable() {
@Override
public void run() {
// Here is the problem. After the first item is removed,
// the next drop to remove is not found in the newly updated
// list of items (deadDrops).
int pos = deadDrops.indexOf(mDeadDrop);
remove(pos);
}
};
mHandler.postDelayed(pendingRemovalRunnable, PENDING_REMOVAL_TIMEOUT);
pendingRunnables.put(mDeadDrop, pendingRemovalRunnable);
}
}
void setDeadDrops(List<DeadDrop> drops) {
deadDrops = drops;
notifyDataSetChanged();
}
private void remove(int position) {
DeadDrop drop = deadDrops.get(position);
dropsPendingRemoval.remove(drop);
if (deadDrops.contains(drop)) {
deadDrops.remove(position);
notifyItemRemoved(position);
((DeadDropActivity) context).mDeadDropViewModel.delete(drop);
notifyDataSetChanged();
}
}
boolean isPendingRemoval(int position) {
return dropsPendingRemoval.contains(deadDrops.get(position));
}
/**
* Drops List View Holder class
*/
protected class DropHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
LinearLayout rowWrapper;
TextView latitude, longitude;
ImageButton mapIt;
Button undoIt;
DropHolder(View itemView) {
super(itemView);
rowWrapper = itemView.findViewById(R.id.row_wrapper);
latitude = itemView.findViewById(R.id.latitude_sexagesimal);
longitude = itemView.findViewById(R.id.longitude_sexagesimal);
mapIt = itemView.findViewById(R.id.button_map_it);
undoIt = itemView.findViewById(R.id.undo_button);
mapIt.setOnClickListener(this);
undoIt.setOnClickListener(this);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.button_map_it) {
String gUri = String.format(Locale.ENGLISH,
"https://www.google.com/maps/@%f,%f," + DeadDropActivity.GMAPS_CLOSE_ZOOM + "z",
deadDrops.get(getLayoutPosition()).getLatitude(),
deadDrops.get(getLayoutPosition()).getLongitude());
Intent gIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(gUri));
gIntent.setClassName("com.google.android.apps.maps",
"com.google.android.maps.MapsActivity");
try {
context.startActivity(gIntent);
} catch (ActivityNotFoundException ex) {
try {
String uri = String.format(Locale.ENGLISH, "geo:%f,%f?z=25",
deadDrops.get(getLayoutPosition()).getLatitude(),
deadDrops.get(getLayoutPosition()).getLongitude());
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
context.startActivity(intent);
} catch (ActivityNotFoundException innerEx) {
Toast.makeText(context, "Please install a maps application or browser.",
Toast.LENGTH_LONG).show();
innerEx.printStackTrace();
}
}
Toast.makeText(context, "Map Button clicked at " + getLayoutPosition(), Toast.LENGTH_SHORT).show();
} else if (v.getId() == R.id.undo_button) {
DeadDrop deadDrop = deadDrops.get(getLayoutPosition());
// user wants to undo the removal, let's cancel the pending task
// Cancelling still works without issue.
Runnable pendingRemovalRunnable = pendingRunnables.get(deadDrop);
pendingRunnables.remove(deadDrop);
if (pendingRemovalRunnable != null)
mHandler.removeCallbacks(pendingRemovalRunnable);
dropsPendingRemoval.remove(deadDrop);
// this will rebind the row in "normal" state
notifyItemChanged(deadDrops.indexOf(deadDrop));
Log.d(TAG, TAG_CLASS + ".onClickUndo(" + getLayoutPosition() + ")");
}
}
}
/**
* Utility class
*/
public static class Converts {
static String latitudeToSexaString(double latitude) {
String latDir = (latitude < 0) ? "S" : "N";
double lat = Math.abs(latitude);
double s;
int d, m;
d = (int) lat;
m = (int) ((lat - d) * 60);
s = (((lat - d) * 60) - m) * 60;
return String.format(Locale.ENGLISH, "%02d\u00B0", d) +
String.format(Locale.ENGLISH, "%02d\u0027", m) +
String.format(Locale.ENGLISH, "%02.1f\"", s) + latDir;
}
static String longitudeToSexaString(double longitude) {
String lonDir = (longitude < 0) ? "W" : "E";
double lon = Math.abs(longitude);
double s;
int d, m;
d = (int) lon;
m = (int) ((lon - d) * 60);
s = (((lon - d) * 60) - m) * 60;
return String.format(Locale.ENGLISH, "%02d\u00B0", d) +
String.format(Locale.ENGLISH, "%02d\u0027", m) +
String.format(Locale.ENGLISH, "%02.1f\"", s) + lonDir;
}
}
}
这里是 logcat,它显示 indexof 返回 -1 因为没有找到实例,但那是因为它是同一对象的新实例(ID 相同,但是 object.toString() 不同:
2019-07-14 02:29:17.890 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.pendingRemoval(): 2
2019-07-14 02:29:17.890 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.pendingRemoval(): {}
2019-07-14 02:29:17.890 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.pendingRemoval(): ID|16
2019-07-14 02:29:20.896 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.[Runnable]run(): 2
DropID = com.daweber.deaddrop.DeadDrop@d769128(ID|16)
DropsList = [com.daweber.deaddrop.DeadDrop@50eec1a, com.daweber.deaddrop.DeadDrop@966a34b, com.daweber.deaddrop.DeadDrop@d769128]
2019-07-14 02:29:20.992 18618-18618/com.daweber.deaddrop D/daweber.DD: .DeadDropActivity.[Observer].onChanged(): [com.daweber.deaddrop.DeadDrop@9f16479, com.daweber.deaddrop.DeadDrop@6fad5be]
2019-07-14 02:29:37.286 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.pendingRemoval(): 1
2019-07-14 02:29:37.287 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.pendingRemoval(): {com.daweber.deaddrop.DeadDrop@d769128=com.daweber.deaddrop.DropsListAdapter@38e0d6c}
2019-07-14 02:29:37.287 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.pendingRemoval(): ID|15
2019-07-14 02:29:37.766 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.pendingRemoval(): 0
2019-07-14 02:29:37.766 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.pendingRemoval(): {com.daweber.deaddrop.DeadDrop@6fad5be=com.daweber.deaddrop.DropsListAdapter@4049458, com.daweber.deaddrop.DeadDrop@d769128=com.daweber.deaddrop.DropsListAdapter@38e0d6c}
2019-07-14 02:29:37.767 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.pendingRemoval(): ID|4
2019-07-14 02:29:40.292 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.[Runnable]run(): 1
DropID = com.daweber.deaddrop.DeadDrop@6fad5be(ID|15)
DropsList = [com.daweber.deaddrop.DeadDrop@9f16479, com.daweber.deaddrop.DeadDrop@6fad5be]
2019-07-14 02:29:40.358 18618-18618/com.daweber.deaddrop D/daweber.DD: .DeadDropActivity.[Observer].onChanged(): [com.daweber.deaddrop.DeadDrop@c9d4e22]
2019-07-14 02:29:40.769 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.[Runnable]run(): -1
DropID = com.daweber.deaddrop.DeadDrop@9f16479(ID|4)
DropsList = [com.daweber.deaddrop.DeadDrop@c9d4e22]
2019-07-14 02:30:02.153 18766-18766/com.daweber.deaddrop D/daweber.DD: .DeadDropActivity.[Observer].onChanged(): [com.daweber.deaddrop.DeadDrop@ff36b61]
所以,现在的问题是,我该如何修改这一行
int pos = deadDrops.indexOf(mDeadDrop);
通过查找 object.getId() 而不是对象签名来获取对象的索引?
所以,我找到了一种 "brute force" 方法来做到这一点,它似乎可以防止崩溃,但如果列表增加到 1000 条条目,这可能不是最有效的方法,所以如果有人有更好的解决方案:
新 运行()
@Override
public void run() {
final String TAG_FUN = ".[Runnable]run(): ";
// TODO: Here is the problem.
for (int i = 0; i <= deadDrops.size(); i++) {
DeadDrop d = deadDrops.get(i);
if (d.getId() == mDeadDrop.getId()) {
int pos = deadDrops.indexOf(d);
Log.d(TAG, TAG_CLS + TAG_FUN + pos
+ "\nDropID = " + d.toString() + "(ID|" + d.getId() + ")"
+ "\nDropsList = " + deadDrops.toString());
remove(pos);
break;
}
}
}
我使用以下示例作为指南在我的应用程序中实现了滑动删除功能 nemanja-kovacevic/recycler-view-swipe-to-delete。最初我使用的是一个简单的 SQLite 数据库 class 并且一切正常。
在尝试更新我的应用程序以利用 Android 的架构组件室和 LiveData 时,我遵循了 Google 的 Room with a view Codelab。更新代码后,它似乎可以工作,并且可以进行一次滑动。但是,如果您在撤消延迟完成之前滑动另一行,LiveData 会更新适配器缓存的列表副本,以便后续的待处理删除可运行对象无法找到它们应该在列表中移动的项目(位置=-1),这会使应用程序崩溃。
那是很多解释,这里是适配器代码:
public class DropsListAdapter extends RecyclerView.Adapter<DropsListAdapter.DropHolder> {
private final static int PENDING_REMOVAL_TIMEOUT = 3000; // 3sec
private final LayoutInflater inflater;
private Context context;
private Handler mHandler = new Handler();
private HashMap<DeadDrop, Runnable> pendingRunnables = new HashMap<>();
private List<DeadDrop> deadDrops;
private List<DeadDrop> dropsPendingRemoval;
DropsListAdapter(Context context) {
inflater = LayoutInflater.from(context);
this.context = context;
this.dropsPendingRemoval = new ArrayList<>();
}
@NonNull
@Override
public DropHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new DropHolder(inflater.inflate(R.layout.drop_list_item, parent, false));
}
@Override
public void onBindViewHolder(@NonNull DropHolder holder, int position) {
final DeadDrop deadDrop = deadDrops.get(position);
if (dropsPendingRemoval.contains(deadDrop)) {
holder.itemView.setBackgroundColor(Color.WHITE);
holder.undoIt.setVisibility(View.VISIBLE);
holder.rowWrapper.setVisibility(View.GONE);
} else {
holder.rowWrapper.setVisibility(View.VISIBLE);
holder.latitude.setText(Converts.latitudeToSexaString(deadDrop.getLatitude()));
holder.longitude.setText(Converts.longitudeToSexaString(deadDrop.getLongitude()));
holder.undoIt.setVisibility(View.GONE);
}
}
@Override
public int getItemCount() {
if (deadDrops != null)
return deadDrops.size();
else return 0;
}
void pendingRemoval(int position) {
final DeadDrop mDeadDrop = deadDrops.get(position);
if (!dropsPendingRemoval.contains(mDeadDrop)) {
dropsPendingRemoval.add(mDeadDrop);
notifyItemChanged(position);
Runnable pendingRemovalRunnable = new Runnable() {
@Override
public void run() {
// Here is the problem. After the first item is removed,
// the next drop to remove is not found in the newly updated
// list of items (deadDrops).
int pos = deadDrops.indexOf(mDeadDrop);
remove(pos);
}
};
mHandler.postDelayed(pendingRemovalRunnable, PENDING_REMOVAL_TIMEOUT);
pendingRunnables.put(mDeadDrop, pendingRemovalRunnable);
}
}
void setDeadDrops(List<DeadDrop> drops) {
deadDrops = drops;
notifyDataSetChanged();
}
private void remove(int position) {
DeadDrop drop = deadDrops.get(position);
dropsPendingRemoval.remove(drop);
if (deadDrops.contains(drop)) {
deadDrops.remove(position);
notifyItemRemoved(position);
((DeadDropActivity) context).mDeadDropViewModel.delete(drop);
notifyDataSetChanged();
}
}
boolean isPendingRemoval(int position) {
return dropsPendingRemoval.contains(deadDrops.get(position));
}
/**
* Drops List View Holder class
*/
protected class DropHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
LinearLayout rowWrapper;
TextView latitude, longitude;
ImageButton mapIt;
Button undoIt;
DropHolder(View itemView) {
super(itemView);
rowWrapper = itemView.findViewById(R.id.row_wrapper);
latitude = itemView.findViewById(R.id.latitude_sexagesimal);
longitude = itemView.findViewById(R.id.longitude_sexagesimal);
mapIt = itemView.findViewById(R.id.button_map_it);
undoIt = itemView.findViewById(R.id.undo_button);
mapIt.setOnClickListener(this);
undoIt.setOnClickListener(this);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.button_map_it) {
String gUri = String.format(Locale.ENGLISH,
"https://www.google.com/maps/@%f,%f," + DeadDropActivity.GMAPS_CLOSE_ZOOM + "z",
deadDrops.get(getLayoutPosition()).getLatitude(),
deadDrops.get(getLayoutPosition()).getLongitude());
Intent gIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(gUri));
gIntent.setClassName("com.google.android.apps.maps",
"com.google.android.maps.MapsActivity");
try {
context.startActivity(gIntent);
} catch (ActivityNotFoundException ex) {
try {
String uri = String.format(Locale.ENGLISH, "geo:%f,%f?z=25",
deadDrops.get(getLayoutPosition()).getLatitude(),
deadDrops.get(getLayoutPosition()).getLongitude());
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
context.startActivity(intent);
} catch (ActivityNotFoundException innerEx) {
Toast.makeText(context, "Please install a maps application or browser.",
Toast.LENGTH_LONG).show();
innerEx.printStackTrace();
}
}
Toast.makeText(context, "Map Button clicked at " + getLayoutPosition(), Toast.LENGTH_SHORT).show();
} else if (v.getId() == R.id.undo_button) {
DeadDrop deadDrop = deadDrops.get(getLayoutPosition());
// user wants to undo the removal, let's cancel the pending task
// Cancelling still works without issue.
Runnable pendingRemovalRunnable = pendingRunnables.get(deadDrop);
pendingRunnables.remove(deadDrop);
if (pendingRemovalRunnable != null)
mHandler.removeCallbacks(pendingRemovalRunnable);
dropsPendingRemoval.remove(deadDrop);
// this will rebind the row in "normal" state
notifyItemChanged(deadDrops.indexOf(deadDrop));
Log.d(TAG, TAG_CLASS + ".onClickUndo(" + getLayoutPosition() + ")");
}
}
}
/**
* Utility class
*/
public static class Converts {
static String latitudeToSexaString(double latitude) {
String latDir = (latitude < 0) ? "S" : "N";
double lat = Math.abs(latitude);
double s;
int d, m;
d = (int) lat;
m = (int) ((lat - d) * 60);
s = (((lat - d) * 60) - m) * 60;
return String.format(Locale.ENGLISH, "%02d\u00B0", d) +
String.format(Locale.ENGLISH, "%02d\u0027", m) +
String.format(Locale.ENGLISH, "%02.1f\"", s) + latDir;
}
static String longitudeToSexaString(double longitude) {
String lonDir = (longitude < 0) ? "W" : "E";
double lon = Math.abs(longitude);
double s;
int d, m;
d = (int) lon;
m = (int) ((lon - d) * 60);
s = (((lon - d) * 60) - m) * 60;
return String.format(Locale.ENGLISH, "%02d\u00B0", d) +
String.format(Locale.ENGLISH, "%02d\u0027", m) +
String.format(Locale.ENGLISH, "%02.1f\"", s) + lonDir;
}
}
}
这里是 logcat,它显示 indexof 返回 -1 因为没有找到实例,但那是因为它是同一对象的新实例(ID 相同,但是 object.toString() 不同:
2019-07-14 02:29:17.890 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.pendingRemoval(): 2
2019-07-14 02:29:17.890 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.pendingRemoval(): {}
2019-07-14 02:29:17.890 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.pendingRemoval(): ID|16
2019-07-14 02:29:20.896 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.[Runnable]run(): 2
DropID = com.daweber.deaddrop.DeadDrop@d769128(ID|16)
DropsList = [com.daweber.deaddrop.DeadDrop@50eec1a, com.daweber.deaddrop.DeadDrop@966a34b, com.daweber.deaddrop.DeadDrop@d769128]
2019-07-14 02:29:20.992 18618-18618/com.daweber.deaddrop D/daweber.DD: .DeadDropActivity.[Observer].onChanged(): [com.daweber.deaddrop.DeadDrop@9f16479, com.daweber.deaddrop.DeadDrop@6fad5be]
2019-07-14 02:29:37.286 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.pendingRemoval(): 1
2019-07-14 02:29:37.287 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.pendingRemoval(): {com.daweber.deaddrop.DeadDrop@d769128=com.daweber.deaddrop.DropsListAdapter@38e0d6c}
2019-07-14 02:29:37.287 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.pendingRemoval(): ID|15
2019-07-14 02:29:37.766 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.pendingRemoval(): 0
2019-07-14 02:29:37.766 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.pendingRemoval(): {com.daweber.deaddrop.DeadDrop@6fad5be=com.daweber.deaddrop.DropsListAdapter@4049458, com.daweber.deaddrop.DeadDrop@d769128=com.daweber.deaddrop.DropsListAdapter@38e0d6c}
2019-07-14 02:29:37.767 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.pendingRemoval(): ID|4
2019-07-14 02:29:40.292 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.[Runnable]run(): 1
DropID = com.daweber.deaddrop.DeadDrop@6fad5be(ID|15)
DropsList = [com.daweber.deaddrop.DeadDrop@9f16479, com.daweber.deaddrop.DeadDrop@6fad5be]
2019-07-14 02:29:40.358 18618-18618/com.daweber.deaddrop D/daweber.DD: .DeadDropActivity.[Observer].onChanged(): [com.daweber.deaddrop.DeadDrop@c9d4e22]
2019-07-14 02:29:40.769 18618-18618/com.daweber.deaddrop D/daweber.DD: .DropListAdapter.[Runnable]run(): -1
DropID = com.daweber.deaddrop.DeadDrop@9f16479(ID|4)
DropsList = [com.daweber.deaddrop.DeadDrop@c9d4e22]
2019-07-14 02:30:02.153 18766-18766/com.daweber.deaddrop D/daweber.DD: .DeadDropActivity.[Observer].onChanged(): [com.daweber.deaddrop.DeadDrop@ff36b61]
所以,现在的问题是,我该如何修改这一行
int pos = deadDrops.indexOf(mDeadDrop);
通过查找 object.getId() 而不是对象签名来获取对象的索引?
所以,我找到了一种 "brute force" 方法来做到这一点,它似乎可以防止崩溃,但如果列表增加到 1000 条条目,这可能不是最有效的方法,所以如果有人有更好的解决方案:
新 运行()
@Override
public void run() {
final String TAG_FUN = ".[Runnable]run(): ";
// TODO: Here is the problem.
for (int i = 0; i <= deadDrops.size(); i++) {
DeadDrop d = deadDrops.get(i);
if (d.getId() == mDeadDrop.getId()) {
int pos = deadDrops.indexOf(d);
Log.d(TAG, TAG_CLS + TAG_FUN + pos
+ "\nDropID = " + d.toString() + "(ID|" + d.getId() + ")"
+ "\nDropsList = " + deadDrops.toString());
remove(pos);
break;
}
}
}