1

I am using the ItemTouchHelper class to support drag and drop in my RecyclerView. While I am dragging an item around it visually updates (swaps rows) as expected. Once I drop the item, another **visual** drag occurs. For example (see diagram below) if I drag item "a" from index 0 to index 3, the correct list shows that item "b" is now at index 0. They recycler view repeats the operation and takes the new item at index 0 ("b") and drags it to index 3! This repeated drag happens no matter what index I drag from or to.

I called it a **visual** drag because the list I am submitting to my RecyclerView's ListAdapter is correctly ordered (verified by logs). And if I restart my app the list is in the correct order. Or if I call notifyDataSetChanged(), after the unwanted animation, it will order itself properly. What could be causing this second animation?

EDIT: According to the documentation, if you use equals() method in your areContentsTheSame() method (DiffUtil), "Incorrectly returning false here will result in too many animations." As far as I can tell, I am properly overriding this method in my POJO file below. I am stumped...

Unexpected reordering of list

Second drag operation

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()) &&
        this.getIsChecked() == item.getIsChecked() &&
        this.getPositionInList() == item.getPositionInList();
  }
}
gig6
  • 307
  • 5
  • 16

0 Answers0