简体   繁体   中英

RecyclerView and ItemTouchHelper swipe to remove issue

I'm trying to implement "swipe to remove" feature via RecyclerView and ItemTouchHelper. I have a strange problem and I can't locate the issue for the life of me. I swipe an item away from the top (not the very first one), it goes away, so far so good. When I scroll away and come back, there is an artefact in the row above swiped away item. Looks like that row is not drawn (or maybe is x translated?). Video shows the issue.

Steps of the video:

  1. I swipe away Item 2
  2. Scroll down to the bottom
  3. Come back
  4. Item 1 is no longer visible
  5. I scroll down to the bottom again
  6. I scroll back up
  7. Everything is fine now
  8. Again, but with a third (not the second) item from the top, same problem
  9. Again, with the very first item, no issue

在此处输入图像描述

Relevant code: (whole github sample app here )

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);
    }

}

}

EDIT:

I have a hack that removes this glitch, but still I want to know the cause and how can I really fix the issue. The hack is calling notifyDataSetChanged() but after the animations are done (otherwise animation gets terminated). Basically I add an ItemDecorator and figure out that an animation ended.

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);
        }
});

Try adding notifyDataSetChanged() in your remove method

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

notifyItemRemoved(position) notifies the RecyclerView Adapter that data in adapter has been removed at a particular position.

notifyDataSetChanged() notifies the attached observers that the underlying data has been changed and any View reflecting the data set should refresh itself.

UPDATE

Try adding mRecyclerView.removeViewAt(position); before notifyItemRemoved(position); This will not mess with the animation.

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);
        }

    }

}

UPDATE: 23.1.1 fixes the bug. Don't use 23.1.0.

Looks like this bug is a regression in the recycler view support library. I think this is the commit causing it, but still don't 100% understand the situation so don't hold me on that.

The bug manifests itself with com.android.support:recyclerview-v7 version 23.1.0 but not with 23.0.1 or 22.2.1

I'll try to find a correct place to report it and will post the link in the comment of this answer.

I want to suggest you see that link because it is the easiest way to implement this functionality Drag and swipe with RecyclerView using ItemTouchHelper

I was having a similar issue with swipe-to-reorder. The RecyclerView was leaving a "ghost" of the view just moved underneath the view that moved into that position. Random's answer of adding 'notifyDataSetChanged()' to my onItemMoved method works, but that destroys the swapping animation. The problem was solved when I ran notifyDataSetChanged after a short delay:

    notifyItemMoved(position1, position2);
    getActivity().getHandler().postDelayed(new Runnable() {
        @Override
        public void run() {
            notifyDataSetChanged();
        }
    }, 300);

I was able to resolve this problem by setting the adapter again on the recyclerview. Upon deletion from your list, simply....

RecyclerView.setAdapter(this);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM