Search code examples
androidandroid-listviewandroid-arrayadapter

Make persistent changes to listview items based on user selections


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>

Visual example: layout rendered

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
}

Solution

  • 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;
    }
    }