4

I have a list of strings in a RecyclerView. I need to be able to click on an item in the list and change the background yellow. I am able to click on an item and change the background, but when I scroll down the list, I see another item in the list has also had its background turned to yellow?

I have tried many different ways of implementing this. I tried setting up an Interface, having the click in the ViewHolder etc.

My code is below.

Activity:

[Activity(Label = "PregnancyListActivity")]
public class PregnancyListActivity : Activity
{
    RecyclerView mRecyclerView;
    RecyclerView.LayoutManager mLayoutManager;
    PregnantAnimalAdapter mAdapter;

    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
        SetContentView(Resource.Layout.PregnancyListLayout);

        mRecyclerView = FindViewById<RecyclerView>(Resource.Id.recyclerView);

        // Plug in the linear layout manager:
        mLayoutManager = new LinearLayoutManager(this);
        mRecyclerView.SetLayoutManager(mLayoutManager);

        // Plug in my adapter:
        mAdapter = new PregnantAnimalAdapter();
        mAdapter.ItemClick += MAdapter_ItemClick;
        mRecyclerView.SetAdapter(mAdapter);
    }
}

View Holder

[Activity(Label = "PregnantAnimalViewHolder")]
public class PregnantAnimalViewHolder : RecyclerView.ViewHolder
{
    public LinearLayout mBackground { get; private set; }
    public TextView mAnimalTag { get; private set; }

    public PregnantAnimalViewHolder(View itemView) : base(itemView)
    {
        // Locate and cache view references:
        mBackground = itemView.FindViewById<LinearLayout>(Resource.Id.pregnancyBackground);
        mAnimalTag = itemView.FindViewById<TextView>(Resource.Id.animalTag);

    }
}

Adapter:

public class PregnantAnimalAdapter : RecyclerView.Adapter
{
public PregnantAnimalAdapter pregnantAnimalAdapter;
public List<PregAnimals> mAnimals;

public PregnantAnimalAdapter()
{
    mAnimals = new List<PregAnimals>{
        new PregAnimals("111", false),
        new PregAnimals("112", false),
        new PregAnimals("113", false),
        new PregAnimals("114", false),
        new PregAnimals("115", false),
        new PregAnimals("221", false),
        new PregAnimals("222", false),
        new PregAnimals("223", false),
        new PregAnimals("224", false),
        new PregAnimals("225", false),
        new PregAnimals("331", false),
        new PregAnimals("332", false),
        new PregAnimals("333", false),

        new PregAnimals("444", false),
        new PregAnimals("443", false),
        new PregAnimals("554", false),
        new PregAnimals("4435", false),
        new PregAnimals("4234", false),
        new PregAnimals("543", false),
        new PregAnimals("3452", false),
        new PregAnimals("3321", false),
        new PregAnimals("6676", false),
        new PregAnimals("4367", false)
    };
}

public override RecyclerView.ViewHolder OnCreateViewHolder(ViewGroup parent, int viewType)
{
    View itemView = LayoutInflater.From(parent.Context).
                                  Inflate(global::RecordMilk.Resource.Layout.PregnancyItem, parent, false);

    PregnantAnimalViewHolder vh = new PregnantAnimalViewHolder(itemView);

    vh.mBackground.Click += (object sender, EventArgs e) => {
        mAnimals[vh.LayoutPosition].Preg = true;
    };

    return vh;
}



public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
    PregnantAnimalViewHolder vh = holder as PregnantAnimalViewHolder;

    vh.mAnimalTag.Text = mAnimals[position].Tag;
    if (mAnimals[position].Preg == true)
    {
        vh.mBackground.SetBackgroundResource(RecordMilk.Resource.Color.munster_yellow);
    }

}



public override int ItemCount
{
    get { return mAnimals.Count; }
}
}

public class PregAnimals
{
    public string Tag { get; set; }
    public bool Preg { get; set; }

    public PregAnimals(string tag, bool preg)
    {
        Tag = tag;
        Preg = preg;
    }

}

I am beyond confused as to why it won't just set the item in the list. Any help would be hugely appreciated.

Thanks in advance!

