Search code examples
androidkotlinandroid-recyclerviewstaggered-gridview

Jumbled tiles in itemTouchHelperCallback in StaggeredGridLayoutManager


I am using StaggeredGridLayoutManager in my code.

Take a look at this first:

my

I am facing these problems:

In 1st attempt, I move the tile '1' directly upwards by one step and then stop. Everything looks good and the tile labled as '1' replaces label '2'.

In 2nd attempt, I drag the tile labeled '3' up but do not stop. Still it automatically gets replaced by '1' which is present just above '3'. Something is trouble here.

In my 3rd and 4th attempt, I try to drag my tile up & sideways. Once I do it the tile automatically gets right shifted and out of the frame(even though my touch is still holding it). It can be visible only when I drag it left. Weird!

Now take a look at my code

In my MainActivity's onCreateMethod

val layoutmanager = StaggeredGridLayoutManager(if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) 3 
                                              else 2, StaggeredGridLayoutManager.VERTICAL)
layoutmanager.gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
recyclerView.layoutManager = layoutmanager
noteAdapter = NoteAdapter(myList, this)
recyclerView.adapter = noteAdapter

My ItemTouchHelperCallback method

private val itemTouchHelperCallback =
            object :
                    ItemTouchHelper.SimpleCallback(0, 0
                    ) {

                override fun isLongPressDragEnabled(): Boolean {
                    return true
                }

                override fun isItemViewSwipeEnabled(): Boolean {
                    return true
                }

                override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
                    val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN or
                            ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
                    val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END
                    return makeMovementFlags(dragFlags, swipeFlags)
                }

                override fun onMove(
                        recyclerView: RecyclerView,
                        viewHolder: RecyclerView.ViewHolder,
                        target: RecyclerView.ViewHolder
                ): Boolean {
                    val fromPosition = viewHolder.adapterPosition
                    val toPosition = target.adapterPosition

                    if (fromPosition < toPosition) {
                        for (i in fromPosition until toPosition) {
                            Collections.swap(myList, i, i + 1)
                        }
                    } else {
                        for (i in fromPosition downTo toPosition + 1) {
                            Collections.swap(myList, i, i - 1)
                        }
                    }
                    noteAdapter.notifyItemMoved(fromPosition, toPosition)


                    return false
                }
                override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
                    
                }

            }

UPDATE

@Zain found out that the real problem was hidden in my NoteAdapter.java, so I am highlighting the faulty method. Check his answer.

 @Override
    public int getItemViewType(int position) {
        return position;
    }

