Kernald Kernald - 3 months ago 43
Android Question

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

Answer

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.

Comments