SmiffyKmc
  • 801
  • 1
  • 16
  • 34
  • You need to read the RecyclerView documentation better :) Rather than directly operate on the recycler view, you usually store the position in on click, then call notifyItemChanged and apply the selection in onBindViewHolder. You likely need to also unselect the previously selected entry. – sigsegv Jul 09 '18 at 14:23
  • "You need to read the RecyclerView documentation better :)" - haha thanks. I've been reading through so many examples and the Xamarin site, but after trying to read and understand while running it, the mind is fuzzled. "You likely need to also unselect the previously selected entry" - yeah that's step two after I am able to finally get step 1 working correctly :(. – SmiffyKmc Jul 09 '18 at 14:27
  • 1
    Working directly with RecyclerView's children views is usually a bad idea because the RV, well, recycles them. When the child scrolls out of the screen, it is cached and can be reintroduced anytime as the outlet view for a totally different entry. If you don't account for this in onBindViewHolder you'll end up with an inconsistent view status. Support libraries 28.0.0 added selection handling, but I haven't used them yet, so I cannot comment. You might want to check them out. – sigsegv Jul 09 '18 at 14:33
  • @sigsegv - that's really helpful actually. Thanks! Best do it right so if it takes a bit of study then that's what it takes. Thanks man – SmiffyKmc Jul 09 '18 at 14:35

2 Answers2

9

Try to add this line in the adapter:

@Override
public int getItemViewType(int position) {
    return position; 
}
m02ph3u5
  • 3,022
  • 7
  • 38
  • 51
1

The problem is the RecyclerView is doing what it should - recycling views. The View whose color you changed, is being reused for a new item, but its color is still selected.

Assuming you want the selection to stay, you need to keep track of which one is selected, and in the OnBindViewHolder set the background color to the selected or unselected color. Otherwise just set the background to unselected.

Also, you can just create a StateListDrawable and assign that to the background that has a state for selected/unselected, so all you need to do is set the View's state to selected. The advantage is it keeps all of your styling in resources.

public class PregnantAnimalViewHolder : RecyclerView.ViewHolder
{
    public LinearLayout mBackground { get; private set; }
    public TextView mAnimalTag { get; private set; }
    public bool Selected {get;set;}
    public int position {get;set;}

    public PregnantAnimalViewHolder(View itemView) : base(itemView)
    {
        // Locate and cache view references:
        mBackground = itemView.FindViewById<LinearLayout>(Resource.Id.pregnancyBackground);
        mAnimalTag = itemView.FindViewById<TextView>(Resource.Id.animalTag);

    }
}

Then in the OnBindViewHolder

public override void OnBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
    PregnantAnimalViewHolder vh = holder as PregnantAnimalViewHolder;

    vh.mAnimalTag.Text = mAnimals[position];
    vh.position = position;
    vh.View.SetBackgroundResource(
         vh.Selected?Resource.Color.munster_yellow:Resource.Color.whatever);

    // or vh.View.SetSelected(vh.Selected) for using StateListDrawable as the background

}
Curtis Shipley
  • 7,990
  • 1
  • 19
  • 28
  • Also, the PregnantAnimalViewHolder doesn't the the Activity attribute since it isn't an Activity :) – Curtis Shipley Jul 09 '18 at 14:36
  • Totally forgot the Activity Attribute was there xD. Good catch! Your solution makes total sense now (facepalm). I was so focused on trying to solve why it wasn't working I didn't just sit back and think it's "Recycling" the view. Going to try as you said and will come back with the good news hopefully :) – SmiffyKmc Jul 09 '18 at 14:40
  • Glad I could help. I wrote all of that off the top of my head so there may be a syntax error or two. – Curtis Shipley Jul 09 '18 at 14:42
  • I think the "Selected" attribute clarification just really shune the light on the issue! a syntax error or two is acceptable ;P – SmiffyKmc Jul 09 '18 at 14:45
  • Will do once I try adding what you recommended :) – SmiffyKmc Jul 09 '18 at 15:28
  • I tried adding the selected bool and having the background change depending on the selected item, but it still seems to be the same. I'm writing up a List Collection of Animal Tags and If they are selected. How would you go about having an item in the list associate an item in the RecyclerView? – SmiffyKmc Jul 10 '18 at 13:51
  • A couple potential ways. You could add a position/index to the ViewHolder, so given a ViewHolder you can look up the item, or you could add a reference to the item directly from the ViewHolder. – Curtis Shipley Jul 10 '18 at 13:53
  • I added the position to the ViewHolder in the example. – Curtis Shipley Jul 10 '18 at 13:55
  • No luck it seems :(. I tried your way and nothing (thanks btw for the help) and used your idea of the position to create a class and try changing the background by checking an attribute in the new PregAnimal Class I made :/. The Recycler View is still showing other Items with a background changed :/ – SmiffyKmc Jul 10 '18 at 15:29
  • Honestly, then there is a bug in something you're doing, or in my code, since I wrote that off the top of my head. This is a very common issue people run into with RecyclerViews and ListViews. Trace statements in the OnBindViewHolder so you can see exactly what is being set in each view as it is bound. – Curtis Shipley Jul 10 '18 at 15:49
  • Make sure you deselect any existing ones in the on click. If you just have a single selection, you don't necessarily need the selected flag in the view holder, you can just keep a SelectedIndex property in the adapter. Then deselect it. This may help https://stackoverflow.com/questions/28972049/single-selection-in-recyclerview – Curtis Shipley Jul 10 '18 at 16:02