3

I've been trying to use the new RecyclerView widget for a couple of days now and thought I've managed to create a Horizontal and Grid layout and also overcome the fact that it lacks of a straightforward way of implementing a OnClickListener (I used GreenRobot's EventBus for this) , I'm still having a hard time selecting an item and preserving that state (style) throughout the recycling process.

This is the code I'm using for my adapter

public class ArticuloItemAdapter extends RecyclerView.Adapter<ArticuloItemAdapter.ItemHolder> {
    private ArrayList<ArticuloObject> mItems;
    private LayoutInflater mLayoutInflater;
    private int mSize;
    private int mSelectedPosition;
    private RecyclerView mRecyclerView;

    public ArticuloItemAdapter(Context context, RecyclerView rv, ArrayList<ArticuloObject> articulos) {

        mLayoutInflater = LayoutInflater.from(context);
        mItems=articulos ;     
        mSize = context.getResources()
                .getDimensionPixelSize(R.dimen.icon);
        mSelectedPosition = -1;
        mRecyclerView = rv;
    }

    @Override
    public ItemHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = mLayoutInflater.inflate(R.layout.gallery_recycler_row, parent, false);
        return new ItemHolder(itemView, this);
    }

    @Override
    public void onBindViewHolder(ItemHolder holder, int position) {      
        Ion.with(holder.getPicImageView())
                .placeholder(R.drawable.owner_placeholder)
                .resize(mSize, mSize)
                .centerCrop()
                .error(R.drawable.owner_error)
                .load(mItems.get(position).getUrl());

        //Update the views as they got recycled
        if (mSelectedPosition != position) {
            holder.getPicImageView().setBackgroundColor(Color.TRANSPARENT);
        } else {
            holder.getPicImageView().setBackgroundColor(Color.CYAN);
        }
    }

    @Override
    public int getItemCount() {
        return mItems.size();
    }

    public int getSelectedPosition() {
        return mSelectedPosition;
    }

    public void setSelectedPosition(int mSelectedPosition) {
        this.mSelectedPosition = mSelectedPosition;
    }

    public RecyclerView getRecyclerView() {
        return mRecyclerView;
    }

    /* Required implementation of ViewHolder to wrap item view */
    public class ItemHolder extends RecyclerView.ViewHolder implements
            View.OnClickListener {
        private ArticuloItemAdapter mParent;
        private ImageView mPicImageView;

        public ItemHolder(View itemView, ArticuloItemAdapter parent) {
            super(itemView);
            itemView.setOnClickListener(this);

            mParent = parent;

            mPicImageView = (ImageView) itemView.findViewById(R.id.picture_image_view);
        }

        public ImageView getPicImageView() {
            return mPicImageView;
        }

        @Override
        public void onClick(View v) {
            EventBus.getDefault().post(new OnArticuloCartaClickEvent(this, getPosition()));
            setItemActivated(v);
        }

        public void setItemActivated(View v) {

            //Check position of previous selected item. The first time an item is selected, the previous position will be  -1
            if (mParent.getSelectedPosition() >= 0) { 
                ItemHolder row = (ItemHolder) mParent.getRecyclerView().findViewHolderForPosition(mParent.getSelectedPosition());
                if (row != null) {
                    row.getPicImageView().setBackgroundColor(Color.TRANSPARENT);
                }
            }
            mParent.setSelectedPosition(getPosition());
            v.findViewById(R.id.picture_image_view).setBackgroundColor(Color.CYAN);

        }
    }

}

Now in the code shown above, all I'm doing to keep track of the selected item is the following:

I. In the onClick(View v) event:

  1. Whenever an item is clicked, I call the setItemActivated method
  2. In the setItemActivated method, I check if the selectedPosition is greater than or equal to 0 ( the default is -1 when no item is selected).
  3. If the previous condition is true I try to get the ViewHolder in that position and if that View is not null, I change the background colour of the ImageView back to it's original background colour (transparent)
  4. Set the new value of mSelectedPosition to the current position using getPosition()
  5. Finally, change the background colour of the ImageView inside the current ViewHolder to a different colour (CYAN in this case)

II. In the onBindViewHolder(ItemHolder holder, int position) method:

If position of selected item (mSelectedPosition) is different from the position of the view being displayed, I change the background colour of the ImageView back to its default colour. Otherwise, I change the colour of the ImageView to the colour used to identify a selected item (CYAN).

All this works as long as the RecyclerView has a considerable number of item that it needs to start recycling the views. But if it doesn't, like in my case, I've noticed that findViewHolderForPosition always returns null, and as a result, I cannot change the colour of the previously selected item back to its default colour and so I ended up with more than one item selected.

enter image description here

And to make matters worse, as the RecyclerView doesn't have to recycle anything, the onBindViewHolder method never gets call and therefore I can't count on the code inside to adjust the colour of the items accordingly.

Please, I'd really appreciate if anyone could tell me what I'm doing wrong here or perhaps give some ideas. It's really frustrating not being able to do something as basic as selecting an item.

Thanks in advance.

eddy
  • 4,373
  • 16
  • 60
  • 94

1 Answers1

4

This is happening because when selected item changes, you are not telling to RecyclerView that the previous item is invalidated.

Instead of trying to set it manually, do the following:

public void setItemActivated(View v) {
   final int myPos = getAdapterPosition();
   if (myPos == RecyclerView.NO_POSITION) {
       // very rare case where item is clicked but its adapter position 
       // is unknown. ignore the event.
       // See docs of getAdapterPosition for details
   }
   final int prevSelected = mParent.getSelectedPosition();
   mParent.setSelectedPosition(myPos);
   if (prevSelected >= 0 && prevSelected < mParent.getItemCount()) {
       mParent.notifyItemChanged(prevSelected);
   }
   mParent.notifyItemChanged(myPos);
}

RecyclerView will call onBind for Views that should be updated. You don't need to care about where previous selected item is.

yigit
  • 37,683
  • 13
  • 72
  • 58
  • Thank you @yigit. Could you tell where I can find this `getAdapterPosition` method? I've tried to use it everywhere in my adapter or within the `ViewHolder` , but the method is not recognized? From the docs it looks like it is something like `getPosition()`, but I don't understand why I can't use it. – eddy Apr 05 '15 at 18:28
  • I guess you don't have the latest version of the support library. https://developer.android.com/reference/android/support/v7/widget/RecyclerView.ViewHolder.html#getAdapterPosition() – yigit Apr 05 '15 at 20:59
  • But I have used `compile 'com.android.support:appcompat-v7:21.0.3'` and `compile 'com.android.support:recyclerview-v7:21.0.3' `. I even used the "+" wildcard to get the latest version, but I still can't use `getAdapterPosition` – eddy Apr 05 '15 at 21:05
  • Sorry about that. Effectively, it turned out to be that there was a revision 22 I was not aware of. After I installed it I was able to use `getAdapterPosition`. One last question, when you compare this `if (myPos == RecyclerView.NO_POSITION)`, you said that I should ignore the event. If the event is the `onClick` event, shouldn't that validation be done before I call `setItemActivated`, passing in the value of `getAdapterPosition` ? – eddy Apr 05 '15 at 22:44
  • 1
    Sure, I don't have the rest of your code. Again, this is a very edge case. Actually, it will only happen if you call `notifyDataSetChanged` or `setAdapter / swapAdapter` and the new layout is not calculated yet. – yigit Apr 06 '15 at 05:41
  • Got it! Thank you very much @yigit . You've been most helpful. – eddy Apr 06 '15 at 11:53