Search code examples
androidandroid-recyclerviewgridlayoutmanager

Why RecyclerView items randomly shift postions in GridLayoutManager?


I am using gridLayoutManager to show my RecyclerView items. I have also implemented onClick and onLongClick on items

 public void onItemClicked(int position) {
    final SquareImageView clickedItem = (SquareImageView)(lLayout.findViewByPosition(position));

    if (actionMode != null) {
        if(clickedItem.getPaddingLeft() == 1) clickedItem.setPadding(7,7,7,7);
        else clickedItem.setPadding(1,1,1,1);

    } else {
        thumbView = (SquareImageView)(lLayout.findViewByPosition(position));
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH)
            zoomImageFromThumb(position);
        else
        imageFromThumb(position);
    }
}

@Override
public boolean onItemLongClicked(int position) {
    final SquareImageView clickedItem = (SquareImageView)(lLayout.findViewByPosition(position));
    clickedItem.setPadding(7,7,7,7);
    if (actionMode == null) {
        actionMode = startSupportActionMode(new ActionModeCallback());
        actionMode.getMenu().findItem(R.id.menu_remove).setIcon(new IconDrawable(this, Iconify.IconValue.fa_trash).colorRes(R.color.accent_color).actionBarSize());
    }
    return true;
}

As you can see I just change the padding of the clicked item at longClick and also at click if actionMode is not null.

Everything works as intended: if I long click the first item its padding do change but when I scroll to the bottom of the grid the padding have shifted to the bottom image or some other random image. Again if I scroll to top, top item will have no padding and the padding have shifted to some other random element.

Is this problem due to recycling of elements? How do I get rid of this?


Solution

  • Yes. This problem is due to recycling of views and is the expected behaviour.

    It's simple to understand this. Only a finite set of views are kept in memory when scrolling the recycler view. But as you can see, the padding is applied only once, just after the click. So, what happens after the view is recycled? How will the system remember to add padding again?

    So, the system redraws the view item again by calling the onBindViewHolder() of your RecyclerViewAdapter, with a view holder that could've been of some other recycled item. You need to make sure that every time the onBindViewHolder() is called you do two things -

    1) Set padding if the item is selected (This ensures that your selected item always gets padding), and

    2) Set padding to 0 if item is not selected (This ensures that random items don't get padding. Again, this is expected since a selected item's ViewHolder may be reused for an unselected item!))

    You can use a SparseBooleanArray to store the selected positions and check its value in onBindViewHolder. Remember, you also need to call notifyItemChanged(i) with the position after the click so that that item is redrawn (onBindViewHolder() called again).

    Roughly, you can add two things to your Adapter's code:

    public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder>{
        //...
        private SparseBooleanArray selectedItems = new SparseBooleanArray();
    
        // Call this from your onItemLongClicked()
        public void selectItem(int position){
            selectedItems.put(position, true);
            notifyItemChanged(position);
        }
    
        // Call this in your onItemClicked() to check if position is selected
        public boolean isItemSelected(int position){
            return selectedItems.get(position, false);
        }
    
        @Override
        public void onBindViewHolder(final ViewHolder holder, final int position) {
            // your existing code
            if(selectedItems.get(position, false)){
            holder.itemView.setPadding(7,7,7,7);
            }
            else {
                holder.itemView.setPadding(1,1,1,1);
            }
        }
    }