#java #android-recyclerview #drag-and-drop #itemtouchhelper #android-diffutils
#java #android-recyclerview #перетаскивание #itemtouchhelper #android-diffutils
Вопрос:
Я использую класс ItemTouchHelper для поддержки перетаскивания в моем RecyclerView. Пока я перетаскиваю элемент, он визуально обновляется (меняет местами строки), как и ожидалось. Как только я отбрасываю элемент, происходит еще одно визуальное перетаскивание. Например (см. Диаграмму ниже), если я перетаскиваю элемент «a» из индекса 0 в индекс 3, правильный список показывает, что элемент «b» теперь имеет индекс 0. Они повторяют операцию и берут новый элемент с индексом 0 («b») и перетаскивают его в индекс 3! Это повторное перетаскивание происходит независимо от того, из какого индекса я перетаскиваю или в какой.
Я назвал это визуальным перетаскиванием, потому что список, который я отправляю в ListAdapter моего RecyclerView, правильно упорядочен (проверен журналами). И если я перезапущу свое приложение, список будет в правильном порядке. Или, если я вызову notifyDataSetChanged() , после нежелательной анимации она будет упорядочена должным образом. Что может быть причиной этой второй анимации?
РЕДАКТИРОВАТЬ: согласно документации, если вы используете метод equals() в вашем методе areContentsTheSame() (DiffUtil), «Неправильный возврат false здесь приведет к слишком большому количеству анимаций». Насколько я могу судить, я правильно переопределяю этот метод в моем файле POJO ниже. Я в тупике…
MainActivity.java
private void setListObserver() {
viewModel.getAllItems().observe(this, new Observer<List<ListItem>>() {
@Override
// I have verified newList has the correct order through log statements
public void onChanged(List<ListItem> newList) {
adapterMain.submitList(newList);
}
});
}
...
// This method is called when item starts dragging
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
...
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
currentList = new ArrayList<>(adapterMain.getCurrentList()); // get current list from adapter
}
...
}
// This method is called when item is dropped
public void clearView(@NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder viewHolder) {
...
// I have verified that all code in this method is returning correct values through log statements.
// If I restart the app, everything is in the correct order
// this is position of the where the item was dragged to, gets its value from the onMoved method.
// it's the last "toPos" value in onMoved() after the item is dropped
int position = toPosition;
// Skip this code if item was deleted (indicated by -1). Otherwise, update the moved item
if(position != -1) {
ListItem movedItem = currentList.get(position);
// If dragged to the beginning of the list, subtract 1 from the previously lowest
// positionInList value (the item below it) and assign it the moved item. This will ensure
// that it now has the lowest positionInList value and will be ordered first.
if(position == 0) {
itemAfterPos = currentList.get(position 1).getPositionInList();
movedItemNewPos = itemAfterPos - 1;
// If dragged to the end of list, add 1 to the positionInList value of the previously
// largest value and assign to the moved item so it will be ordered last.
} else if (position == (currentList.size() - 1)) {
itemBeforePos = currentList.get(position - 1).getPositionInList();
movedItemNewPos = itemBeforePos 1;
// If dragged somewhere in the middle of list, get the positionInList variable value of
// the items before and after it. They are used to compute the moved item's new
// positionInList value.
} else {
itemBeforePos = currentList.get(position - 1).getPositionInList();
itemAfterPos = currentList.get(position 1).getPositionInList();
// Calculates the moved item's positionInList variable to be half way between the
// item above it and item below it
movedItemNewPos = itemBeforePos ((itemAfterPos - itemBeforePos) / 2.0);
}
updateItemPosInDb(movedItem, movedItemNewPos);
}
private void updateItemPosInDb(ListItem movedItem, double movedItemNewPos) {
movedItem.setPositionInList(movedItemNewPos);
viewModel.update(movedItem); // this updates the database which triggers the onChanged method
}
public void onMoved(@NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder source, int fromPos,
@NonNull RecyclerView.ViewHolder target, int toPos, int x, int y) {
Collections.swap(currentList, toPos, fromPos);
toPosition = toPos; // used in clearView()
adapterMain.notifyItemMoved(fromPos, toPos);
}
}).attachToRecyclerView(recyclerMain);
RecyclerAdapterMain.java
public class RecyclerAdapterMain extends ListAdapter<ListItem, RecyclerAdapterMain.ListItemHolder> {
// Registers MainActivity as a listener to checkbox clicks. Main will update database accordingly.
private CheckBoxListener checkBoxListener;
public interface CheckBoxListener {
void onCheckBoxClicked(ListItem item); // Method implemented in MainActivity
}
public void setCheckBoxListener(CheckBoxListener checkBoxListener) {
this.checkBoxListener = checkBoxListener;
}
public RecyclerAdapterMain() {
super(DIFF_CALLBACK);
}
// Static keyword makes DIFF_CALLBACK variable available to the constructor when it is called
// DiffUtil will compare two objects to determine if updates are needed
private static final DiffUtil.ItemCallback<ListItem> DIFF_CALLBACK =
new DiffUtil.ItemCallback<ListItem>() {
@Override
public boolean areItemsTheSame(@NonNull ListItem oldItem, @NonNull ListItem newItem) {
return oldItem.getId() == newItem.getId();
}
// Documentation - NOTE: if you use equals, your object must properly override Object#equals().
// Incorrectly returning false here will result in too many animations.
// As far as I can tell I am overriding the equals() properly in my POJO below
@Override
public boolean areContentsTheSame(@NonNull ListItem oldItem, @NonNull ListItem newItem) {
return oldItem.equals(newItem);
}
};
@NonNull
@Override
public ListItemHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.recycler_item_layout_main, parent, false);
return new ListItemHolder(itemView);
}
@Override
public void onBindViewHolder(@NonNull ListItemHolder holder, int position) {
ListItem item = getItem(position);
Resources resources = holder.itemView.getContext().getResources();
holder.txtItemName.setText(item.getItemName());
holder.checkBox.setChecked(item.getIsChecked());
// Set the item to "greyed out" if checkbox is checked, normal color otherwise
if(item.getIsChecked()) {
holder.txtItemName.setTextColor(Color.LTGRAY);
holder.checkBox.setButtonTintList(ColorStateList
.valueOf(resources.getColor(R.color.checkedColor, null)));
} else {
holder.txtItemName.setTextColor(Color.BLACK);
holder.checkBox.setButtonTintList(ColorStateList
.valueOf(resources.getColor(R.color.uncheckedColor, null)));
}
}
public class ListItemHolder extends RecyclerView.ViewHolder {
private TextView txtItemName;
private CheckBox checkBox;
public ListItemHolder(@NonNull View itemView) {
super(itemView);
txtItemName = itemView.findViewById(R.id.txt_item_name);
// Toggle checkbox state
checkBox = itemView.findViewById(R.id.checkBox);
checkBox.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
checkBoxListener.onCheckBoxClicked(getItem(getAdapterPosition()));
}
});
}
}
public ListItem getItemAt(int position) {
return getItem(position);
}
}
ListItem.java (POJO)
@Entity(tableName = "list_item_table")
public class ListItem {
@PrimaryKey(autoGenerate = true)
private long id;
private String itemName;
private boolean isChecked;
private double positionInList;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getItemName() {
return itemName;
}
public void setItemName(String itemName) {
this.itemName = itemName;
}
public void setChecked(boolean isChecked) {
this.isChecked = isChecked;
}
public boolean getIsChecked() {
return isChecked;
}
public void setPositionInList(double positionInList) {
this.positionInList = positionInList;
}
public double getPositionInList() {
return positionInList;
}
@Override
public boolean equals(@Nullable Object obj) {
ListItem item = new ListItem();
if(obj instanceof ListItem) {
item = (ListItem) obj;
}
return this.getItemName().equals(item.getItemName()) amp;amp;
this.getIsChecked() == item.getIsChecked() amp;amp;
this.getPositionInList() == item.getPositionInList();
}
}
Комментарии:
1. Вы уверены, что ваш
equals(..)
правильный? Что произойдет, когда вы это сделаете.if(!(obj instanceof ListItem)) return false; return this.id == ((ListItem)obj).id
? Обязательно установите уникальный идентификатор для каждогоListItem
.2. После модульного тестирования мой оригинальный метод equals ведет себя так, как ожидалось.
3. Найдено какое-либо решение?
4. К сожалению, нет. Вы тоже испытываете это дурацкое поведение?