如何在使用notifyItemChanged(pos)后防止RecyclerView项目闪烁?

86

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

notifyItemChanged(position);
notifyItemRangeChanged(position, mList.size());
每次调用notifyItemChanged()时,我的回收站视图上的项目会正确更新,但是它会闪烁,因为这会导致onBindViewHolder再次被调用。所以每次都像是新加载一样。如果可能的话,我该如何防止这种情况发生?

我正在使用 Glide 加载图像,尝试了所有答案来解决闪烁问题,但都没有起作用。后来使用 Picasso 解决了这个问题,并且运行非常流畅。 - Muklas
请注意这个 https://dev59.com/hl0b5IYBdhLWcg3wUP0X#32488059 - Konstantin Konopko
请查看此文章:https://blog.undabot.com/recyclerview-time-to-animate-with-payloads-and-diffutil-4278beb8d4dd - Ievgen
我对这个有问题,我尝试了一切,将supportChangeAnimations设置为false,使用stableIds,diffutils,都没有起作用,还有其他解决方案吗? - Limun
11个回答

146

RecyclerView内置了动画效果,通常会增加不错的效果。在你的情况下,你需要禁用它们:

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

(默认的回收视图动画程序应该已经是 SimpleItemAnimator 的实例)

对于 Kotlin,

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

1
是的!这正是我所需要的。 - portfoliobuilder
14
我认为这也会禁用诸如notifyItemInserted等方法的默认动画。 - user6650650
@user6650650 对我来说可以。而且你也是正确的。谢谢! - portfoliobuilder
2
对我来说非常棒! - Begginer

59

您可以禁用项目动画。

mRecyclerView.setItemAnimator(null);

5
这似乎使得使用databinding的Flickr比以前更快,StableIds是合理的优化,但仍然会在更新的行上出现Flickr(它们有单选按钮改变状态,但视图的其余部分没有变化,所以不应该Flickr)。 - hmac
1
这是最好的答案。一次性修复了几个漏洞。 - Master

22

问题可能不是来自动画,而是来自于列表项的不稳定ID

要使用稳定的ID,您需要:

  • setHasStableIds(true)
    在RecyclerView.Adapter中,我们需要设置setHasStableIds(true); true表示此适配器将发布唯一值作为数据集中项目的键。适配器可以使用该键来指示它们是否相同或不同,以便在通知数据更改后使用。

  • override getItemId(int position)
    然后,我们必须重写getItemId(int position),以返回位置处项目的标识长整型。我们需要确保没有具有相同返回ID的不同项目数据。

解决方案的来源在这里


6
应该接受这个答案。将setAnimator设置为null是一个临时解决方案,对于那些想在他们的RecyclerView中使用动画的人不适用。 - mochadwi
1
当然应该,但是...欢迎来到SO。 - Marian Paździoch
1
对我来说不起作用。 - Tara
什么不起作用? - Marian Paździoch

9

如果你想保持RecyclerView的动画效果,又不想看到闪烁的条目,可以按照以下步骤进行:

1. 直接更改视图组件状态 2. 直接更改适配器中的数据 3. 不需要通过调用notifyItemChanged()手动刷新UI

直接更改视图并不能立即生效,更好的方法是使用payload

在你的适配器中:

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

并且使用它:


yourAdapter.notifyItemChanged(position, "sample_pay_load")

这里是 notifyitemchanged的文档


使用有效载荷作为部分更新的指示是一个不错的解决方法。 - A. Petrov
其中一种优雅的解决方案。不错 (Y) - Mushahid Gillani

7

在您的适配器中使用 stableId .

调用 adapter.setHasStableIds(true)并覆盖适配器类中的 getItemId(int position)方法。

此外,对于每个项目从 getItemId(int position)返回一些唯一的 id。不要简单地返回位置。


7
对我来说没有任何改变。当使用一个按钮更新一项时,这个项目仍然会闪烁。 - Andrew

