151

I have a RecyclerView which loads some data from API, includes an image url and some data, and I use networkImageView to lazy load image.

@Override
public void onResponse(List<Item> response) {
   mItems.clear();
   for (Item item : response) {
      mItems.add(item);
   }
   mAdapter.notifyDataSetChanged();
   mSwipeRefreshLayout.setRefreshing(false);
}

Here is implementation for Adapter:

public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, final int position) {
        if (isHeader(position)) {
            return;
        }
        // - get element from your dataset at this position
        // - replace the contents of the view with that element
        MyViewHolder holder = (MyViewHolder) viewHolder;
        final Item item = mItems.get(position - 1); // Subtract 1 for header
        holder.title.setText(item.getTitle());
        holder.image.setImageUrl(item.getImg_url(), VolleyClient.getInstance(mCtx).getImageLoader());
        holder.image.setErrorImageResId(android.R.drawable.ic_dialog_alert);
        holder.origin.setText(item.getOrigin());
    }

Problem is when we have refresh in the recyclerView, it is blincking for a very short while in the beginning which looks strange.

I just used GridView/ListView instead and it worked as I expected. There were no blincking.

configuration for RecycleView in onViewCreated of my Fragment:

mRecyclerView = (RecyclerView) view.findViewById(R.id.recyclerView);
        // use this setting to improve performance if you know that changes
        // in content do not change the layout size of the RecyclerView
        mRecyclerView.setHasFixedSize(true);

        mGridLayoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager();
        mGridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(int position) {
                return mAdapter.isHeader(position) ? mGridLayoutManager.getSpanCount() : 1;
            }
        });

        mRecyclerView.setAdapter(mAdapter);

Anyone faced with such a problem? what could be the reason?

