如何防止回收视图项目闪烁后通知 ItemChanged (pos) ?

我目前有一个回收视图的数据每5秒更新一次。为了更新列表中的数据,我使用

notifyItemChanged(position);
notifyItemRangeChanged(position, mList.size());

每次我调用 notifyItemChanged ()时,回收器视图中的项目都会正确地更新,但是它会闪烁,因为这会导致再次调用 onBindViewHolder。就好像每次都是新鲜的一样。如果可能的话,我如何防止这种情况发生?

43797 次浏览

RecyclerView has built in animations which usually add a nice polished effect. in your case you'll want to disable them:

((SimpleItemAnimator) mRecyclerView.getItemAnimator()).setSupportsChangeAnimations(false);

(The default recycler view animator should already be an instance of SimpleItemAnimator)

For Kotlin,

(mRecyclerView?.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false

You can disable the item animations.

mRecyclerView.setItemAnimator(null);

The problem may not come from animation but from not stable id of the list item.

To use stable IDs, you need to:

- setHasStableIds(true)
In RecyclerView.Adapter, we need to set setHasStableIds(true); true means this adapter would publish a unique value as a key for item in data set. Adapter can use the key to indicate they are the same one or not after notifying data changed.

- override getItemId(int position)
Then we must override getItemId(int position), to return identified long for the item at position. We need to make sure there is no different item data with the same returned id.

The source of solution for that is here.

I read the below solution and implemented and it works fine on every device I tested.

  • Clone Default animator class.
  • In animateChange() method, comment below 3 lines,

    final float prevAlpha = oldHolder.itemView.getAlpha();
    .
    .
    oldHolder.itemView.setAlpha(prevAlpha);
    .
    .
    newHolder.itemView.setAlpha(0);
    
  • Set recyclerview item animator to that of your cloned class.

//Note: I do understand how the solution works with not changing the alpha value of new holder but this is not my solution, I read this on stackoverflow itself but for some reason could not find it anymore. Sharing this to help out fellow developers.

Use stableId in your adapter.

Call adapter.setHasStableIds(true) and override getItemId(int position) method in your adapter class.

Also, return some unique id from getItemId(int position) for each item. Don't just simply return position.

In my case, neither any of above nor the answers from other stackoverflow questions having same problems worked.

Well, I was using custom animation each time the item gets clicked, for which I was calling notifyItemChanged(int position, Object Payload) to pass payload to my CustomAnimator class.

Notice, there are 2 onBindViewHolder(...) methods available in RecyclerView Adapter. onBindViewHolder(...) method having 3 parameters will always be called before onBindViewHolder(...) method having 2 parameters.

Generally, we always override the onBindViewHolder(...) method having 2 parameters and the main root of problem was I was doing the same, as each time notifyItemChanged(...) gets called, our onBindViewHolder(...) method will be called, in which I was loading my image in ImageView using Picasso, and this was the reason it was loading again regardless of its from memory or from internet. Until loaded, it was showing me the placeholder image, which was the reason of blinking for 1 sec whenever I click on the itemview.

Later, I also override another onBindViewHolder(...) method having 3 parameters. Here I check if the list of payloads is empty, then I return the super class implementation of this method, else if there are payloads, I am just setting the alpha value of the itemView of holder to 1.

And yay I got the solution to my problem after wasting a one full day sadly!

Here's my code for onBindViewHolder(...) methods:

onBindViewHolder(...) with 2 params:

@Override
public void onBindViewHolder(@NonNull RecyclerAdapter.ViewHolder viewHolder, int position) {
Movie movie = movies.get(position);


Picasso.with(context)
.load(movie.getImageLink())
.into(viewHolder.itemView.posterImageView);
}

onBindViewHolder(...) with 3 params:

@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) {
if (payloads.isEmpty()) {
super.onBindViewHolder(holder, position, payloads);
} else {
holder.itemView.setAlpha(1);
}
}

Here's the code of method I was calling in onClickListener of viewHolder's itemView in onCreateViewHolder(...):

private void onMovieClick(int position, Movie movie) {
Bundle data = new Bundle();
data.putParcelable("movie", movie);


// This data(bundle) will be passed as payload for ItemHolderInfo in our animator class
notifyItemChanged(position, data);
}

Note: You can get this position by calling getAdapterPosition() method of your viewHolder from onCreateViewHolder(...).

I have also overridden getItemId(int position) method as follows:

@Override
public long getItemId(int position) {
Movie movie = movies.get(position);
return movie.getId();
}

and called setHasStableIds(true); on my adapter object in activity.

Hope this helps if none of the answers above work!

If you want to keep RecyclerView's animation and avoid item flash in the same time, you can follow the steps below:

1. change view component status directly 2. change data in Adapter directly 3. don't need to refresh UI manually by calling notifyItemChanged()

Just change the view directly would not make change immediately, the better way is using payload to do it.

In your adapter:

override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int,
payloads: MutableList<Any>
) {
// Using payload to refresh partly
if (payloads.isNullOrEmpty()) {
// refresh all
onBindViewHolder(holder, position)
return
}
// just change one component's status which would not "flash".
(holder as YourHolder).apply{
// make you view change
// etc. checkBox.isChecked = true
}
}

And using it:


yourAdapter.notifyItemChanged(position, "sample_pay_load")


Here is notifyitemchanged's Doc

This happening because Adapter is creating new ViewHolder and trying to animate between old and new ViewHolders. That's why all the "disable animations" answers technically work.

Instead you can just tell the RecyclerView to re-use Viewholders with canReuseUpdatedViewHolder(). See this answer to a similar question: https://stackoverflow.com/a/60427676/1650674

Additionaly, try to run each notifyItemChanged in a separate launched coroutine job inside Main scope. It worked perfectly for me removing blinking significantly.

for (i in 0 until adapter.itemCount) {
CoroutineScope(Main).launch {
adapter.notifyItemChanged(i)
}
}

If only the content inside the view needs changes then would suggest using notifyItemChanged with payload. If the whole view type changes then notifyItemChanged without payload will work but will re-render the view giving a flicker effect. Here is the code.

adapter.notifyItemChanged(position, "CHANGE_TEXT_VIEW_ITEM");

And then in the RecyclerViewAdapter override onBindViewHolder which has payload with it.

@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
if (payloads.isEmpty()) {
super.onBindViewHolder(holder, position, payloads);
} else {
for (Object payload : payloads) {
if (payload.equals("CHANGE_TEXT_VIEW_ITEM")) {
//you have holder and position to do relevant changes
}
}
}
}

This will update the recycler items without any flicker. This can be achieved for notifyItemRangeChanged as well.