理解RecyclerView的setHasFixedSize方法

189

我对setHasFixedSize()的理解有些困难。从文档中了解到,它用于在RecyclerView的大小不变时进行优化。

但这是什么意思呢?在大多数情况下,ListView几乎总是固定大小的。那么在什么情况下它不是固定大小的呢?这是否意味着它在屏幕上占用的实际空间会随内容增长而增加?


https://dev59.com/9V4b5IYBdhLWcg3wqDSj#40707099 - Sotti
我发现这个答案非常有帮助,而且很容易理解 StackOverflow - rv.setHasFixedSize(true); - M. H.
11个回答

139

RecyclerView 的一个非常简化的版本有:

void onItemsInsertedOrRemoved() {
   if (hasFixedSize) layoutChildren();
   else requestLayout();
}

这个链接解释了为什么调用requestLayout可能很耗费资源。基本上,每当插入、移动或删除项目时,RecyclerView的尺寸(宽度和高度)可能会发生变化,从而影响视图层次结构中任何其他视图的大小。如果频繁添加或删除项,这可能会特别麻烦。

当更改适配器的内容不会改变其高度或宽度时,通过设置setHasFixedSize为true来避免不必要的布局传递。


更新: JavaDoc已经更新,以更好地描述该方法实际上所做的工作。

如果RecyclerView事先知道RecyclerView的大小不受适配器内容的影响,则可以执行几个优化。RecyclerView仍然可以根据其他因素(例如其父级的大小)更改其大小,但是此大小计算不能依赖于其子元素的大小或适配器的内容(除了适配器中的项目数量)。

如果您使用的RecyclerView属于此类别,请将其设置为{@code true}。这将允许RecyclerView在其适配器内容更改时避免使整个布局无效。

@param hasFixedSize 如果适配器更改不会影响RecyclerView的大小,则为true。


213
RecyclerView的大小每次添加东西时都会改变,无论如何。 setHasFixedSize 的作用是确保(由用户输入)RecyclerView的大小不变。项的高度(或宽度)不会改变。添加或删除每个项目都将相同。如果您不设置此选项,则会检查项的大小是否已更改,这很耗费资源。仅澄清一下,因为该答案有些令人困惑。 - Arnold Balliu
11
@ArnoldB 的解释非常好,我甚至认为它可以作为一个独立的答案。 - young_souvlaki
5
您的问题是:@ArnoldB - 我还是有点困惑。您是否建议,如果所有子项的宽度/高度都是恒定的,我们应将 hasFixedSize 设置为 true?如果是这样,那么如果可能在运行时删除某些子项(例如我有一个滑动删除功能),设置为 true 是否可行?回答:您的理解是正确的。如果所有子项的宽度/高度都是恒定的,则建议将 hasFixedSize 设置为 true。即使您具有运行时删除子项的功能,也可以安全地将其设置为 true。 - dev
3
可以的。因为物品的宽度和高度并没有改变,只是增加或减少了数量。增加或减少物品并不会改变它们的尺寸。 - Arnold Balliu
3
@ArnoldB 我认为物品的大小(宽度/高度)在这里不是问题。 它也不会检查物品的大小。 它只是告诉RecyclerView在数据集更新后是否调用requestLayout - Kimi Chiu
显示剩余6条评论

40

简单说明

如果我们有一个高度/宽度使用match_parentRecyclerView,我们应该添加setHasFixedSize(true),因为插入或删除项目时RecyclerView本身的大小不会发生变化。

如果我们有一个高度/宽度使用wrap_contentRecyclerView,则setHasFixedSize应该是false。因为适配器插入的每个元素都可能根据插入/删除的项目而改变RecyclerView的大小,因此每次添加/删除项目时RecyclerView的大小都会有所不同。

更清楚地说,如果我们使用固定的宽度/高度

<android.support.v7.widget.RecyclerView
    android:id="@+id/my_recycler_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

我们可以使用my_recycler_view.setHasFixedSize(true)

