MultiSelectListPreference dla Android w wersji API wcześniejszej niż 11

MultiSelectListPreference to ustawienie dodane w wersji API 11. Pozwala na zapis i odczyt wielu możliwych opcji np. chcemy wybrać w które dni tygodnia nasza aplikacja ma być aktywna. W poprzednich wersjach API nie ma możliwości zapisu i odczytu tablic danych. Poniżej rozwiązanie które to zapewnia.

Oto jak prezentuje się ten widget:

Wystarczy dodać poniższą klasę do projektu:

package pl.wavesoftware.widget;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import android.app.AlertDialog.Builder;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnMultiChoiceClickListener;
import android.content.res.TypedArray;
import android.preference.ListPreference;
import android.util.AttributeSet;

public class MultiSelectListPreference extends ListPreference {

    private String separator;
    private static final String DEFAULT_SEPARATOR = "\u0001\u0007\u001D\u0007\u0001";
    private boolean[] entryChecked;

    public MultiSelectListPreference(Context context, AttributeSet attributeSet) {
        super(context, attributeSet);
        entryChecked = new boolean[getEntries().length];
        separator = DEFAULT_SEPARATOR;
    }

    public MultiSelectListPreference(Context context) {
        this(context, null);
    }

    @Override
    protected void onPrepareDialogBuilder(Builder builder) {
        CharSequence[] entries = getEntries();
        CharSequence[] entryValues = getEntryValues();
        if (entries == null || entryValues == null
                || entries.length != entryValues.length) {
            throw new IllegalStateException(
                    "MultiSelectListPreference requires an entries array and an entryValues "
                            + "array which are both the same length");
        }

        restoreCheckedEntries();
        OnMultiChoiceClickListener listener = new DialogInterface.OnMultiChoiceClickListener() {
            public void onClick(DialogInterface dialog, int which, boolean val) {
                entryChecked[which] = val;
            }
        };
        builder.setMultiChoiceItems(entries, entryChecked, listener);
    }

    private CharSequence[] unpack(CharSequence val) {
        if (val == null || "".equals(val)) {
            return new CharSequence[0];
        } else {
            return ((String) val).split(separator);
        }
    }

    /**
     * Gets the entries values that are selected
     * 
     * @return the selected entries values
     */
    public CharSequence[] getCheckedValues() {
        return unpack(getValue());
    }

    private void restoreCheckedEntries() {
        CharSequence[] entryValues = getEntryValues();

        // Explode the string read in sharedpreferences
        CharSequence[] vals = unpack(getValue());

        if (vals != null) {
            List<CharSequence> valuesList = Arrays.asList(vals);
            for (int i = 0; i < entryValues.length; i++) {
                CharSequence entry = entryValues[i];
                entryChecked[i] = valuesList.contains(entry);
            }
        }
    }

    @Override
    protected void onDialogClosed(boolean positiveResult) {
        List<CharSequence> values = new ArrayList<CharSequence>();

        CharSequence[] entryValues = getEntryValues();
        if (positiveResult && entryValues != null) {
            for (int i = 0; i < entryValues.length; i++) {
                if (entryChecked[i] == true) {
                    String val = (String) entryValues[i];
                    values.add(val);
                }
            }

            String value = join(values, separator);
            setSummary(prepareSummary(values));
            setValueAndEvent(value);
        }
    }

    private void setValueAndEvent(String value) {
        if (callChangeListener(unpack(value))) {
            setValue(value);
        }
    }

    private CharSequence prepareSummary(List<CharSequence> joined) {
        List<String> titles = new ArrayList<String>();
        CharSequence[] entryTitle = getEntries();
        CharSequence[] entryValues = getEntryValues();
        int ix = 0;
        for (CharSequence value : entryValues) {
            if (joined.contains(value)) {
                titles.add((String) entryTitle[ix]);
            }
            ix += 1;
        }
        return join(titles, ", ");
    }

    @Override
    protected Object onGetDefaultValue(TypedArray typedArray, int index) {
        return typedArray.getTextArray(index);
    }

    @Override
    protected void onSetInitialValue(boolean restoreValue,
            Object rawDefaultValue) {
        String value = null;
        CharSequence[] defaultValue;
        if (rawDefaultValue == null) {
            defaultValue = new CharSequence[0];
        } else {
            defaultValue = (CharSequence[]) rawDefaultValue;
        }
        List<CharSequence> joined = Arrays.asList(defaultValue);
        String joinedDefaultValue = join(joined, separator);
        if (restoreValue) {
            value = getPersistedString(joinedDefaultValue);
        } else {
            value = joinedDefaultValue;
        }

        setSummary(prepareSummary(Arrays.asList(unpack(value))));
        setValueAndEvent(value);
    }

    /**
     * Joins array of object to single string by separator
     * 
     * Credits to kurellajunior on this post
     * http://snippets.dzone.com/posts/show/91
     * 
     * @param iterable
     *            any kind of iterable ex.: <code>["a", "b", "c"]</code>
     * @param separator
     *            separetes entries ex.: <code>","</code>
     * @return joined string ex.: <code>"a,b,c"</code>
     */
    protected static String join(Iterable<?> iterable, String separator) {
        Iterator<?> oIter;
        if (iterable == null || (!(oIter = iterable.iterator()).hasNext()))
            return "";
        StringBuilder oBuilder = new StringBuilder(String.valueOf(oIter.next()));
        while (oIter.hasNext())
            oBuilder.append(separator).append(oIter.next());
        return oBuilder.toString();
    }

}

i dodać konfigurację pola w standardowy sposób (z wykorzystaniem tablic danych):

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
    <pl.wavesoftware.widget.MultiSelectListPreference
        android:defaultValue="@array/pref_autoon_defaults"
        android:entries="@array/pref_autoon_titles"
        android:entryValues="@array/pref_autoon_values"
        android:key="autoon"
        android:persistent="true"
        android:title="@string/pref_autoon_events_title" />
</PreferenceScreen>

a oto przykładowe dane:

<resources>
    <string-array name="pref_autoon_values">
        <item>1</item>
        <item>2</item>
        <item>3</item>
        <item>4</item>
    </string-array>
    <string-array name="pref_autoon_titles">
        <item>If the phone rings</item>
        <item>While traveling</item>
        <item>While charging</item>
        <item>Periodically checks</item>
    </string-array>
    <string-array name="pref_autoon_defaults">
        <item>2</item>
        <item>3</item>
    </string-array>
</resources>

Gotowe. Zmiany w polu zapisują się oczywiście automatycznie. Jeżeli chcemy obsłużyć dodatkowo event zmiany danych, mamy do dyspozycji prosty interfejs z listą aktualnie zaznaczonych pozycji np.:

private static OnPreferenceChangeListener autoOnChangeListener = new OnPreferenceChangeListener() {

	@Override
	public boolean onPreferenceChange(Preference rawPreference, Object newValue) {
            List<CharSequence> selected = Arrays.asList((CharSequence[]) newValue);
            if (selected.contains("1")) {
                // do some work
            }
            return true;
	}
};

 

Adres do GIST: https://gist.github.com/cardil/4754571