RecyclerView和ItemTouchHelper滑动删除问题

4
我正在尝试通过RecyclerView和ItemTouchHelper实现“滑动删除”功能。我遇到了一个奇怪的问题,但我无法找到问题所在。当我将一个项目从顶部(而不是第一个)向左滑动时,它会消失,目前为止一切正常。当我滚动并返回时,在被滑动掉的项上方有一个伪像。看起来该行未被绘制(或者可能是x轴平移?)。视频展示了这个问题。
视频步骤:
  1. 我滑动删除第二个项目
  2. 向下滚动到底部
  3. 回来
  4. 项目1不再可见
  5. 我再次向下滚动到底部
  6. 我向上滚动回来
  7. 现在一切都好了
  8. 再次,但使用从顶部开始的第三个(而不是第二个)项目,同样的问题
  9. 再次,使用第一个项目,没有问题

enter image description here

相关代码:(完整的 Github 示例应用程序 在此处
public class MainActivity extends AppCompatActivity {

RecyclerView mRecyclerView;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);
    mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
    mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
    mRecyclerView.setAdapter(new TestAdapter());
    setUpItemTouchHelper();

}

private void setUpItemTouchHelper() {
    ItemTouchHelper.SimpleCallback simpleItemTouchCallback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {

        @Override
        public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
            return false;
        }

        @Override
        public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
            int swipedPosition = viewHolder.getAdapterPosition();
            TestAdapter adapter = (TestAdapter)mRecyclerView.getAdapter();
            adapter.remove(swipedPosition);
        }

    };
    ItemTouchHelper itemTouchHelper = new ItemTouchHelper(simpleItemTouchCallback);
    itemTouchHelper.attachToRecyclerView(mRecyclerView);
}

static class TestAdapter extends RecyclerView.Adapter {

    List<String> items;

    public TestAdapter() {
        items = new ArrayList<>();
        // this should give us a couple of screens worth
        for (int i=1; i<= 15; i++) {
            items.add("Item " + i);
        }
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new TestViewHolder(parent);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        TestViewHolder viewHolder = (TestViewHolder)holder;
        String item = items.get(position);
        viewHolder.titleTextView.setText(item);
    }

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

    public void remove(int position) {
        if (position < 0 || position >= items.size()) {
            return;
        }
        items.remove(position);
        notifyItemRemoved(position);
    }
}

static class TestViewHolder extends RecyclerView.ViewHolder {

    TextView titleTextView;

    public TestViewHolder(ViewGroup parent) {
        super(LayoutInflater.from(parent.getContext()).inflate(R.layout.row_view, parent, false));
        titleTextView = (TextView) itemView.findViewById(R.id.title_text_view);
    }

}

}

编辑:

我有一个解决这个问题的方法,但是我想知道它的原因以及如何真正地解决这个问题。我的方法是调用notifyDataSetChanged(),但要在动画完成后再调用(否则动画会被终止)。基本上,我添加了一个ItemDecorator并找出了动画何时结束。

mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {

        boolean running;

        @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            if (parent.getItemAnimator().isRunning()) {
                running = true;
            }
            if (running == true && !parent.getItemAnimator().isRunning()) {
                // first time it's not running
                running = false;
                parent.getAdapter().notifyDataSetChanged();
            }
            super.onDraw(c, parent, state);
        }
});
5个回答

9
尝试在您的删除方法中添加notifyDataSetChanged()
public void remove(int position) {
    if (position < 0 || position >= items.size()) {
        return;
    }
    items.remove(position);
    notifyItemRemoved(position);
    notifyDataSetChanged();
}

notifyItemRemoved(position)会通知RecyclerView适配器,数据在特定位置被删除。

notifyDataSetChanged()会通知已附加的观察器数据已更改,并且任何反映数据集的视图都应刷新自身。

更新

尝试在notifyItemRemoved(position);之前添加mRecyclerView.removeViewAt(position);。这不会影响动画效果。

public class MainActivity extends AppCompatActivity {

