【问题标题】:RecyclerView.ItemDecoration doesn't update after item is removed from RecyclerView.Adapter从 RecyclerView.Adapter 中删除项目后 RecyclerView.ItemDecoration 不会更新
【发布时间】:2017-11-17 01:43:46
【问题描述】:

我的问题

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

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

之前和之后:

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

我尝试过的

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

如果我继续使用notifyItemRemoved() 并且另外在前一个项目上调用notifyItemChanged(),那么我将获得正确的项目偏移量,但动画有些卡顿;倒数第二个项目(在移除之前)似乎淡出然后再次出现(在此屏幕截图中,“项目 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>

【问题讨论】:

    标签: android android-recyclerview


    【解决方案1】:

    我通过如下更改我的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) 引起的闪烁?它与默认附加到所有RecyclerViews 的RecyclerView.ItemAnimator 相关:DefaultItemAnimator

    DefaultItemAnimator的源代码包含这个方法:

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

    您会注意到此方法还采用List&lt;Object&gt; payloads 参数。这与上述其他 onBindViewHolder() 调用中使用的有效负载列表相同。

    因为我们传递了一个有效载荷参数,所以这个方法将返回true。而且由于动画师现在被告知它可以重用已经存在的ViewHolder 用于我们“更改”的项目,它不会将其拆除并创建一个新项目(或重用一个回收的项目) )... 这会停止运行已更改项目的默认淡入淡出动画!

    【讨论】:

    • 谢谢你!我有一个与删除项目有关的稍微不同的问题:ItemDecoration 正在应用于我刚刚删除的项目。添加notifyItemChanged,即使没有修复该有效负载。
    • 您,先生,是个传奇!在 DiffUtils 计算和传播更改后,我没有通过 ItemDecoration 应用边距设置问题。在AdapterUpdateCallback 中使用notifyItemChanged 解决所有问题。谢谢!
    【解决方案2】:

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

    【讨论】:

    • 这对我有用,但我必须确保对invalidateItemDecorations() 的调用是在更新之后
    【解决方案3】:

    ListAdapter解决方案:

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

    关键是来自最新 RecyclerView 版本的新 submitList(list, commitCallback) 方法。它允许您在 ListAdapter 将所有更改应用到 RecyclerView 之后调用 invalidateItemDecorations()

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

    【讨论】:

    • 非常令人沮丧的是,它需要像这样被强制,并加上额外的post,但最终它可以工作。谢谢。
    猜你喜欢
    • 2020-01-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-09-17
    • 2019-01-26
    相关资源
    最近更新 更多