Android RecyclerView在数据集变化时创建和绑定所有视图

19

我有一个非常大的应用程序,几乎在所有地方都使用了RecyclerViews。我知道如何实现RecyclerViews,并且从来没有遇到过任何问题!但是最近,在一个RV上出现了非常糟糕的延迟(让我向您保证,我的bindview方法完全正确,我在异步任务中加载所有图像等...),并且在第一次显示RV时,我会收到ANR错误!

经过数小时的调试,我发现每次数据集更改(notifyDataset()),包括RV首次填充时,所有视图都会同时被创建和绑定(是的,约200次onCreateView()被调用,这与RecyclerView的理念完全相反!)

一旦所有视图都被创建和绑定,应用程序开始正常运行,我的意思是没有更多的onCreateView()和as required onBindView()调用,也没有任何延迟。

我知道很难找到原因,但我想也许有明显的设置或...会导致这种情况,或者SO用户的某些过去经验可以帮助我。


编辑

显然,原因是由于我的XML及如何使RV填充空白区域造成的,因为当我将其高度设置为指定值(例如400 dp)时,一切都可以正常工作,但是如果我使用LinearLayout和layout_weight 或者使用RelativeLayout(设置为以下方对齐元素,并以上方对齐底部的元素)会重新创建此问题。

所以我可以说,当需要动态计算RV的高度时,就会出现此问题,并且我猜测它会创建所有子项来计算其高度,显然这是支持库中的一个错误(在当时的com.android.support:recyclerview-v7:23.4.0中)。

有人有解决方法吗?或者可以告诉我我的错误?


解决方案

对于未来的谷歌搜索者

似乎解决方案是在LayoutManager上使用setAutoMeasureEnabled(false);

此方法已在支持库的23.2.0版本中引入,旨在帮助使用wrap_content作为RV项目的布局参数,我通过这种方法克服了问题,并且使用wrap_content作为我的项目高度而没有任何问题。

2
感谢您提供解决方案。 - Dipendra
1
谢谢。你的解决方案真的帮助我理解了我的错误。 - Pooja
4
当与 CoordinatorLayout 和带动画的 BottomNavigationView 一起使用时,设置 setAutoMeasureEnabled(false) 是无效的(现在已经被弃用了)。唯一有用的方法是给 RecyclerView 设置一个固定的大小。请注意,不要改变原文意思。 - Till Krempel
@TillKrempel,很高兴看到有人和我面临同样的问题!然而,使用RV / NSV的固定大小仍然无法解决我的问题... - Samuel T. Chou
5个回答

3
onBindViewHolder是用于显示视图的方法。当您第一次设置适配器时,不会调用200次onBindViewHolder()。它只会为可见的项目数调用。例如,如果有4个可见项目,则onBindViewHolder将被调用4次。随着滚动,将根据需要绘制多少项来调用onBindViewHolder
如果某些数据正在更改并且您使用notifyDatasetChanged(),则所有视图都将被重新绘制。如果要插入元素并希望刷新适配器,请使用类似以下内容:
adapter.notifyItemInserted(position);

或者删除
adapter.notifyItemRemoved(positionOfObject);
adapter.notifyItemRangeChanged(positionOfObject, arrayList.size());

这是更加高效的做法


当然,你是对的,问题在于RV没有按预期行事!我只是提到了notifyDatasetChanged(),因为我看到这会重新创建问题,但第一个问题仍然是在一开始创建和绑定所有项目。 - Muhammad Naderi
你在onBindViewHolder中放置了日志语句以进行检查吗? - Shashank Udupa
是的,这就是我发现onCreateViewHolder()onBindViewHolder()方法在所有项目中都被调用的方式。 - Muhammad Naderi

1

当 Recycler View 被创建时,它应该仅调用 OnCreateViewHolder(...) 和 OnBindViewHolder(...) 以显示当前可见的行。如果您需要刷新列表中的某一行(例如当图像加载完成时),请避免使用 notifyDataSetChanged() 方法。相反,您可以使用 invalidateChild(...) 方法之一来实现。要找到要使其无效的正确子项,可以使用 findViewHolderForAdapterPosition(int position) 方法。另一种方法是使用适配器的 notifyItemChanged(int position) 方法。


就像我对Shashank说的那样,当然,你是正确的,问题在于RV没有按预期行事!我只是提到了notifyDatasetChanged(),因为我看到这会重新创建问题,但最初的问题仍然是在一开始创建和绑定所有项目。 - Muhammad Naderi
当我使用notifyDatasetChanged()时,我确实需要调用它,但主要问题在于创建RV本身,以及为什么它正在创建和绑定所有项目,请注意它也会为每个项目调用onCreateViewHolder()方法(200+次-可见和不可见)。 - Muhammad Naderi

1
我遇到了类似的问题,对于运行在Android 5上的设备,将所有元素添加到列表中会出现问题。当我将RecyclerView的高度设置为wrap_content时,它会出现问题,但是当我将其改为固定高度时,它可以正常工作。

1

我遇到了类似的问题,我在ViewPager内使用了RecyclerView。当设置适配器项时,RecyclerView会绑定所有视图两次。我在基本的ViewPager类中找到了问题所在。我添加了一些代码来动态计算ViewPager高度,根据其子项进行调整。当我删除那些代码时,RecyclerView可以正常工作。以下是该代码;

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int mode = MeasureSpec.getMode(heightMeasureSpec);
    // Unspecified means that the ViewPager is in a ScrollView WRAP_CONTENT.
    // At Most means that the ViewPager is not in a ScrollView WRAP_CONTENT.
    if (mode == MeasureSpec.UNSPECIFIED || mode == MeasureSpec.AT_MOST) {
        // super has to be called in the beginning so the child views can be initialized.
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int height = 0;
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
            int h = child.getMeasuredHeight();
            if (h > height) height = h;
        }
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
    }
    // super has to be called again so the new specs are treated as exact measurements
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}

0

我遇到了类似的问题:onBindViewHolder被调用了所有数据条目,即使我希望它只被调用可见的。

我发现这是由于我的视图持有者大小定义为wrap content,并且内容是一个异步加载图像的图像加载库,因此当视图持有者最初绑定时,它的大小为0像素,并且所有持有者对用户可见。并且视图持有者仅在图像加载完成后才会被重新调整大小。但是在那段时间内,视图持有者已经被绑定了。


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