Почему анимация перетаскивания повторяется в RecyclerView?

#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. К сожалению, нет. Вы тоже испытываете это дурацкое поведение?