RecyclerView row flashes/blinks when item inserted

2.3k Views Asked by At

Any time I insert an item into my Room database, my recycler view flashes the row containing the newly inserted view holder (the rest of the list does not flash). I am using LiveData to keep my list automatically updated by calling submitList(list) in onChanged() of the observed ViewModel method. My adapter extends ListAdapter and I am using DiffUtil to track changes in the list. That being said, I don't call notifyItemInserted(position) directly as DiffUtil should do this for me. There are 2 instances where an item gets inserted (1) a completely new item gets inserted at the end of the list (2) a deleted item gets reinserted into the list. Both cases the item will insert itself then flash. I have read numerous posts where people suggest disabling animations on the recycler view but this is not an option for me as I rely on animations elsewhere in my code. Any other suggestions would be appreciated. I'm trying to keep the posted code brief but I can post more if it would be helpful.

MainActivity.java

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

@Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    initComponents();
    initRecyclerView();
    setListeners();
    setListObserver();
    createItemTouchHelper();
  }

  private void setListObserver() {
    viewModel.getAllItems().observe(this, new Observer<List<ListItem>>() {
      @Override
      public void onChanged(List<ListItem> newList) {
        adapterMain.submitList(newList);
      }
    });
  }

...

// Inserts a new ListItem when MainActivity's EditText is used
  public void onClick(View v) {
    if (v.getId() == R.id.img_add_item_main) {
        String itemName = String.valueOf(edtAddItem.getText());
        if (!itemName.trim().equals("")) { // Insert new list item only if the EditText is not empty
          ListItem item = new ListItem();
          item.setItemName(itemName);
          viewModel.insert(item);
        }

...

// SnackBar to allow a user to undo a delete operation
  public void showUndoSnackBar(ListItem deletedItem) {
    Snackbar undoSnackBar = Snackbar.make(constraintLayout, "Undo deleted Item",
        Snackbar.LENGTH_LONG).setAction("UNDO", new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        // Restore deleted item to its original position in the list if UNDO is clicked
        viewModel.insert(deletedItem);
      }
    });
    undoSnackBar.show();
  }

RecyclerAdapterMain.java

public class RecyclerAdapterMain extends ListAdapter<ListItem, RecyclerAdapterMain.ListItemHolder> {

  public RecyclerAdapterMain() {
    super(DIFF_CALLBACK);
  }

    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();
    }

    @Override
    public boolean areContentsTheSame(@NonNull ListItem oldItem, @NonNull ListItem newItem) {   
      return oldItem.equals(newItem);
    }

@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);

    holder.txtItemName.setText(item.getItemName());
    holder.checkBox.setChecked(item.getIsChecked());

    if(item.getIsChecked()) {
      holder.txtItemName.setTextColor(Color.LTGRAY);
    } else {
      holder.txtItemName.setTextColor(Color.BLACK);
    }
  }

...

ListItem.java (POJO)

@Entity(tableName = "list_item_table")
public class ListItem {

  @PrimaryKey(autoGenerate = true)
  private long id;

  private String itemName;
  private boolean isChecked;

  public long getId() {
    return id;
  }

  public void setId(long id) {
    this.id = id;
  }

  ... other getters and setters

  public boolean equals(@Nullable ListItem listItem) {
    return this.itemName.equals(listItem.getItemName()) && this.isChecked == listItem.getIsChecked();
  }
}
1

There are 1 best solutions below

0
On BEST ANSWER

I determined that the DefaultItemAnimator class (which is the animator for RecyclerView at the time of this writing) has a method called animateAdd(final RecyclerView.ViewHolder holder) that sets the holder's alpha to 0 then animates it to 1 over time. I verified that this was the cause of the blinking by changing the default setting to 1. I solved the problem by a using a combination of the accepted answer from this StackOverflow post and the documentation for DefaultItemAnimator.

First, I created a new animator class that extends DefaultItemAnimator in order to override the animateAdd() method. Under the section for animateAdd(), the documentation states, "Called when an item is added to the RecyclerView. Implementors can choose whether and how to animate that change, but must always call dispatchAddFinished(RecyclerView.ViewHolder) when done, either immediately (if no animation will occur) or after the animation actually finishes." I called dispatchAddFinished() immediately to avoid the add animation. Instead of disabling animations altogether, all animations are present except for the problematic one.

MyRecyclerViewAnimator.java

public class MyRecyclerViewAnimator extends DefaultItemAnimator {

  @Override
  public boolean animateAdd(RecyclerView.ViewHolder holder) {
    dispatchAddFinished(holder); // this is what bypasses the animation
    return true;
  }

/* this is the default implementation of animateAdd() in DefaultItemAnimator
@Override
    public boolean animateAdd(final RecyclerView.ViewHolder holder) {
        resetAnimation(holder);
        holder.itemView.setAlpha(0); // this is what caused the flashing/blinking
        mPendingAdditions.add(holder);
        return true;
    }
*/
}

MainActivity.java

...

  recyclerMain.setItemAnimator(new MyRecyclerViewAnimator()); // set the default animator to your extended class

...