RecyclerView.Adapter notifyItemRangeChanged()导致错误视图的重新绑定

时间:2018-12-06 21:06:51

标签: android android-recyclerview

简短版

我打电话给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]

1 个答案:

答案 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); 调用。