gpt4 book ai didi

android - RecyclerView.Adapter notifyItemRangeChanged() 导致重新绑定(bind)错误的 View

转载 作者:行者123 更新时间:2023-11-29 23:25:30 27 4
gpt4 key购买 nike

精简版

我调用了 adapter.notifyItemRangeChanged(),但看到(通过日志语句)指定范围之外的位置正在重新绑定(bind)。什么可能导致这种情况?

更长的版本

我的应用程序包含一个包含大量项目的 RecyclerView。我有一个每秒更新一次的计数器,每当计数器更新时,我想仅修改RecyclerView 中的可见 View 。我不想调用notifyDataSetChanged() 或要求适配器完全重置 View 。

为此,我使用了 adapter.notifyItemRangeChanged()。我用 the overload of this method that accepts a payload object这样我就可以进行“有效的部分更新”(即只更新计数器 View 而不是重新绑定(bind)整个 ViewHolder)。

但是,我在日志中看到 RecyclerView 正在绑定(bind) ViewHolder,但我没有告诉它。例如,如果位置 5 到 14 在屏幕上可见,我会得到 5-14 的有效部分更新(如预期的那样),但我也会得到位置 17-24 的完全重新绑定(bind)。

此外,当 RecyclerView 完全重新绑定(bind)这些其他位置时,它有时会调用 onCreateViewHolder()。看起来 RecyclerView 想要绑定(bind)这些其他 View ,但是缓存大小小于它想要重新绑定(bind)的范围,所以它必须创建 ViewHolders (然后立即扔掉)。

是否有任何解释为什么这些其他职位被重新绑定(bind)?同样,这些位置超出了我通知适配器的范围。

如果 RecyclerView 只调用过 onBindViewHolder(),我可以忍受性能损失。但由于它有时也会调用 onCreateViewHolder(),因此快速打开 View 会导致丢帧。这是 Not Acceptable 。


此问题可通过以下应用重现:

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 的位置。右侧的文本是我想要每秒更新的计数器 View 。我希望能够更新计数器 View 而无需触摸任何其他内容。

这是日志中的示例 fragment 。您可以看到我的 notifyItemRangeChanged() 调用应该只更新位置 5-14,但是位置 15-19 也被重新绑定(bind),位置 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]

最佳答案

您的问题是由 RecyclerView.LayoutManager 执行额外测量以提供“预测项目动画”引起的,这些 View 可能由于更新 View 的(大小)变化或移出或移入屏幕其他修改(包括 adapter.notifyItemMoved)。

您可以通过覆盖 supportsPredictiveItemAnimations 来禁用此功能LayoutManager 的:

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

您可能还(或选择)更改 RecyclerView.RecycledViewPool 的大小这将阻止创建额外的 ViewHolder,即使动画仍然启用:

// 0 is default itemViewType, 5 is default size - for example use 20
recycler.getRecycledViewPool().setMaxRecycledViews(0, 20);

请注意您的日志如何在池用完之前有 5 个 onBindViewHolder() - full 调用,并且适配器必须开始创建更多。

关于android - RecyclerView.Adapter notifyItemRangeChanged() 导致重新绑定(bind)错误的 View ,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53659692/

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com