Ali
Ali

Reputation: 9994

RecyclerView blinking after notifyDatasetChanged()

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?

Upvotes: 154

Views: 95437

Answers (22)

UnreachableCode
UnreachableCode

Reputation: 1791

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.

Upvotes: 0

Phil
Phil

Reputation: 4870

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

Upvotes: 0

Thiago
Thiago

Reputation: 13302

Try this in Kotlin

binding.recyclerView.apply {
    (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}

Upvotes: 4

mahmood
mahmood

Reputation: 399

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

Upvotes: 0

Burchik
Burchik

Reputation: 1

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

Upvotes: 0

Robert Pal
Robert Pal

Reputation: 820

Kotlin solution:

(recyclerViewIdFromXML.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false

Upvotes: 6

Tom
Tom

Reputation: 360

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" />

Upvotes: 2

Parth Bhanushali
Parth Bhanushali

Reputation: 91

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!

Upvotes: 2

Gregory
Gregory

Reputation: 987

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()
    // ...
}

Upvotes: 5

Farruh Habibullaev
Farruh Habibullaev

Reputation: 2392

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

Upvotes: 1

Wesely
Wesely

Reputation: 1475

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.

Upvotes: 16

Anatoly  Vdovichev
Anatoly Vdovichev

Reputation: 1717

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.

Upvotes: 171

Sreedhu Madhu
Sreedhu Madhu

Reputation: 2468

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!!

Upvotes: 0

Sabeer
Sabeer

Reputation: 4120

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

Upvotes: 124

vvbYWf0ugJOGNA3ACVxp
vvbYWf0ugJOGNA3ACVxp

Reputation: 1116

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

Upvotes: 5

Pramod
Pramod

Reputation: 1133

for me recyclerView.setHasFixedSize(true); worked

Upvotes: 0

Deefer
Deefer

Reputation: 41

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

Upvotes: 0

Hamzeh Soboh
Hamzeh Soboh

Reputation: 7710

This simply worked:

recyclerView.getItemAnimator().setChangeDuration(0);

Upvotes: 51

Mohamed Farouk
Mohamed Farouk

Reputation: 183

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)

Upvotes: 12

developer_android
developer_android

Reputation: 1

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

Upvotes: 0

Android learner
Android learner

Reputation: 1871

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.

Upvotes: 1

yigit
yigit

Reputation: 38253

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.

Upvotes: 4

Related Questions