سؤال

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:

enter image description here enter image description here

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.

هل كانت مفيدة؟

المحلول 3

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.

نصائح أخرى

Inside your ListAdapter(), you can Store a customize Class which store Name and if item is selected or Not.

public class ListAdapter extends BaseAdapter {
    private static LayoutInflater layoutInflater = null;
    private ArrayList<Object E> data; // Here E will your customize Class.
    ...
}

I not sure what's your aim.

One instance of ListAdapter:

You will agree that there will not be any problem in this with my above mention answer and one small change in getView as give below:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
      if(data.selected) {
             // Make item selected.
       }
}

Multiple instance of ListAdapter:

Should access them with different pointer.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top