从RecyclerView.Adapter中删除项目后,RecyclerView.ItemDecoration不会更新 [英] RecyclerView.ItemDecoration doesn't update after item is removed from RecyclerView.Adapter

查看:86
本文介绍了从RecyclerView.Adapter中删除项目后,RecyclerView.ItemDecoration不会更新的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的问题

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

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

之前和之后:

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

我尝试过的

如果我将notifyItemRemoved()调用更改为notifyDataSetChanged(),则新的最后一项将应用正确的项偏移,但是我会丢失从notifyItemRemoved()获得的内置动画.

如果我继续在上一个项目上使用notifyItemRemoved() 调用notifyItemChanged(),那么我将获得正确的项目偏移量,但动画有些混乱;第二个倒数第二个项目(在删除之前)似乎逐渐消失,然后又消失了(在此屏幕截图中,刚刚删除了标有"item 19"的卡片):

我知道可以通过在<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> 

解决方案

我通过更改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是因为它是一个众所周知的"对象,并且它传达了一些意图:我不希望动画在我要更改的项目上运行. >

为什么起作用

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

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(),我们告诉系统我们只想执行部分更新"(在可能的情况下).

因此,可以肯定的是,我们已指示系统执行部分更新...但是如何阻止仅调用notifyItemChanged(position - 1)引起的闪烁?它与默认连接到所有RecyclerViewRecyclerView.ItemAnimator有关: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.而且由于现在告知动画制作人员它可以重复使用用于我们的已更改"项目的现有ViewHolder,因此它不会分解并创建新的项目(或重复使用回收的项目) )...,这将停止运行已更改项目上的默认淡入淡出动画!

My question

How can I get my ItemDecoration to "update" the item offsets for other views when I remove a given view from my adapter and then call notifyItemRemoved() ?

In my particular situation, I have an ItemDecoration that provides a small amount of top offset to every item, and a large amount of bottom offset to the last item only. When I remove the last item, the view that becomes the new last item does not have any bottom offset.

Before and after:

If I then scroll the list around, when I return to the bottom of the list, my "new" last item has the correct offsets. It's only an issue for as long as the "new" last item remains visible on the screen.

What I've tried

If I change my notifyItemRemoved() call to notifyDataSetChanged(), the new last item will have the correct item offsets applied, but I lose the built-in animation I get from notifyItemRemoved().

If I keep using notifyItemRemoved() and additionally call notifyItemChanged() on the previous item, then I will get the correct item offsets but the animation is somewhat janky; the second-to-last item (before the removal) seems to fade out and then in again (in this screenshot, the card saying "item 19" was just removed):

I know that I can create a large space at the end of my list by applying bottom padding to my <RecyclerView> tag and specifying android:clipToPadding="false", but this solution is not acceptable for various reasons.

To reproduce

I originally encountered this issue in a much larger app I'm working on, but here's a tiny app that demonstrates the issue.

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>

解决方案

I solved this by changing my removeItemAt() method as follows:

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

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

The second argument to notifyItemChanged() is the key. It can be literally any object, but I chose Boolean.FALSE because it's a "well-known" object and because it conveys a little bit of intent: I don't want animations to run on the item I'm changing.

Why it works

It turns out that there's a second onBindViewHolder() method defined in RecyclerView.Adapter that I'd never seen before. From the source code:

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

Excerpted from that method's documentation:

The payloads parameter is a merge list from notifyItemChanged(int, Object) or notifyItemRangeChanged(int, int, Object). If the payloads list is not empty, the ViewHolder is currently bound to old data and Adapter may run an efficient partial update using the payload info. If the payload is empty, Adapter must run a full bind.

In other words, by passing something (anything) as a payload in notifyItemChanged(), we're telling the system that we want to perform only a "partial update" (in situations where that's possible).

So, sure, we've instructed the system to perform a partial update... but how does that stop the flickering caused by simply calling notifyItemChanged(position - 1) ? It has to do with the RecyclerView.ItemAnimator that's attached to all RecyclerViews by default: DefaultItemAnimator.

DefaultItemAnimator's source code includes this method:

@Override
public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
        @NonNull List<Object> payloads) {
    return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);
}

You'll notice that this method also takes a List<Object> payloads parameter. This is the same list of payloads as used in the other onBindViewHolder() call mentioned above.

Because we passed a payload argument, this method will return true. And since the animator is now told that it can reuse the already-existing ViewHolder for our "changed" item, it doesn't tear it down and create a new one (or reuse a recycled one)... which stops the default fade animation on the changed item from running!

这篇关于从RecyclerView.Adapter中删除项目后,RecyclerView.ItemDecoration不会更新的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