Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RecyclerView StaggeredGridLayoutManager reordering issue

I'm trying to display three (at least that's the case I have an issue with) items in a RecyclerView with a StaggeredGridLayoutManager with two columns. The first item is spanned across the two rows. Here's how it looks like:

Correct initial behaviour

Now, I'm moving the item "Item 2" to top. Here's the code I call, in the adapter (it's a sample I wrote to demonstrate the issue I have in a more complex project):

private int findById(int id) {
    for (int i = 0; i < items.size(); ++i) {
        if (items.get(i).title.equals("Item " + id)) {
            return i;
        }
    }

    return -1;
}

// Moving the item "Item 2" with id = 2 and position = 0
public void moveItem(int id, int position) {
    final int idx = findById(id);
    final Item item = items.get(idx);

    if (position != idx) {
        items.remove(idx);
        items.add(position, item);
        notifyItemMoved(idx, position);
        //notifyDataSetChanged();
    }
}

After that, the array is fine: [Item 2, Item 1, Item 3]. However, the view is far from fine:

Layout issue

If I touch the RecyclerView (enough to trigger the overscroll effect if there's not enough items to scroll), Item 2 move to the left, where I expected to see it in the first place (with a nice animation):

Expected result

As you maybe saw in the code, I tried to replace notifyItemMoved(idx, position) by a call to notifyDataSetChanged(). It works, but the change is not animated.

I wrote a complete sample to demonstrate this and put it on GitHub. It's nearly minimal (there are options to move the item and toggle their spanning).

I don't see what I can be doing wrong. Is this a bug with StaggeredGridLayoutManager? I would like to avoid notifyDataSetChanged() as I would like to keep consistency regarding the animations.


Edit: after some digging, there's no need for a fully-spanned item to show the issue. I removed the full-span. When I try to move Item 2 to position 0, it doesn't move: Item 1 goes after it, and Item 3 is moved on the right, so I have: empty cell, Item 2, new line, Item 1, Item 3. I still have the correct layout after a scroll.

What's more interesting is that I don't have the issue with a GridLayoutManager. I need a full-span item so it's not a solution, but I guess it's indeed a bug in the StaggeredGridLayoutManager

like image 768
Marc Plano-Lesay Avatar asked Jan 06 '15 14:01

Marc Plano-Lesay


2 Answers

I don't have a complete answer, but I can point you to both a workaround and the bug report (that I believe is related).

The trick to updating the layout so that it looks like your second screenshot, is to call invalidateSpanAssignments() on the StaggeredGridLayoutManger (sglm) after you've called notifyItemMoved(). The "challenge" is that if you call it immediately after nIM(), it won't run. If you delay the call for a few ms, it will. So, in your referenced code for MainActivity, I've made your sglm a private field:

private StaggeredGridLayoutManager sglm;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    adapter = new Adapter();
    recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
    sglm = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
    recyclerView.setLayoutManager(sglm);
    recyclerView.setItemAnimator(new DefaultItemAnimator());
    recyclerView.setAdapter(adapter);
}

And down in the switch block, reference it in a handler:

        case R.id.move_sec_top:
            adapter.moveItem(2, 0);
            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    sglm.invalidateSpanAssignments();
                }
            }, 100);
            return true;

The result is that your animation still runs, the layout ends up the way you want it. This is a real kludge, but it does work. I believe this is the same bug that I found and reported at the following link:

https://code.google.com/p/android/issues/detail?id=93156

While my "symptom" and required call were different, the underlying issue seems to be identical.

Good luck!

EDIT: No need to postDelayed, simply posting will do the trick:

        case R.id.move_sec_top:
            adapter.moveItem(2, 0);
            new Handler().post(new Runnable() {
                @Override
                public void run() {
                    sglm.invalidateSpanAssignments();
                }
            });
            return true;

My original theory was that the call was blocked until the layout pass was over, but I believe that is not the case. Instead, I now think that if you call invalidateSpanAssignments() immediately, it actually executes too soon (before the layout changes have completed). So, the post above (without delay) simply adds the call to the end of the rendering queue where it happens after the layout.

like image 116
MojoTosh Avatar answered Oct 21 '22 22:10

MojoTosh


Well I have done this way.

StaggeredGridLayoutManager gaggeredGridLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL);
gaggeredGridLayoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
recyclerView.setLayoutManager(gaggeredGridLayoutManager);

dataList = YourDataList (Your Code for Arraylist);

recyclerView.setItemAnimator(new DefaultItemAnimator());
recyclerAdapter = new DataAdapter(dataList, recyclerView);
recyclerView.setAdapter(recyclerAdapter);

// Magic line
recyclerView.addOnScrollListener(new ScrollListener());

Create class for Custom RecyclerView Scroll Listener.

private class ScrollListener extends RecyclerView.OnScrollListener {
   @Override
   public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        gaggeredGridLayoutManager.invalidateSpanAssignments();
   }
}

Hope this will help you.

like image 26
Hiren Patel Avatar answered Oct 22 '22 00:10

Hiren Patel