Solution

  • The problem:

    Returning the item position from getItemViewType() of the RecyclerView adapter; while you only have one item view type.

    Short Answer:

    You shouldn't override getItemViewType() to keep it to the default, or you can make it return 0 to indicate that there is a single view type for the adapter.

    Long Answer:

    When the adapter is first attached to the RecyclerView, The RecyclerView's LayoutManager needs to know the layout type of each item position; and to do that it calls getItemViewType(position) for each single position in the adapter; so if the adapter list has 10 items, then it's called 10 times; each time it is called, then it returns the type of this particular item.

    When getItemViewType(position) is not overriden the returned value for all of the items is 0; which indicates that all the items have the same type.

    By logging that while returning 0:

    2021-01-13 11:05:25.941 : getItemViewType:  Pos = 0 Return: 0
    2021-01-13 11:05:25.950 : onBindViewHolder: Pos: 0
    2021-01-13 11:05:25.954 : getItemViewType:  Pos = 1 Return: 0
    2021-01-13 11:05:25.959 : onBindViewHolder: Pos: 1
    2021-01-13 11:05:25.962 : getItemViewType:  Pos = 2 Return: 0
    2021-01-13 11:05:25.970 : onBindViewHolder: Pos: 2
    2021-01-13 11:05:25.974 : getItemViewType:  Pos = 3 Return: 0
    2021-01-13 11:05:25.979 : onBindViewHolder: Pos: 3
    2021-01-13 11:05:25.982 : getItemViewType:  Pos = 4 Return: 0
    2021-01-13 11:05:25.988 : onBindViewHolder: Pos: 4
    2021-01-13 11:05:26.000 : getItemViewType:  Pos = 0 Return: 0
    2021-01-13 11:05:26.001 : getItemViewType:  Pos = 1 Return: 0
    2021-01-13 11:05:26.001 : getItemViewType:  Pos = 2 Return: 0
    2021-01-13 11:05:26.001 : getItemViewType:  Pos = 3 Return: 0
    2021-01-13 11:05:26.001 : getItemViewType:  Pos = 4 Return: 0
    

    But as you instead return position from getItemViewType(position); then you have as many types of items as the number of positions you have (i.e. the size of the list).

    The log in this case:

    2021-01-13 11:11:30.123 : getItemViewType:  Pos = 0 Return: 0
    2021-01-13 11:11:30.145 : onBindViewHolder: Pos: 0
    2021-01-13 11:11:30.149 : getItemViewType:  Pos = 1 Return: 1
    2021-01-13 11:11:30.160 : onBindViewHolder: Pos: 1
    2021-01-13 11:11:30.164 : getItemViewType:  Pos = 2 Return: 2
    2021-01-13 11:11:30.172 : onBindViewHolder: Pos: 2
    2021-01-13 11:11:30.175 : getItemViewType:  Pos = 3 Return: 3
    2021-01-13 11:11:30.179 : onBindViewHolder: Pos: 3
    2021-01-13 11:11:30.183 : getItemViewType:  Pos = 4 Return: 4
    2021-01-13 11:11:30.190 : onBindViewHolder: Pos: 4
    2021-01-13 11:11:30.199 : getItemViewType:  Pos = 0 Return: 0
    2021-01-13 11:11:30.199 : getItemViewType:  Pos = 1 Return: 1
    2021-01-13 11:11:30.200 : getItemViewType:  Pos = 2 Return: 2
    2021-01-13 11:11:30.200 : getItemViewType:  Pos = 3 Return: 3
    2021-01-13 11:11:30.200 : getItemViewType:  Pos = 4 Return: 4
    

    Now the RecyclerView is in the steady state (i.e. populated with all the items), and we are going to drag one item and drop it on another.

    Now we'll Replace Note no.1 with 3 (i.e. replacing position 4 with position 2 in the adapter)

    The log when getItemViewType() returns 0

    2021-01-13 11:14:26.620 : getItemViewType:  Pos = 0 Return: 0
    2021-01-13 11:14:26.620 : getItemViewType:  Pos = 1 Return: 0
    2021-01-13 11:14:26.620 : getItemViewType:  Pos = 2 Return: 0
    2021-01-13 11:14:26.621 : getItemViewType:  Pos = 3 Return: 0
    2021-01-13 11:14:26.621 : getItemViewType:  Pos = 4 Return: 0
    

    Here the getItemViewType is called only once for each position and that is because you have only a single view type. And there is no calls to onBindViewHolder as we just called notifyItemMoved so that the RecyclerView only calls gets cached/recycled versions as all the view types are the same in this case.

    The log when getItemViewType() returns position

    2021-01-13 11:12:13.659 : getItemViewType:  Pos = 0 Return: 0
    2021-01-13 11:12:13.660 : getItemViewType:  Pos = 1 Return: 1
    2021-01-13 11:12:13.660 : getItemViewType:  Pos = 2 Return: 2
    2021-01-13 11:12:13.660 : getItemViewType:  Pos = 2 Return: 2
    2021-01-13 11:12:13.665 : onBindViewHolder: Pos: 2
    2021-01-13 11:12:13.667 : getItemViewType:  Pos = 3 Return: 3
    2021-01-13 11:12:13.668 : getItemViewType:  Pos = 3 Return: 3
    2021-01-13 11:12:13.673 : onBindViewHolder: Pos: 3
    2021-01-13 11:12:13.675 : getItemViewType:  Pos = 4 Return: 4
    2021-01-13 11:12:13.675 : getItemViewType:  Pos = 4 Return: 4
    2021-01-13 11:12:13.681 : onBindViewHolder: Pos: 4
    

    Here the getItemViewType is called multiple times as each item has its unique view type, and it needs to recalculate that View type, and also redraw (or recalculate the layout) of the item using onBindViewHolder for the from (item 2) & to (item 4) items and the items in-between (item 3).

    Making these multiple view types make you unable to complete the Drag (probably due to new layout recalculation), and also let the intermediate Views unable to shift between the dragged and dropped-on items.