从RecyclerView.Adapter中删除项目后,RecyclerView.ItemDecoration不会更新

时间:2017-11-17 01:43:47

标签: android android-recyclerview

我的问题

当我从适配器中删除给定视图然后调用ItemDecoration时,如何让notifyItemRemoved()“更新”其他视图的项目偏移量?

在我的特定情况下,我有一个ItemDecoration,它为每个项目提供少量顶部偏移量,并且有大量底部偏移量仅限最后一项。当我删除最后一项时,成为新的最后一项的视图没有任何底部偏移。

之前和之后:

enter image description here enter image description here

如果我然后滚动列表,当我返回到列表的底部时,我的“新”最后一项具有正确的偏移量。只要“新”最后一项在屏幕上保持可见,这只是一个问题。

我尝试了什么

如果我将notifyItemRemoved()来电更改为notifyDataSetChanged(),新的最后一项将会应用正确的项目抵消,但我会丢失从notifyItemRemoved()获得的内置动画。< / p>

如果我继续使用notifyItemRemoved()另外在前一项上调用notifyItemChanged(),那么我将获得正确的项目偏移,但动画有些笨拙;倒数第二个项目(删除前)似乎淡出然后再次出现(在此屏幕截图中,卡片上写着“第19项”刚刚删除):

enter image description here

我知道我可以通过在我的<RecyclerView>标记中应用底部填充并指定android:clipToPadding="false"来在列表末尾创建一个大空格,但由于各种原因,此解决方案无法接受。

重现

我最初在一个我正在处理的更大的应用程序中遇到过这个问题,但这里有一个很小的应用程序来证明这个问题。

MainActivity.java:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    final MyAdapter adapter = new MyAdapter(20);

    Button button = findViewById(R.id.button);
    button.setOnClickListener(new View.OnClickListener() {

        @Override
        public void onClick(View view) {
            adapter.removeItemAt(adapter.getItemCount() - 1);
        }
    });

    RecyclerView recycler = findViewById(R.id.recycler);
    recycler.setLayoutManager(new LinearLayoutManager(this));
    recycler.addItemDecoration(new MyItemDecoration());
    recycler.setAdapter(adapter);
}

private static class MyItemDecoration extends RecyclerView.ItemDecoration {

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        int viewPosition = parent.getChildViewHolder(view).getLayoutPosition();
        int lastPosition = state.getItemCount() - 1;

        outRect.top = 20;
        outRect.bottom = (viewPosition == lastPosition) ? 200 : 0;
    }
}

private static class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {

    private final List<String> list;

    private MyAdapter(int count) {
        this.list = new ArrayList<>();

        for (int i = 0; i < count; ++i) {
            list.add("" + i);
        }
    }

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        View itemView = inflater.inflate(R.layout.item, parent, false);
        return new MyViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        holder.text.setText("item: " + list.get(position));
    }

    @Override
    public int getItemCount() {
        return list.size();
    }

    private void removeItemAt(int position) {
        if (list.size() > 0) {
            list.remove(position);
            notifyItemRemoved(position);
        }
    }
}

private static class MyViewHolder extends RecyclerView.ViewHolder {

    private final TextView text;

    public MyViewHolder(View itemView) {
        super(itemView);
        this.text = itemView.findViewById(R.id.text);
    }
}
}

activity_main.xml中:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#eee">

    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="REMOVE LAST ITEM"/>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>

</LinearLayout>

item.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="4dp">

    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="64dp"
        android:layout_margin="8dp"
        android:gravity="center"
        android:textColor="#000"
        android:textSize="16sp"/>

</android.support.v7.widget.CardView>

3 个答案:

答案 0 :(得分:10)

我通过更改removeItemAt()方法解决了这个问题,如下所示:

private void removeItemAt(int position) {
    if (list.size() > 0) {
        list.remove(position);
        notifyItemRemoved(position);

        if (position != 0) {
            notifyItemChanged(position - 1, Boolean.FALSE);
        }
    }
}

notifyItemChanged()的第二个参数是关键。它可以是字面上的任何对象,但我选择Boolean.FALSE,因为它是一个着名的&#34;&#34;对象,因为它传达了一点意图:我不希望动画在我改变的项目上运行。

为什么会这样?

事实证明,onBindViewHolder()中定义的第二个RecyclerView.Adapter方法是我以前从未见过的。来自源代码:

public void onBindViewHolder(VH holder, int position, List<Object> payloads) {
    onBindViewHolder(holder, position);
}

摘自该方法的文件:

  

payloads参数是来自notifyItemChanged(int, Object)notifyItemRangeChanged(int, int, Object)的合并列表。如果payloads列表不为空,则ViewHolder当前绑定到旧数据,Adapter可以使用有效内容信息运行有效的部分更新。如果有效负载为空,Adapter必须运行完全绑定。

换句话说,通过在notifyItemChanged()中将某些内容(任何内容)作为有效负载传递,我们告诉系统我们只想执行部分更新&#34; (在可能的情况下)。

所以,当然,我们已经指示系统执行部分更新......但是如何阻止简单地调用notifyItemChanged(position - 1)导致的闪烁?它与默认情况下附加到所有RecyclerView.ItemAnimator的{​​{1}}有关:RecyclerView

DefaultItemAnimator的源代码包含此方法:

DefaultItemAnimator

您会注意到此方法也采用@Override public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder, @NonNull List<Object> payloads) { return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads); } 参数。这与上面提到的其他List<Object> payloads调用中使用的有效负载列表相同。

因为我们传递了一个有效负载参数,所以此方法将返回onBindViewHolder()。而且现在动画师现在被告知它可以重用已经存在的true用于我们的&#34;更改&#34; item,它不会拆掉它并创建一个新的(或重复使用一个回收的)...它会阻止更改的项目上的默认淡入淡出动画运行!

答案 1 :(得分:3)

这是一个太迟的答案,但是我会回答,因为我有同样的问题。在插入/更新/删除数据之前,请尝试调用RecyclerView.invalidateItemDecorations()。

答案 2 :(得分:1)

ListAdapter 解决方案:

listAdapter.submitList(newList) {
    handler.post { // or just "post" if you're inside View
        recyclerView.invalidateItemDecorations()
    }
}

密钥是最新的RecyclerView版本中的新submitList(list, commitCallback)方法。它允许您在{strong>之后之后调用invalidateItemDecorations()。ListAdapter将所有更改应用于RecyclerView。

我还必须添加post {}调用才能使其正常工作-避免装饰过早失效,并且什么也没有发生。