So I'm back to android programming after a few years and I'm struggling with something that I thought would be very simple. I'm populating a listview using a custom array adapter with a custom layout.
Basically, every item of the array list is a custom class (named Card) with 4 properties: (int)id, (String)name, (boolean)include,(int)deck (the idea is that the user selects which cards to include in each of 3 different decks).
I created a simple layout with 2 textviews, 1 checkbox and 1 radiogroup with 3 radio buttons inside (and a bunch of icons) (see below).
My problem is that although the listview gets populated and it is shown correctly, whenever I (the user) changes something (e.g. hit the checkbox or the radio buttons) and I scroll, everything is messed up (the selection I made gets copied to other rows). I tried, to no avail, setting listeners inside the getView()
method of the adapter and notifying data changes. I know views are recycled, but clearly I don't understand properly how. Any help would be greatly appreciated.
card_layout.xml
<ImageView
android:id="@+id/icon_eye"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center_vertical"
app:srcCompat="@drawable/eye" />
<TextView
android:id="@+id/tv_card_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginLeft="5dp"
android:layout_weight="1"
android:visibility="gone"
android:text="" />
<TextView
android:id="@+id/tv_card_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginLeft="5dp"
android:layout_weight="2"
android:text="TextView" />
<CheckBox
android:id="@+id/cb_include"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="30dp"
android:layout_marginRight="10dp"
android:checked="true"
android:text="" />
<RadioGroup
android:id="@+id/rbg_decks"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal">
<ImageView
android:id="@+id/icon_deck_1"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center_vertical"
android:clickable="false"
android:focusable="false"
app:srcCompat="@drawable/die_1" />
<RadioButton
android:id="@+id/rb_deck_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="30dp"
android:layout_marginRight="30dp"
android:text="" />
<ImageView
android:id="@+id/icon_deck_2"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center_vertical"
android:clickable="false"
android:focusable="false"
app:srcCompat="@drawable/die_2" />
<RadioButton
android:id="@+id/rb_deck_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="30dp"
android:layout_marginRight="30dp"
android:text="" />
<ImageView
android:id="@+id/icon_deck_3"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center_vertical"
android:clickable="false"
android:focusable="false"
app:srcCompat="@drawable/die_3" />
<RadioButton
android:id="@+id/rb_deck_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="" />
</RadioGroup>
This is the custom adapter
AdapterCard.xml
public class AdapterCard extends ArrayAdapter<Card> {
Activity context;
AdapterCard(Activity context, ArrayList<Card> list_cards)
{
super(context, R.layout.card_layout, list_cards);
this.context = context;
}
static class ViewHolder {
private ImageView icon_eye,icon_deck_1,icon_deck_2,icon_deck_3;
private TextView tv_card_name, tv_card_id;
private CheckBox cb_include;
private RadioGroup rbg_decks;
private RadioButton rb_1,rb_2,rb_3;
}
public View getView(int position, View convertView, ViewGroup parent)
{
ViewHolder mViewHolder = null;
if(convertView == null){
mViewHolder = new ViewHolder();
LayoutInflater vi = context.getLayoutInflater();
convertView = vi.inflate(R.layout.card_layout, parent, false);
mViewHolder.icon_eye = (ImageView) convertView.findViewById(R.id.icon_eye);
mViewHolder.icon_deck_1 = (ImageView) convertView.findViewById(R.id.icon_deck_1);
mViewHolder.icon_deck_2 = (ImageView) convertView.findViewById(R.id.icon_deck_2);
mViewHolder.icon_deck_3 = (ImageView) convertView.findViewById(R.id.icon_deck_3);
mViewHolder.tv_card_name = (TextView) convertView.findViewById(R.id.tv_card_name);
mViewHolder.tv_card_id = (TextView) convertView.findViewById(R.id.tv_card_id);
mViewHolder.cb_include = (CheckBox) convertView.findViewById(R.id.cb_include);
mViewHolder.rbg_decks = (RadioGroup) convertView.findViewById(R.id.rbg_decks);
mViewHolder.rb_1 = (RadioButton) convertView.findViewById(R.id.rb_deck_1);
mViewHolder.rb_2 = (RadioButton) convertView.findViewById(R.id.rb_deck_2);
mViewHolder.rb_3 = (RadioButton) convertView.findViewById(R.id.rb_deck_3);
convertView.setTag(mViewHolder);
} else{
mViewHolder = (ViewHolder) convertView.getTag();
}
Card card_i = getItem(position);
mViewHolder.tv_card_name.setText(card_i.name);
mViewHolder.tv_card_id.setText(card_i.getStringId());
// I would like to do this only ONCE to initialize the radiobuttons
//with the card default deck data but as it is now,
//every time I scroll, the radiobuttons are reset
switch(card_i.deck) {
case 1:
mViewHolder.rb_1.setChecked(true);
break;
case 2:
mViewHolder.rb_2.setChecked(true);
break;
case 3:
mViewHolder.rb_3.setChecked(true);
break;
default:
break;
}
mViewHolder.cb_include.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
card_i.include = isChecked;
AdapterCard.this.notifyDataSetChanged();
}
});
/*mViewHolder.rbg_decks.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
card_i.deck = checkedId;
AdapterCard.this.notifyDataSetChanged();
}
});*/
return convertView;
}
}
And this is the part in the MainActivity where I set the adapter:
ArrayList<Card> list = ... //populate the list
listView = findViewById(R.id.lv_cards);
AdapterCard adapter = new AdapterCard(this,list);
listView.setAdapter(adapter);
EDIT: I forgot to add that the final goal is to recover all the user selections by looping through the whole list. So far I have managed to get the Card object, but since the position changes when scrolling, it is not useful:
for(int i = 0; i < adapter.getCount(); i++){
Card card_i = adapter.getItem(i);
// retrieve card data to generate the decks
}
I finally managed to do it by simply storing the states I was interested on in boolean lists and listening to "clicks" and instead of changed states. The Adapter now looks:
public class AdapterCard extends ArrayAdapter<Card> {
Activity context;
public List<Boolean> cb_include_state, cb_must_include_state, rb_deck_1_state, rb_deck_2_state, rb_deck_3_state;
AdapterCard(Activity context, ArrayList<Card> list_cards)
{
super(context, R.layout.card_layout, list_cards);
this.context = context;
// Initialize states
cb_include_state = new ArrayList<Boolean>(Collections.nCopies(list_cards.size(), true));
cb_must_include_state = new ArrayList<Boolean>(Collections.nCopies(list_cards.size(), false));
rb_deck_1_state = new ArrayList<Boolean>(Collections.nCopies(list_cards.size(), false));
rb_deck_2_state = new ArrayList<Boolean>(Collections.nCopies(list_cards.size(), false));
rb_deck_3_state = new ArrayList<Boolean>(Collections.nCopies(list_cards.size(), false));
for (int i=0; i<list_cards.size(); i++) {
Card card_i = list_cards.get(i);
switch(card_i.deck) {
case 1:
rb_deck_1_state.set(i,true);
break;
case 2:
rb_deck_2_state.set(i,true);
break;
case 3:
rb_deck_3_state.set(i,true);
break;
default:
break;
}
}
}
static class ViewHolder {
private ImageView icon_eye,icon_deck_1,icon_deck_2,icon_deck_3;
private TextView tv_card_name, tv_card_id;
private CheckBox cb_include, cb_must_include;
private RadioGroup rbg_decks;
private RadioButton rb_1,rb_2,rb_3;
}
public View getView(int position, View convertView, ViewGroup parent)
{
ViewHolder mViewHolder = null;
if(convertView == null){
mViewHolder = new ViewHolder();
LayoutInflater vi = context.getLayoutInflater();
convertView = vi.inflate(R.layout.card_layout, parent, false);
mViewHolder.icon_eye = (ImageView) convertView.findViewById(R.id.icon_eye);
mViewHolder.icon_deck_1 = (ImageView) convertView.findViewById(R.id.icon_deck_1);
mViewHolder.icon_deck_2 = (ImageView) convertView.findViewById(R.id.icon_deck_2);
mViewHolder.icon_deck_3 = (ImageView) convertView.findViewById(R.id.icon_deck_3);
mViewHolder.tv_card_name = (TextView) convertView.findViewById(R.id.tv_card_name);
mViewHolder.tv_card_id = (TextView) convertView.findViewById(R.id.tv_card_id);
mViewHolder.cb_include = (CheckBox) convertView.findViewById(R.id.cb_include);
mViewHolder.cb_must_include = (CheckBox) convertView.findViewById(R.id.cb_must_include);
mViewHolder.rbg_decks = (RadioGroup) convertView.findViewById(R.id.rbg_decks);
mViewHolder.rb_1 = (RadioButton) convertView.findViewById(R.id.rb_deck_1);
mViewHolder.rb_2 = (RadioButton) convertView.findViewById(R.id.rb_deck_2);
mViewHolder.rb_3 = (RadioButton) convertView.findViewById(R.id.rb_deck_3);
convertView.setTag(mViewHolder);
} else{
mViewHolder = (ViewHolder) convertView.getTag();
}
Card card_i = getItem(position);
mViewHolder.tv_card_name.setText(card_i.name);
mViewHolder.tv_card_id.setText(card_i.getStringId());
mViewHolder.cb_include.setChecked(cb_include_state.get(position));
mViewHolder.cb_must_include.setChecked(cb_must_include_state.get(position));
mViewHolder.rb_1.setChecked(rb_deck_1_state.get(position));
mViewHolder.rb_2.setChecked(rb_deck_2_state.get(position));
mViewHolder.rb_3.setChecked(rb_deck_3_state.get(position));
mViewHolder.cb_must_include.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
CheckBox cb = (CheckBox) v;
String checked = cb.isChecked() ? "yes" : "no";
cb_must_include_state.set(position,cb.isChecked());
Log.e("onclick must include","item i: " + position + " checked: " + checked);
}
});
mViewHolder.cb_include.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
CheckBox cb = (CheckBox) v;
String checked = cb.isChecked() ? "yes" : "no";
cb_include_state.set(position,cb.isChecked());
Log.e("onclick include","item i: " + position + " checked: " + checked);
}
});
mViewHolder.rb_1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
RadioButton rb = (RadioButton) v;
String checked = rb.isChecked() ? "yes" : "no";
rb_deck_1_state.set(position,rb.isChecked());
rb_deck_2_state.set(position,!rb.isChecked());
rb_deck_3_state.set(position,!rb.isChecked());
Log.e("onclick rb1","item i: " + position + " checked: " + checked);
}
});
mViewHolder.rb_2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
RadioButton rb = (RadioButton) v;
String checked = rb.isChecked() ? "yes" : "no";
rb_deck_1_state.set(position,!rb.isChecked());
rb_deck_2_state.set(position,rb.isChecked());
rb_deck_3_state.set(position,!rb.isChecked());
Log.e("onclick rb2","item i: " + position + " checked: " + checked);
}
});
mViewHolder.rb_3.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
RadioButton rb = (RadioButton) v;
String checked = rb.isChecked() ? "yes" : "no";
rb_deck_1_state.set(position,!rb.isChecked());
rb_deck_2_state.set(position,!rb.isChecked());
rb_deck_3_state.set(position,rb.isChecked());
Log.e("onclick rb3","item i: " + position + " checked: " + checked);
}
});
return convertView;
}
}