5
这是因为Adapter正在创建新的ViewHolder,并尝试在旧的和新的ViewHolders之间进行动画处理。这就是为什么所有“禁用动画”答案在技术上都有效的原因。
相反,您可以使用canReuseUpdatedViewHolder()告诉RecyclerView重复使用Viewholders。请参阅类似问题的答案: https://dev59.com/Buo6XIcBkEYKwwoYFQLr#60427676

3
我阅读了下面的解决方案并实施了它,在我测试的每个设备上都可以正常工作。
  • 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.

//注意:我了解如何在不改变新持有者的alpha值的情况下使解决方案工作,但这不是我的解决方案。我在stackoverflow上阅读过这个解决方案,但由于某种原因无法再找到它。分享这个解决方案是为了帮助其他开发人员。


我认为这是最好的答案之一。谢谢! - TootsieRockNRoll
我可能做错了什么或者这个方法不起作用。我扩展了DefaultItemAnimator()类并重写了animateChange(),但是没有任何变化。 - Andrew

2
在我的情况下,以上任何一种方法和其他stackoverflow问题的答案都没有起作用。
我每次点击项目时都使用自定义动画,为此我调用notifyItemChanged(int position,Object Payload)将负载传递给我的CustomAnimator类。
请注意,RecyclerView Adapter中有2个可用的onBindViewHolder(...)方法。始终会在具有3个参数的onBindViewHolder(...)方法之前调用具有2个参数的onBindViewHolder(...)方法。
通常,我们总是重写具有2个参数的onBindViewHolder(...)方法,而主要问题的根源是我也在做同样的事情,因为每次调用notifyItemChanged(...),我们的onBindViewHolder(...)方法都会被调用,在其中我使用Picasso在ImageView中加载我的图像,这就是它无论从内存还是从互联网加载时都会重新加载的原因。在加载之前,它会向我显示占位符图像,这就是每当我单击itemview时闪烁1秒钟的原因。
后来,我还覆盖了另一个具有3个参数的onBindViewHolder(...)方法。在这里,如果负载列表为空,则返回此方法的超类实现,否则,如果存在负载,则仅将持有人的itemView的alpha值设置为1。
很遗憾,我浪费了整整一天的时间,但是我找到了解决问题的方法!
以下是我的onBindViewHolder(...)方法的代码:
具有2个参数的onBindViewHolder(...):
@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);
    }

有 3 个参数的 onBindViewHolder(...):

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

以下是我在onCreateViewHolder(...)方法中的viewHolder的itemView的onClickListener中调用的代码:

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

注意:您可以通过在onCreateViewHolder(...)中调用viewHolder的getAdapterPosition()方法来获取此位置。

我还重写了getItemId(int position)方法,如下所示:

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

如果以上答案都不起作用,我希望这个能帮到你:在活动中调用setHasStableIds(true);来设置我的适配器对象。

希望这可以帮助你!


这对我有用,我还注意到在动画期间减少的是项目的 alpha。这修复了它,谢谢! - Jeroen Flietstra

1
此外,尝试在主作用域内使用单独启动的协程作业运行每个notifyItemChanged。对我来说非常有效,可以显著减少闪烁。
for (i in 0 until adapter.itemCount) {
    CoroutineScope(Main).launch {
        adapter.notifyItemChanged(i)
    }
}

0
如果只有视图内部的内容需要更改,那么建议使用带有负载的 notifyItemChanged。如果整个视图类型发生变化,则可以使用不带负载的 notifyItemChanged,但会重新渲染视图,产生闪烁效果。以下是代码。
adapter.notifyItemChanged(position, "CHANGE_TEXT_VIEW_ITEM");

然后在RecyclerViewAdapter中覆盖带有有效负载的onBindViewHolder方法。

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

这将更新回收站项目而不会出现任何闪烁。同样可以通过notifyItemRangeChanged实现。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接