Ali
  • 9,800
  • 19
  • 72
  • 152
  • Possible duplicate of [Disable onChange animations on ItemAnimator for RecyclerView](https://stackoverflow.com/questions/35766497/disable-onchange-animations-on-itemanimator-for-recyclerview) – Apfelsaft23 Nov 24 '18 at 15:06

22 Answers22

169

Try using stable IDs in your RecyclerView.Adapter

setHasStableIds(true) and override getItemId(int position).

Without stable IDs, after notifyDataSetChanged(), ViewHolders usually assigned to not to same positions. That was the reason of blinking in my case.

You can find a good explanation here.

Paul Spiesberger
  • 5,630
  • 1
  • 43
  • 53
Anatoly Vdovichev
  • 1,697
  • 1
  • 10
  • 4
  • 3
    I added setHasStableIds(true), which solved the flickering, but in my GridLayout, items still change place at scrolling. What should I override in getItemId()? Thanks – Balázs Orbán Jul 03 '17 at 23:20
  • 4
    Any idea on how should we generate the ID? – Mauker Jul 25 '17 at 16:34
  • You are awesome! Google is NOT. Google either don't know about this OR they know and don't want us to know ! THANK YOU MAN – MBH Aug 04 '17 at 23:22
  • How do you override getitemposition? I mean what code do you override with? – John Aug 17 '17 at 13:52
  • 1
    mess up the items positions , items comes from firebase database in correct order. – MuhammadAliJr Jul 07 '18 at 22:09
  • working for me! @Override public long getItemId(int position) { return position; } – Chanh Mar 26 '19 at 07:52
  • @Anatoly Vdovichev , what is "pid" in "return product.pid;" in your link? – truong luu Jul 04 '19 at 01:06
  • 1
    @Mauker If your object has a unique number you can use that or if you have a unique string you can use object.hashCode(). That works perfectly fine for me – Simon Schubert Sep 11 '19 at 14:44
  • This way notifyItemChanged is not working properly. I use ItemTouchHelper for swipe to delete funcitonality, and if a decline the deletion and call notifyItemChanged the item is not reset to its previous state. Without adding hasStableIds it works as expected, but the flickering is present :( – AceStan Oct 27 '21 at 08:45
  • Not working even after override getItemId(int position) – Dhara Patel Mar 31 '22 at 09:54
121

According to this issue page ....it is the default recycleview item change animation... You can turn it off.. try this

recyclerView.getItemAnimator().setSupportsChangeAnimations(false);

Change in latest version

Quoted from Android developer blog:

Note that this new API is not backward compatible. If you previously implemented an ItemAnimator, you can instead extend SimpleItemAnimator, which provides the old API by wrapping the new API. You’ll also notice that some methods have been entirely removed from ItemAnimator. For example, if you were calling recyclerView.getItemAnimator().setSupportsChangeAnimations(false), this code won’t compile anymore. You can replace it with:

ItemAnimator animator = recyclerView.getItemAnimator();
if (animator instanceof SimpleItemAnimator) {
  ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
}
Sufian
  • 6,405
  • 16
  • 66
  • 120
Sabeer
  • 3,940
  • 1
  • 24
  • 20
50

This simply worked:

recyclerView.getItemAnimator().setChangeDuration(0);
Hamzeh Soboh
  • 7,572
  • 5
  • 43
  • 54
  • it's a good alternative to `code` ItemAnimator animator = recyclerView.getItemAnimator(); if (animator instanceof SimpleItemAnimator) { ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false); } `code` – ziniestro Sep 14 '16 at 02:40
  • 2
    stops all the animations ADD and REMOVE – MBH Aug 04 '17 at 23:23
  • Works for flickering issue from backgroung selector xml – Suchith Nov 10 '21 at 06:34
16

I have the same issue loading image from some urls and then imageView blinks. Solved by using

notifyItemRangeInserted()    

instead of

notifyDataSetChanged()

which avoids to reload those unchanged old datas.

Wesely
  • 1,425
  • 14
  • 23
12

try this to disable the default animation

ItemAnimator animator = recyclerView.getItemAnimator();

if (animator instanceof SimpleItemAnimator) {
  ((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
}

this the new way to disable the animation since android support 23

this old way will work for older version of the support library

recyclerView.getItemAnimator().setSupportsChangeAnimations(false)
Mohamed Farouk
  • 183
  • 2
  • 4
6

Kotlin solution:

(recyclerViewIdFromXML.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
Robert Pal
  • 714
  • 1
  • 7
  • 15
5

Recyclerview uses DefaultItemAnimator as it's default animator. As you can see from the code below, they change the alpha of the view holder upon item change:

@Override
public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromX, int fromY, int toX, int toY) {
    ...
    final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);
    ...
    ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);
    if (newHolder != null) {
        ....
        ViewCompat.setAlpha(newHolder.itemView, 0);
    }
    ...
    return true;
}

I wanted to retain the rest of the animations but remove the "flicker" so I cloned DefaultItemAnimator and removed the 3 alpha lines above.

To use the new animator just call setItemAnimator() on your RecyclerView:

mRecyclerView.setItemAnimator(new MyItemAnimator());
vvbYWf0ugJOGNA3ACVxp
  • 1,086
  • 10
  • 23
5

In Kotlin you can use 'class extension' for RecyclerView:

fun RecyclerView.disableItemAnimator() {
    (itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
}

// sample of using in Activity:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                          savedInstanceState: Bundle?): View? {
    // ...
    myRecyclerView.disableItemAnimator()
    // ...
}
Gregory
  • 802
  • 10
  • 16
4

Assuming mItems is the collection that backs your Adapter, why are you removing everything and re-adding? You are basically telling it that everything has changed, so RecyclerView rebinds all views than I assume the Image library does not handle it properly where it still resets the View even though it is the same image url. Maybe they had some baked in solution for AdapterView so that it works fine in GridView.

Instead of calling notifyDataSetChanged which will cause re-binding all views, call granular notify events (notify added/removed/moved/updated) so that RecyclerView will rebind only necessary views and nothing will flicker.

Ali
  • 9,800
  • 19
  • 72
  • 152
yigit
  • 37,683
  • 13
  • 72
  • 58
  • 4
    Or maybe it is just a "bug" in RecyclerView? Obviously if it worked fine for last 6 years with AbsListView and now it doesn't with RecyclerView, means something is not OK with RecyclerView, right? :) Quick look into it shows that when you refresh data in ListView and GridView they keep track of view+position, so when you refresh you will get exactly the same viewholder. While RecyclerView shuffles view holders, which leads in flickering. – vovkab May 20 '15 at 17:09
  • Working on ListView *does not* mean it is correct for RecyclerView. These components have different architectures. – yigit May 20 '15 at 19:27
  • 1
    Agree, but sometimes it is really hard to know what items is changed, for example if you use cursors or you just refresh you whole data. So recyclerview should handle this case correctly too. – vovkab May 20 '15 at 20:58
4

Try this in Kotlin

binding.recyclerView.apply {
    (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
Thiago
  • 12,778
  • 14
  • 93
  • 110
2

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!

2

In my case there was a much simpler problem, but it can look/feel very much like the problem above. I had converted an ExpandableListView to a RecylerView with Groupie (using Groupie's ExpandableGroup feature). My initial layout had a section like this:

<androidx.recyclerview.widget.RecyclerView
  android:id="@+id/hint_list"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:background="@android:color/white" />

With layout_height set to "wrap_content" the animation from expanded group to collapsed group felt like it would flash, but it was really just animating from the "wrong" position (even after trying most of the recommendations in this thread).

Anyway, simply changing layout_height to match_parent like this fixed the problem.

<androidx.recyclerview.widget.RecyclerView
  android:id="@+id/hint_list"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:background="@android:color/white" />
Tom
  • 290
  • 3
  • 9
1

Hey @Ali it might be late replay. I also faced this issue and solved with below solution, it may help you please check.

LruBitmapCache.java class is created to get image cache size

import android.graphics.Bitmap;
import android.support.v4.util.LruCache;
import com.android.volley.toolbox.ImageLoader.ImageCache;

public class LruBitmapCache extends LruCache<String, Bitmap> implements
        ImageCache {
    public static int getDefaultLruCacheSize() {
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        final int cacheSize = maxMemory / 8;

        return cacheSize;
    }

    public LruBitmapCache() {
        this(getDefaultLruCacheSize());
    }

    public LruBitmapCache(int sizeInKiloBytes) {
        super(sizeInKiloBytes);
    }

    @Override
    protected int sizeOf(String key, Bitmap value) {
        return value.getRowBytes() * value.getHeight() / 1024;
    }

    @Override
    public Bitmap getBitmap(String url) {
        return get(url);
    }

    @Override
    public void putBitmap(String url, Bitmap bitmap) {
        put(url, bitmap);
    }
}

VolleyClient.java singleton class [extends Application] added below code

in VolleyClient singleton class constructor add below snippet to initialize the ImageLoader

private VolleyClient(Context context)
    {
     mCtx = context;
     mRequestQueue = getRequestQueue();
     mImageLoader = new ImageLoader(mRequestQueue,getLruBitmapCache());
}

I created getLruBitmapCache() method to return LruBitmapCache

public LruBitmapCache getLruBitmapCache() {
        if (mLruBitmapCache == null)
            mLruBitmapCache = new LruBitmapCache();
        return this.mLruBitmapCache;
}

Hope its going to help you.

Android learner
  • 1,871
  • 4
  • 24
  • 36
  • Thanks for your answer. That's what I have exactly done in my VollyClient.java. Take a look at :[VolleyClient.java](https://bitbucket.org/ali-rezaei/twitzer/src/ba60b041fce1abfc271ae4a084381e205c972ee5/app/src/main/java/com/dynamo/android/client/volley/VolleyClient.java?at=master) – Ali Jun 20 '15 at 12:25
  • Just check once with using LruCache class, I think its going to solve your problem. Take a look at [LruCache](http://developer.android.com/reference/android/util/LruCache.html) – Android learner Jun 20 '15 at 14:55
  • Did you check once the code that I shared with you in comment? what do you have in your class that I missed there? – Ali Jun 20 '15 at 15:04
  • You are missing extending LruCache class and overriding sizeOf() method like how I done, remaining all are seems okay to me. – Android learner Jun 20 '15 at 15:11
  • Ok, I give it a try very soon, but could you explain me what have you done there which did the magic for you and solve the problem? [sourcecode](https://android.googlesource.com/platform/frameworks/support.git/+/795b97d901e1793dac5c3e67d43c96a758fec388/v4/java/android/support/v4/util/LruCache.java) For me it sounds like your overriden sizeOf method should be in the source code. – Ali Jun 20 '15 at 16:48
  • Are you mentioning a bug in source code? What is your solution in Sizeof() method and why it is not in the source? Just look at Official Android developer MySingleton class for volley (https://developer.android.com/training/volley/requestqueue.html). – Ali Jun 20 '15 at 16:50
  • I tried with your code [Twitzer](https://bitbucket.org/ali-rezaei/twitzer/src/ba60b041fce1?at=master), I am not facing blinking problem at all, but images are taking little time to load. – Android learner Jun 23 '15 at 03:54
  • I bet you did not disable animation for recycleView items when it get notified about change in data set. That's why you can not see the problem. – Ali Jul 15 '15 at 21:00
  • I appreciate a lot that you shared a solution with me, but I will be happier if you could explain me about why your overridden sizeOf method should solve the problem, instead of encouraging me to just paste your code, and that is the main purpose of asking this question. – Ali Jul 15 '15 at 21:03
1

Try to use the stableId in recycler view. The following article briefly explains it

https://medium.com/@hanru.yeh/recyclerviews-views-are-blinking-when-notifydatasetchanged-c7b76d5149a2

Farruh Habibullaev
  • 2,342
  • 1
  • 26
  • 33
0

I had similar issue and this worked for me You can call this method to set size for image cache

private int getCacheSize(Context context) {

    final DisplayMetrics displayMetrics = context.getResources().
            getDisplayMetrics();
    final int screenWidth = displayMetrics.widthPixels;
    final int screenHeight = displayMetrics.heightPixels;
    // 4 bytes per pixel
    final int screenBytes = screenWidth * screenHeight * 4;

    return screenBytes * 3;
}
ketan
  • 19,129
  • 42
  • 60
  • 98
0

for my application, I had some data changing but I didn't want the entire view to blink.

I solved it by only fading the oldview down 0.5 alpha and starting the newview alpha at 0.5. This created a softer fading transition without making the view disappear completely.

Unfortunately because of private implementations, I couldn't subclass the DefaultItemAnimator in order to make this change so I had to clone the code and make the following changes

in animateChange:

ViewCompat.setAlpha(newHolder.itemView, 0);  //change 0 to 0.5f

in animateChangeImpl:

oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() { //change 0 to 0.5f
thor
  • 21,418
  • 31
  • 87
  • 173
Deefer
  • 41
  • 5
0

for me recyclerView.setHasFixedSize(true); worked

Pramod
  • 1,123
  • 2
  • 12
  • 33
0

Using appropriate recyclerview methods to update views will solve this issue

First, make changes in the list

mList.add(item);
or mList.addAll(itemList);
or mList.remove(index);

Then notify using

notifyItemInserted(addedItemIndex);
or
notifyItemRemoved(removedItemIndex);
or
notifyItemRangeChanged(fromIndex, newUpdatedItemCount);

Hope this will help!!

Sreedhu Madhu
  • 2,480
  • 2
  • 30
  • 40
  • Absolutely not. If you are making several updates by seconds (BLE scanning in my case) it doesn't work at all. I spent one day to update to this shit RecyclerAdapter... Better to keep ArrayAdapter. It's a shame that's not using MVC pattern, but at least it's almost usable. – Gojir4 Jun 04 '18 at 15:01
0

In my case I used SwipeRefresh and RecycleView with viewmodel binding and faced with blinking. Solved with -> Use submitList() to keep the list updated because DiffUtils have done the job, otherwise the list reloading entirely refer to CodeLab https://developer.android.com/codelabs/kotlin-android-training-diffutil-databinding#4

Burchik
  • 1
  • 2
0

when using livedata I solved it with diffUtil This is how I combined diffUtill and databinding with my adapter

class ItemAdapter(
    private val clickListener: ItemListener
) :
    ListAdapter<Item, ItemAdapter.ViewHolder>(ItemDiffCallback()) {

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(clickListener, getItem(position)!!)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return ViewHolder.from(parent)
    }

    class ViewHolder private constructor(val binding: ListItemViewBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(
            clickListener: ItemListener,
            item: Item) {

            binding.item = item
            binding.clickListener = clickListener
            binding.executePendingBindings()
        }

        companion object {
            fun from(parent: ViewGroup): ViewHolder {
                val layoutInflater = LayoutInflater.from(parent.context)
                val view = ListItemViewBinding
                    .inflate(layoutInflater, parent, false)

                return ViewHolder(view)
            }
        }
    }

    class ItemDiffCallback :
        DiffUtil.ItemCallback<Item>() {
        override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
            return oldItem.itemId == newItem.itemId
        }

        override fun getChangePayload(oldItem: Item, newItem: Item): Any? {
            return newItem
        }

        override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
            return oldItem == newItem
        }
    }
}

class ItemListener(val clickListener: (item: Item) -> Unit) {
    fun onClick(item: Item) = clickListener(item)
}
mahmood
  • 369
  • 3
  • 8
0

In my case (shared element transition between one image to a higher resolution image), I added some delay to the item decorator in order to make it less noticeable:

(yourRv.itemAnimator as? DefaultItemAnimator)?.changeDuration = 2000
Phil
  • 4,730
  • 1
  • 41
  • 39
0

My own issue was a very specific problem which I'm going to share some insight for here, though I don't yet fully understand it.

Basically I had a re-usable fragment with RecyclerView, so that I could have a menu with nested menus. Pick an item in the RecyclerView, and it opens another fragment with more options. All facilitated using the JetPack navigation component and data binding using LiveData in the layout xml.

Anyway, here's how I fixed my issue of items flickering when the RecyclerView changed (although it's worth bearing in mind, this was only the appearance as it was a 'new' RecyclerView each time). To update the LiveData list of items (some viewmodels representing objects for the menu items, in my case) I was using LiveData.value = new items. Changing it to postValue(new items) fixed the issue, though I'm not yet sure why.

I've read up the difference between value (setValue in Java) and PostValue, and I understand they're to do with using the main thread or background threading, and the latter only gets applied one time on the main thread when it's ready. But other than that, I'm not sure why this fixed flickering in my RecyclerView. Maybe someone has some insight? In any case, hopefully this will help someone facing a similar problem to me.

Chucky
  • 1,701
  • 7
  • 28
  • 62