I am trying to implement a multiple-choice list based on custom checkable lists. It is rather simple when just one list is needed, but gets complicated when you should be able to switch between multiple lists.
In my case, an activity has many buttons on top that represent categories. Just below them there is a list that contains items in a currently chosen category.
Items' list view is based on a ListAdapter
that extends BaseAdapter
:
public class ListAdapter extends BaseAdapter {
private static LayoutInflater layoutInflater = null;
private ArrayList<String> data;
public ListAdapter(Activity activity, ArrayList<String> data) {
layoutInflater = (LayoutInflater) activity
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
this.data = data;
}
public void setData(ArrayList<String> data) {
this.data = data;
}
@Override
public int getCount() {
return data.size();
}
@Override
public Object getItem(int position) {
return data.get(position);
}
@Override
public long getItemId(int position) {
return Long.valueOf(position);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
CheckableRelativeLayout view = (CheckableRelativeLayout) convertView;
if (view == null) {
view = (CheckableRelativeLayout) layoutInflater.inflate(
R.layout.checkable_relative_layout, null);
}
TextView number = (TextView) view.findViewById(R.id.number);
TextView name = (TextView) view.findViewById(R.id.name);
number.setText(Integer.toString(position));
name.setText(data.get(position));
return view;
}
}
Each item's view is an inflated CheckableRelativeLayout
:
public class CheckableRelativeLayout extends RelativeLayout implements
Checkable {
private static final int[] CheckedStateSet = { android.R.attr.state_checked };
private boolean isChecked = false;
public CheckableRelativeLayout(Context context) {
super(context);
}
public CheckableRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CheckableRelativeLayout(Context context, AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean isChecked() {
return isChecked;
}
@Override
public void setChecked(boolean checked) {
isChecked = checked;
refreshDrawableState();
}
@Override
public void toggle() {
isChecked = !isChecked;
refreshDrawableState();
}
@Override
protected int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (isChecked()) {
mergeDrawableStates(drawableState, CheckedStateSet);
}
return drawableState;
}
}
Selection representation in UI is specified within a selection.xml
:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/gradient_bg_checked" android:state_checked="true"/>
<item android:drawable="@drawable/gradient_bg"/>
</selector>
And the main activity goes as follows:
public class MultipleCheckList extends Activity {
private ListView listView;
private ListAdapter adapter;
private SparseArray<ArrayList<String>> data = new SparseArray<ArrayList<String>>();
private class ButtonClickListner implements OnClickListener {
@Override
public void onClick(View buttonView) {
Integer tag = (Integer) buttonView.getTag();
adapter.setData(data.get(tag));
adapter.notifyDataSetChanged();
listView.refreshDrawableState();
// How to save user's selections?
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_multiple_check);
Button button1 = (Button) findViewById(R.id.button_1);
Button button2 = (Button) findViewById(R.id.button_2);
button1.setTag(1);
button2.setTag(2);
button1.setOnClickListener(new ButtonClickListner());
button2.setOnClickListener(new ButtonClickListner());
button1.setText("First category");
button2.setText("Second category");
ArrayList<String> data1 = new ArrayList<String>();
data1.add("John");
data1.add("Bob");
data1.add("Ted");
ArrayList<String> data2 = new ArrayList<String>();
data2.add("Summer");
data2.add("Winter");
data2.add("Spring");
data.append(1, data1);
data.append(2, data2);
adapter = new ListAdapter(this, data1);
listView = (ListView) findViewById(R.id.list);
listView.setAdapter(adapter);
listView.setItemsCanFocus(false);
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
}
}
My question is, what would be the best way to save user's selections?
I've tried to store a ListAdapter
instance for each category in a HashMap
and reuse it by invoking setAdapter()
for each onClick()
in ButtonClickListner
. Not only it doesn't seem to work (selections' state are not restored), but also it is not quite efficient due to unnecessary garbage collection.
Then I've tried to use setData()
, notifyDataSetChanged()
and refreshDrawableState()
invocations (as in the code above) and combine them with some HashMap
for selection storage. The problem is, even if refreshDrawableState()
is being invoked, all selections are shared among categories:
I've also looked to Parcelable
, but that seems to be useful for cross-activity resources exchange only. Maybe inflating each list separately could be seen as a good solution? But what about efficiency then? setData()
approach would possibly be much more economical.
Thank you for your help.
I've finally solved my problem.
First of all, I've added new methods to the ListAdapter
class. restoreCheckStatesIfNeeded()
in particular:
private SparseArray<SparseBooleanArray> setsCheckStates = new SparseArray<SparseBooleanArray>();
private void restoreCheckStatesIfNeeded(ListView listView, int position) {
if (getCheckStates() == null) {
setsCheckStates.put(currentCollection, new SparseBooleanArray());
}
if (stillSwitchingCollection) {
Boolean itemLatestState = getCheckStates().get(position, false);
listView.setItemChecked(position, itemLatestState);
}
if (position == getCount() - 1) {
stillSwitchingCollection = false;
}
}
public void setCurrentCollection(int currentCollection) {
this.currentCollection = currentCollection;
stillSwitchingCollection = true;
}
public SparseBooleanArray getCheckStates() {
return setsCheckStates.get(currentCollection);
}
stillSwitchingCollection
field denotes whether we just have changed a collection and its list's items are still being drawn. It helps us to tell if we need to restore previous list's item check state or not.
Also, it is important to notice that we cannot save check states of our list's items within a getView()
, otherwise, many strange things will happen (e.g. some items would be selected even if we haven't clicked on them etc.). It is because after a getView()
call there is still some ongoing selection setup in ListView.setupChild()
.
Because of that, a better option would be to save check states within ButtonClickListner.onClick()
:
for (int i = 0; i < listView.getChildCount(); i++) {
CheckableRelativeLayout listItem = (CheckableRelativeLayout) listView.getChildAt(i);
adapter.getCheckStates().put(i, listItem.isChecked());
}
Fortunately, such an approach solves the problem.