我打电话给adapter.notifyItemRangeChanged()
,但是(通过日志语句)看到超出指定范围的位置被重新绑定。是什么原因造成的?
我的应用程序包含一个RecyclerView
,其中包含大量项目。我有一个每秒更新的计数器,每当计数器更新时,我只想修改RecyclerView
中的可见视图。我不不想呼叫notifyDataSetChanged()
,或以其他方式要求适配器完全重置视图。
为此,我使用adapter.notifyItemRangeChanged()
。我使用the overload of this method that accepts a payload object是为了进行“有效的部分更新”(即仅更新计数器视图,而不是重新绑定整个ViewHolder
)。
但是,我在日志中看到RecyclerView
绑定了我没有告诉过的ViewHolder
。例如,如果在屏幕上可以看到位置5到14,则可以得到5-14的有效部分更新(如预期的那样),但是我也可以完全重新绑定位置17-24。
此外,当RecyclerView
完全重新绑定其他位置时,有时会调用onCreateViewHolder()
。看来RecyclerView
希望绑定这些其他视图,但是缓存大小小于它想要重新绑定的范围,因此它必须创建ViewHolder
(然后立即丢弃) )。
对于为什么要重新绑定其他职位有什么解释吗?同样,这些位置不在我通知适配器的范围内。
如果RecyclerView
仅被称为onBindViewHolder()
,那么我可以忍受性能损失。但是由于有时它还会调用onCreateViewHolder()
,因此,快速浏览会导致丢帧。这是不可接受的。
使用以下应用可重现此问题:
MainActivity.java
package com.example.stackoverflow;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
public class MainActivity extends AppCompatActivity {
private enum CounterTag {
INSTANCE
}
private static final String TAG = "StackOverflow";
private Handler handler;
private int counter;
private MyAdapter adapter;
private LinearLayoutManager layoutManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handler = new Handler();
handler.postDelayed(this::updateCounter, 1000);
adapter = new MyAdapter();
layoutManager = new LinearLayoutManager(this);
RecyclerView recycler = findViewById(R.id.recycler);
recycler.setAdapter(adapter);
recycler.setLayoutManager(layoutManager);
}
private void updateCounter() {
++counter;
int first = layoutManager.findFirstVisibleItemPosition();
int last = layoutManager.findLastVisibleItemPosition();
int length = (last - first) + 1;
Log.d(TAG, "notifying " + length + " items changed, starting at " + first);
adapter.notifyItemRangeChanged(first, length, CounterTag.INSTANCE);
handler.postDelayed(this::updateCounter, 1000);
}
private class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
Log.d(TAG, "onCreateViewHolder()");
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View itemView = inflater.inflate(R.layout.item, parent, false);
return new MyViewHolder(itemView);
}
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
Log.d(TAG, "onBindViewHolder() - full [" + position + "]");
holder.bindPosition(position);
holder.bindCounter();
}
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position, @NonNull List<Object> payloads) {
if (payloads.isEmpty()) {
onBindViewHolder(holder, position);
}
else if (payloads.get(0) == CounterTag.INSTANCE) {
Log.d(TAG, "onBindViewHolder() - partial [" + position + "]");
holder.bindCounter();
}
}
@Override
public int getItemCount() {
return 100_000;
}
}
private class MyViewHolder extends RecyclerView.ViewHolder {
private final TextView positionView;
private final TextView counterView;
private MyViewHolder(@NonNull View itemView) {
super(itemView);
this.positionView = itemView.findViewById(R.id.position);
this.counterView = itemView.findViewById(R.id.counter);
}
private void bindPosition(int position) {
positionView.setText("" + position);
}
private void bindCounter() {
counterView.setText("Counter: " + counter);
}
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="64dp"
android:layout_marginBottom="1dp"
android:orientation="horizontal">
<TextView
android:id="@+id/position"
android:layout_width="72dp"
android:layout_height="match_parent"
android:gravity="center"
android:background="#eee"
tools:text="3"/>
<TextView
android:id="@+id/counter"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center"
android:background="#ddd"
tools:text="99"/>
</LinearLayout>
这是我的示例应用程序的外观。左侧的文本是ViewHolder
的位置。右侧的文本是我想每秒更新的计数器视图。我希望能够更新计数器视图而无需触摸其他任何东西。
这是日志中的样本片段。您可以看到我的notifyItemRangeChanged()
通话应仅更新位置5-14,但位置15-19也被重新绑定,位置22-24需要创建新的ViewHolder
。>
D StackOverflow: notifying 10 items changed, starting at 5 D StackOverflow: onBindViewHolder() - full [15] D StackOverflow: onBindViewHolder() - full [16] D StackOverflow: onBindViewHolder() - full [17] D StackOverflow: onBindViewHolder() - full [18] D StackOverflow: onBindViewHolder() - full [19] D StackOverflow: onCreateViewHolder() D StackOverflow: onBindViewHolder() - full [22] D StackOverflow: onCreateViewHolder() D StackOverflow: onBindViewHolder() - full [23] D StackOverflow: onCreateViewHolder() D StackOverflow: onBindViewHolder() - full [24] D StackOverflow: onBindViewHolder() - partial [5] D StackOverflow: onBindViewHolder() - partial [6] D StackOverflow: onBindViewHolder() - partial [7] D StackOverflow: onBindViewHolder() - partial [8] D StackOverflow: onBindViewHolder() - partial [9] D StackOverflow: onBindViewHolder() - partial [10] D StackOverflow: onBindViewHolder() - partial [11] D StackOverflow: onBindViewHolder() - partial [12] D StackOverflow: onBindViewHolder() - partial [13] D StackOverflow: onBindViewHolder() - partial [14]
答案 0 :(得分:1)
您的问题是由于RecyclerView.LayoutManager
进行了额外的测量以为由于更新视图的(大小)更改或其他修改(包括{ {1}}。
您可以通过覆盖adapter.notifyItemMoved
中的supportsPredictiveItemAnimations
来禁用此功能:
LayoutManager
您可能还希望(或选择)更改RecyclerView.RecycledViewPool
的大小,即使在启用动画的情况下,这也会阻止创建额外的@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.recycler);
adapter = new MyAdapter();
// extend layout manager and override this method
layoutManager = new LinearLayoutManager(this){
@Override
public boolean supportsPredictiveItemAnimations() {
return false;
}
};
RecyclerView recycler = findViewById(R.id.recycler);
recycler.setAdapter(adapter);
recycler.setLayoutManager(layoutManager);
}
:
ViewHolder
请注意,在池用尽且适配器必须开始创建更多日志之前,您的日志如何进行5次// 0 is default itemViewType, 5 is default size - for example use 20
recycler.getRecycledViewPool().setMaxRecycledViews(0, 20);
调用。