Reputation: 3560
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:
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
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
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
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
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
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