or am-shalem or am-shalem - 4 months ago 39
Android Question

handle list view with check box and search implemention

i have a list view which has check box on each row and i`ve implemented a serch view to search in the list view. the problem is that when i search the list view and select item at postion X from the modifided list, delete the text from the search view i see the original list with the item checked in position X.

Example - list of a,b,c....z .

i can see 5 items on the list:

A
B
C
D
E

then search for z so i see only Z and check it. then i delete Z and see A-E again and A is checked.

this is my main:

public class AddRoomatesScreen extends Activity implements AdapterView.OnItemClickListener,SearchView.OnQueryTextListener {
SharedPreferences preferences;
List<String> nameList = new ArrayList<String>();
List<String> phoneList = new ArrayList<String>();
MyAdapter adapter;
Button btnSelect;
String apartmentnumber;
SearchView searchView;
ArrayList<Contact> contactList = new ArrayList<>();

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.add_roomate);
preferences = getSharedPreferences("appData", 0);
int apartmentNumber = preferences.getInt("apartmentNumber", 0);
apartmentnumber = Integer.toString(apartmentNumber);
getAllContacts(this.getContentResolver());
ListView lv = (ListView) findViewById(R.id.lv);
adapter = new MyAdapter(nameList,contactList);
lv.setAdapter(adapter);
lv.setOnItemClickListener(this);
lv.setItemsCanFocus(false);
lv.setTextFilterEnabled(true);
searchView = (SearchView) findViewById(R.id.search_view);
searchView.setOnQueryTextListener(this);
// adding
btnSelect = (Button) findViewById(R.id.addSelectedContacts);
btnSelect.setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View v) {
for(int i = 0; i<adapter.mCheckStates.size(); i++){
Contact contact = (adapter.contactsListCopy.get(adapter.mCheckStates.keyAt(i)));
new addRoommate().execute(apartmentnumber, contact.getPhoneNumber());
}
preferences = getSharedPreferences("appData", 0);
SharedPreferences.Editor editor = preferences.edit();
editor.putBoolean("roomatesLoadedFromDB", false);
editor.apply();
}
});


}

@Override
public boolean onQueryTextSubmit(String query) {
return false;
}

@Override
public boolean onQueryTextChange(String newText) {
adapter.getFilter().filter(newText);
return false;
}

@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
// TODO Auto-generated method stub
adapter.toggle(arg2);
}

public void getAllContacts(ContentResolver cr) {

Cursor phones = cr.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null);
while (phones.moveToNext()) {
String name = phones.getString(phones.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
String phoneNumber = phones.getString(phones.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
nameList.add(name);
phoneList.add(phoneNumber);
contactList.add(new Contact(name,phoneNumber));
}

phones.close();
}


this is my adapter:

class MyAdapter extends BaseAdapter implements CompoundButton.OnCheckedChangeListener,Filterable {
private SparseBooleanArray mCheckStates;
LayoutInflater mInflater;
TextView tv;
CheckBox cb;
ValueFilter filter;
ArrayList<Contact> contactsList;
ArrayList<Contact> contactsListCopy;

MyAdapter(List<String> nameList , ArrayList<Contact> contactsList) {
mCheckStates = new SparseBooleanArray(contactsList.size());
mInflater = (LayoutInflater) AddRoomatesScreen.this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
this.contactsList = contactsList;
this.contactsListCopy = this.contactsList;
getFilter();
}

@Override
public int getCount() {
return contactsListCopy.size();
}

@Override
public Object getItem(int position) {
return contactsListCopy.get(position);
}

@Override
public long getItemId(int position) {
return 0;
}

@Override
public View getView(final int position, View convertView, ViewGroup parent) {
// TODO Auto-generated method stub
View vi = convertView;
if (convertView == null)
vi = mInflater.inflate(R.layout.contact_list_item, null);
TextView tv = (TextView) vi.findViewById(R.id.textView3);

cb = (CheckBox) vi.findViewById(R.id.checkBox);
Contact contact = contactsListCopy.get(position);
tv.setText(contact.getName());
cb.setTag(position);
cb.setChecked(mCheckStates.get(position, false));
cb.setOnCheckedChangeListener(this);

return vi;
}

public boolean isChecked(int position) {
return mCheckStates.get(position, false);
}

public void setChecked(int position, boolean isChecked) {
mCheckStates.put(position, isChecked);
notifyDataSetChanged();
}

public void toggle(int position) {
setChecked(position, !isChecked(position));
}

@Override
public void onCheckedChanged(CompoundButton buttonView,
boolean isChecked) {

mCheckStates.put((Integer) buttonView.getTag(), isChecked);
}

@Override
public Filter getFilter() {
if (filter == null){
filter = new ValueFilter();
}
return filter;

}

private class ValueFilter extends Filter {


@Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults results = new FilterResults();

if (constraint != null && constraint.length() > 0) {
ArrayList<Contact> temp = new ArrayList<Contact>();
for (int i = 0; i < contactList.size(); i++) {
Contact c = contactList.get(i);
if ((c.getName().toString())
.contains(constraint.toString())) {
temp.add(c);
}
}
results.count = temp.size();
results.values = temp;
} else {
results.count = contactList.size();
results.values = contactList;
}
return results;

}

@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
contactsListCopy = (ArrayList<Contact>) results.values;
notifyDataSetChanged();

}
}


}


this is my model:

public class Contact{
private String phoneNumber;
private String name;

public Contact(String name , String phoneNumber){
this.phoneNumber = phoneNumber;
this.name = name;
}

public String getName() {
return name;
}
public String getPhoneNumber() {
return phoneNumber;
}

}

Answer

EDIT:

You can use the ID from the cursor as your unique id.

This code would go with the code originally posted below.

Add id to your Contact class:

public class Contact{
    private long id;
    private String phoneNumber;
    private String name;

    public Contact(long id, String name , String phoneNumber) {
        this.id = id;
        this.phoneNumber = phoneNumber;
        this.name = name;
    }

    public long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }

}

This is how you get the ID from the cursor:

public void getAllContacts(ContentResolver cr) {

    Cursor phones = cr.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null);
    while (phones.moveToNext()) {
        long id = phones.getLong(phones.getColumnIndex(ContactsContract.Data._ID));
        String name = phones.getString(phones.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
        String phoneNumber = phones.getString(phones.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
        nameList.add(name);
        phoneList.add(phoneNumber);
        contactList.add(new Contact(id, name, phoneNumber));
    }

    phones.close();
}

Your mCheckStates in the adapter changes to:

    private LongSparseArray<Boolean> mCheckStates;

Some of the original answer below has been changed to match the long type used for id.


The problem is that your mCheckStates boolean map isn't kept in sync with contactsListCopy as it is filtered.

  • You filtered on Z so Z is now item 0,
  • You checked Z which sets position 0 as checked,
  • You redisplay the whole list. Now A is position 0 so it shows as checked.

Instead of tracking checked positions, you need to track checked contacts. To do that, you need to use the unique ID from the contact record.

Here's the changes you need to make:

Make your adapter use the unique id:

    @Override
    public long getItemId(int position) {
        return contactsListCopy.get(position).getId(); 
    }

Now your activity has to use the unique id instead of position:

    @Override
    public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
        adapter.toggle(arg3);
    }

In your adapter getView(), you have to use the ID for the check box now

    cb.setTag(contact.getId());
    cb.setChecked(mCheckStates.get(contact.getId(), false));

I would change some other methods just to avoid confusion:

    public boolean isChecked(long id) {
        return mCheckStates.get(id, false);
    }

    public void setChecked(long id, boolean isChecked) {
        mCheckStates.put(id, isChecked);
        notifyDataSetChanged();
    }

    public void toggle(long id) {
        setChecked(id, !isChecked(id));
    }