如果我们不使用固定的宽度/高度

<android.support.v7.widget.RecyclerView
        android:id="@+id/my_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

我们应该使用my_recycler_view.setHasFixedSize(false),因为对于widthheightwrap_content可能会改变我们的RecyclerView的大小。

当我们谈论RecyclerView setHasFixedSize时,我们不是在谈论其中的元素数量,而是在谈论View本身的大小。这个大小取决于其中的项目是否也具有不同的大小,所以,如果您在recyclerview中有一个比其他项目更大的项目,则RecyclerView的大小不是固定的。


31
可以确认setHasFixedSize与RecyclerView本身有关,而不是每个适配到其中的项目的大小。现在可以在RecyclerView上使用android:layout_height="wrap_content",这样CollapsingToolbarLayout就知道当RecyclerView为空时它不应该折叠。这仅在您在RecylcerView上使用setHasFixedSize(false)时才有效。
如果您在RecyclerView上使用setHasFixedSize(true),即使RecyclerView确实为空,也无法防止CollapsingToolbarLayout折叠。如果setHasFixedSize与项目的大小有关,则在RecyclerView没有任何项目时它不应产生任何影响。

5
我刚有了一个类似的经历。使用GridLayoutManager(每行3个项目)和layout_height = wrap_content的RecyclerView。当我点击一个添加3个新项目到列表的按钮时,RecyclerView不会扩展以适应新的项目。相反,它保持相同的大小,查看新项目的唯一方法是滚动它。即使项目具有相同的大小,我仍然需要删除setHasFixedSize(true)才能使它在添加新项目时扩展。 - Mateus Gondim
我认为你是正确的。从文档中可以看到,hasFixedSize: 如果适配器更改不会影响RecyclerView的大小,则设置为true。因此,即使项目的大小发生变化,您仍然可以将其设置为true。 - Kimi Chiu

15

ListView有一个类似的函数,我认为它反映了各个列表项高度的大小信息。RecyclerView的文档非常明确地说明它是指RecyclerView本身的大小,而不是其项目的大小。

从RecyclerView源代码的注释中可以看到setHasFixedSize()方法:

 * RecyclerView can perform several optimizations if it can know in advance that changes in
 * adapter content cannot change the size of the RecyclerView itself.
 * If your use of RecyclerView falls into this category, set this to true.

16
RecyclerView中的"size"是如何定义的?它是仅指屏幕上可见的大小,还是指RecyclerView的完整大小,即所有项高度之和加上padding和spacing的总和? - Vicky Chijwani
2
确实,这需要更多的信息。如果您删除项目并且RecyclerView收缩,那么是否认为它已更改大小? - Henrique de Sousa
5
我会把这个想法类比为TextView如何进行布局。如果你指定wrap_content,那么当你设置文本时,TextView可以请求重新布局并更改它在屏幕上占用的空间。如果你指定match_parent或固定尺寸,则TextView将不会请求重新布局,因为大小是固定的,文本内容的多少也不会影响所占用的空间。RecyclerView也是一样的。setHasFixedSize()提示RV它永远不需要根据适配器项目的更改请求重新布局。 - dangVarmit
1
@dangVarmit 解释得很好! - howerknea

7
当我们在RecyclerView上设置setHasFixedSize(true)时,这意味着回收器的大小是固定的,并且不受适配器内容的影响。在这种情况下,当我们更新适配器的数据时,回收站上不会调用onLayout(但有一个例外)。
让我们来看一个例子:
RecyclerView有一个RecyclerViewDataObserver(在此文件中查找默认实现),其中包含几个方法,最重要的是:
void triggerUpdateProcessor() {
    if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
        ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
    } else {
        mAdapterUpdateDuringMeasure = true;
        requestLayout();
    }
}

