理解 RecyclerView setHasFixedSize

Understanding RecyclerView setHasFixedSize

我在理解 setHasFixedSize() 时遇到了一些问题。我知道当 RecyclerView 的大小不变时,它用于优化,来自文档。

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

ListView 有一个类似的命名函数,我认为它确实反映了有关单个列表项高度大小的信息。 RecyclerView 的文档非常清楚地指出它指的是 RecyclerView 本身的大小,而不是其项目的大小。

来自 setHasFixedSize() 方法上方的 RecyclerView 源注释:

 * 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.

setHasFixedSize(true) 表示 RecyclerView 具有固定宽度和高度的子项(项目)。这允许 RecyclerView 通过根据您的适配器计算出整个列表的确切高度和宽度来更好地优化。

一个非常的RecyclerView简化版有:

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

This link 描述了为什么调用 requestLayout 可能很昂贵。基本上,无论何时插入、移动或删除项目,RecyclerView 的大小(宽度和高度)都可能发生变化,进而视图层次结构中任何其他视图的大小也可能发生变化。如果经常添加或删除项目,这尤其麻烦。

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


更新: JavaDoc 已更新以更好地描述该方法的实际作用。

RecyclerView can perform several optimizations if it can know in advance that RecyclerView's size is not affected by the adapter contents. RecyclerView can still change its size based on other factors (e.g. its parent's size) but this size calculation cannot depend on the size of its children or contents of its adapter (except the number of items in the adapter).

If your use of RecyclerView falls into this category, set this to {@code true}. It will allow RecyclerView to avoid invalidating the whole layout when its adapter contents change.

@param hasFixedSize true if adapter changes cannot affect the size of the RecyclerView.

可以确认setHasFixedSize与RecyclerView本身有关,与适配的各个item大小无关

您现在可以在 RecyclerView 上使用 android:layout_height="wrap_content",除其他外,它允许 CollapsingToolbarLayout 知道它在 RecyclerView 为空时不应折叠。这仅在您在 RecylcerView 上使用 setHasFixedSize(false) 时有效。

如果您在 RecyclerView 上使用 setHasFixedSize(true),这种防止 CollapsingToolbarLayout 折叠的行为不起作用,即使 RecyclerView 确实是空的。

如果setHasFixedSize与项目的大小有关,那么当RecyclerView没有项目时它应该没有任何影响。

它会影响recyclerview 的动画,如果是false.. 插入和删除动画将不会显示。所以确保它是 true 以防你为 recyclerview 添加动画。

如果 RecyclerView 的大小(RecyclerView 本身)

...不依赖于适配器内容:

mRecyclerView.setHasFixedSize(true);

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

mRecyclerView.setHasFixedSize(false);

当我们在 RecyclerView 上设置 setHasFixedSize(true) 时,这意味着回收器的大小是固定的并且不受适配器内容的影响。在这种情况下,当我们更新适配器的数据时,onLayout 不会在回收器上调用(但有一个例外)。

我们来看例子:

RecyclerView有一个RecyclerViewDataObserver(find default implemntation in this file)有几个方法,主要重要的是:

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方法。

简单说明

如果我们有一个 RecyclerView,其中 match_parentheight/width,我们应该添加 setHasFixedSize(true),因为RecyclerView 本身不会更改向其中插入或删除项目。

setHasFixedSize 如果我们有一个 RecyclerView with wrap_content as height/width 应该是 false 因为插入的每个元素适配器可以根据项目 inserted/deleted 更改 RecyclerView 的大小,因此,每次 add/delete 项目时 RecyclerView 的大小都会不同。

为了更清楚,如果我们使用固定的 width/height

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

那么如果我们不使用固定的width/height

<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) 因为 wrap_content 对于 widthheight 可以改变我们 RecyclerView.

当我们谈论 RecyclerView setHasFixedSize 时,我们不是在谈论其中元素的数量,而是 View 本身的大小。

RecyclerView 的大小在每次添加内容时都会发生变化。

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

如果您不设置此项,它将检查项目的大小是否已更改且价格昂贵。 (来自评论)