    RecyclerView mRecyclerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        mRecyclerView.setAdapter(new TestAdapter());
        setUpItemTouchHelper();

    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == R.id.menu_item_add_5_items) {
            ((TestAdapter)mRecyclerView.getAdapter()).addItems(5);
        }
        return super.onOptionsItemSelected(item);
    }

    private void setUpItemTouchHelper() {
        ItemTouchHelper.SimpleCallback simpleItemTouchCallback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {

            @Override
            public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
                return false;
            }

            @Override
            public void onSwiped(RecyclerView.ViewHolder viewHolder, int swipeDir) {
                int swipedPosition = viewHolder.getAdapterPosition();
                TestAdapter adapter = (TestAdapter)mRecyclerView.getAdapter();
                adapter.remove(swipedPosition);
            }

        };
        ItemTouchHelper itemTouchHelper = new ItemTouchHelper(simpleItemTouchCallback);
        itemTouchHelper.attachToRecyclerView(mRecyclerView);
    }

    class TestAdapter extends RecyclerView.Adapter {

        List<String> items;
        int lastInsertedIndex;

        public TestAdapter() {
            items = new ArrayList<>();
            lastInsertedIndex = 15;
            // this should give us a couple of screens worth
            for (int i=1; i<= lastInsertedIndex; i++) {
                items.add("Item " + i);
            }
        }

        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            return new TestViewHolder(parent);
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            TestViewHolder viewHolder = (TestViewHolder)holder;
            String item = items.get(position);
            viewHolder.titleTextView.setText(item);
        }

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

        public void addItems(int howMany){
            if (howMany > 0) {
                for (int i = lastInsertedIndex + 1; i <= lastInsertedIndex + howMany; i++) {
                    items.add("Item " + i);
                    notifyItemInserted(items.size() - 1);
                }
                lastInsertedIndex = lastInsertedIndex + howMany;
            }
        }

        public void remove(int position) {
            if (position < 0 || position >= items.size()) {
                return;
            }
            items.remove(position);
            mRecyclerView.removeViewAt(position);
            notifyItemRemoved(position);
        }
    }

    static class TestViewHolder extends RecyclerView.ViewHolder {

        TextView titleTextView;

        public TestViewHolder(ViewGroup parent) {
            super(LayoutInflater.from(parent.getContext()).inflate(R.layout.row_view, parent, false));
            titleTextView = (TextView) itemView.findViewById(R.id.title_text_view);
        }

    }

}

1
谢谢,但是notifyDataSetChanged()会破坏删除动画,所以这不是我正在寻找的解决方案。可行的方法是等待动画完成,然后调用notifyDataSetChanged(),但是没有回调函数,我需要猜测。请参见我的问题编辑,了解我如何做到这一点。但是,我不愿意在生产代码中留下这样的东西。 - Nemanja Kovacevic
并不完全正确。适配器中的位置与回收视图子项中的位置不是一对一映射关系。您可以在适配器中有数百个项目,但您只能在一个屏幕上拥有x个适合的视图加上几个。您的修复方法需要进行轻微修改:如果我将其调用到onSwipe()而不是removeViewAt(position),而是像这样删除视图 - mRecyclerView.removeView(viewHolder.itemView); 无论如何,这也可能只是一个“hack”解决方案,该问题最近引入,绝对是一个错误。请查看我的答案。非常感谢您的帮助! - Nemanja Kovacevic
好的,没问题。看起来确实是一个 bug。 - random

3

更新:23.1.1修复了该漏洞,请勿使用23.1.0。

看起来这个漏洞是回收视图支持库中的一个回归。我认为这是导致它的提交,但仍然不完全理解情况,所以不要对我有任何要求。

该漏洞表现为com.android.support:recyclerview-v7版本23.1.0,但不表现为23.0.122.2.1

我将尝试找到正确的报告位置,并在此答案的评论中发布链接。


1

这篇文章的第二部分说什么鬼,位置从0到1没有升级等等...我在 onItemMore 中修复了这个问题,在 .. fromPosition? toPosition-1: toPosition... 中需要删除 -1。 - AlexS

0

我通过重新在Recyclerview上设置适配器来解决了这个问题。当您从列表中删除时,只需......

RecyclerView.setAdapter(this);


0
我在使用滑动重新排序时遇到了类似的问题。RecyclerView 在移动视图后会在移动到的位置下方留下一个“幽灵”视图。Random 的答案是在 onItemMoved 方法中添加 'notifyDataSetChanged()',这确实解决了问题,但也破坏了交换动画。当我在短暂延迟后运行 notifyDataSetChanged 时,问题得到了解决。
    notifyItemMoved(position1, position2);
    getActivity().getHandler().postDelayed(new Runnable() {
        @Override
        public void run() {
            notifyDataSetChanged();
        }
    }, 300);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接