如果我们使用setHasFixedSize(true)并通过notifyItemRangeChanged, notifyItemRangeInserted, notifyItemRangeRemoved or notifyItemRangeMoved更新适配器的数据,则会调用此方法。在这种情况下,不会调用回收器的onLayout,但会调用requestLayout以更新子元素。
但是,如果我们使用setHasFixedSize(true)并通过notifyItemChanged更新适配器的数据,则会调用回收器默认RecyclerViewDataObserveronChange,而不会调用triggerUpdateProcessor。在这种情况下,每当我们将setHasFixedSize设置为truefalse时,都会调用回收器的onLayout
// no calls to triggerUpdateProcessor
@Override
public void onChanged() {
    assertNotInLayoutOrScroll(null);
     mState.mStructureChanged = true;

     processDataSetCompletelyChanged(true);
     if (!mAdapterHelper.hasPendingUpdates()) {
         requestLayout();
     }
}

// calls to triggerUpdateProcessor
@Override
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
    assertNotInLayoutOrScroll(null);
    if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
        triggerUpdateProcessor();
    }
}

我可以帮您翻译成中文。这段内容是关于编程的。它告诉你如何自己检查代码。您需要创建一个自定义的RecyclerView并覆盖它。
override fun requestLayout() {
    Log.d("CustomRecycler", "requestLayout is called")
    super.requestLayout()
}

override fun invalidate() {
    Log.d("CustomRecycler", "invalidate is called")
    super.invalidate()
}

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    Log.d("CustomRecycler", "onLayout is called")
    super.onLayout(changed, l, t, r, b)
}

将回收器大小设置为match_parent(在xml中)。尝试使用replaceDatareplaceOne更新适配器的数据,并设置setHasFixedSize(true),然后设置为false
// onLayout is called every time
fun replaceAll(data: List<String>) {
    dataSet.clear()
    dataSet.addAll(data)
    this.notifyDataSetChanged()
}

// onLayout is called only for setHasFixedSize(false)
fun replaceOne(data: List<String>) {
    dataSet.removeAt(0)
    dataSet.addAll(0, data[0])
    this.notifyItemChanged(0)
}

"并检查您的日志。"
"

我的日志:

"
// for replaceAll
D/CustomRecycler: requestLayout is called
D/CustomRecycler: onMeasure is called
D/CustomRecycler: onMeasure is called
D/CustomRecycler: onLayout
D/CustomRecycler: requestLayout is called
D/CustomRecycler: requestLayout is called
D/CustomRecycler: onDraw is called

// for replaceOne
D/CustomRecycler: requestLayout is called
D/CustomRecycler: onDraw is called
D/CustomRecycler: requestLayout is called
D/CustomRecycler: onDraw is called

总结:

如果我们设置 setHasFixedSize(true) 并以除调用 notifyDataSetChanged 之外的其他方式通知观察者来更新适配器的数据,则可以提高性能,因为不会调用回收器的 onLayout 方法。


你用 wrap_content 或者 match_parent 测试过 RecyclerView 的高度了吗? - Lubos Mudrak

1
如果RecyclerView的大小(指RecyclerView本身)不取决于适配器的内容:
mRecyclerView.setHasFixedSize(true);

"

...取决于适配器内容:

"
mRecyclerView.setHasFixedSize(false);

1
它会影响RecyclerView的动画效果,如果为false,则插入和删除动画将不会显示。因此,请确保在为RecyclerView添加动画时将其设置为true

0

setHasFixedSize() 是指 itemView 或者可以说是 view 的大小,所以它与列表或 ArrayList 没有任何关系。如果您想要在适配器中为不同的位置更改视图的高度和宽度,则应将其设置为 false。


0

无论添加什么,RecyclerView的大小每次都会改变。

setHasFixedSize的作用是确保(通过用户输入)RecyclerView的大小不变。项目的高度(或宽度)不会改变。添加或删除的每个项目都将相同。

如果您不设置此项,则会检查项目的大小是否已更改,这是昂贵的。(来自评论)


0
当您将setHasFixedSize()设置为true时,它告诉系统recyclerview的高度和宽度不会改变。

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