Nemanja Kovacevic
Nemanja Kovacevic

Reputation: 3560

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

enter image description here

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

Upvotes: 4

Views: 22460

Answers (5)

Tommy Weisend
Tommy Weisend

Reputation: 1

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

RecyclerView.setAdapter(this);

Upvotes: 0

Lee Hounshell
Lee Hounshell

Reputation: 862

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

Upvotes: 0

Ritesh Mathur
Ritesh Mathur

Reputation: 829

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

Upvotes: 1

Nemanja Kovacevic
Nemanja Kovacevic

Reputation: 3560

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.

Upvotes: 3

random
random

Reputation: 10309

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

    }

}

Upvotes: 9

Related Questions