前回の続き

複数選択はMultiSelectListPreferenceを使いますが、例えば、最低1個、最大3個までチェック可能という制御ができません。今回は、それが可能なカスタム複数選択を実装します。

サンプル: https://github.com/stack3/AndroidPreferenceSamples

前回同様サンプルを起動してCustomPreferenceを選択します。

お気に入りの色を選択すると複数選択リストのダイアログが表示されます。何もチェックしていない、もしくは、4個以上チェックするとOKが押せなくなります。

01

02

03

カスタムクラス用の属性

res/values/attrs.xmlで、entries(選択項目)、entryValues(選択項目値)、minChecked(最小チェック可能数)、maxChecked(最大チェック可能数)という属性を定義しています。

entries、entryValuesはandroidであらかじめ用意された属性を使うので、android:で始めています。ちなみに、これらはformatは指定する必要はありません。

<declare-styleable name="CustomMultiSelectListPreference">
  <attr name="android:entries" />
  <attr name="android:entryValues" />
  <attr name="minChecked" format="integer" />
  <attr name="maxChecked" format="integer" />
</declare-styleable>

設定画面のレイアウトXML

res/xml/custom_preference_screen_sample.xmlで以下のようにCustomMutilSelectListPreferenceを配置しています。

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res/net.stack3.preferencesamples">
〜〜 中略 〜〜
  <net.stack3.preferencesamples.custompreference.CustomEditTextPreference
    android:key="favorite_colors"
                android:title="@string/favorite_colors"
                android:summary="@string/choose_favorite_colors"
                android:entries="@array/color_names"
                android:entryValues="@array/color_values"
                android:defaultValue="@array/default_color_values"
                android:dialogTitle="@string/favorite_colors"
                android:dialogLayout="@layout/custom_multi_select_list_preference"
                app:minChecked="1"
                app:maxChecked="3" />

〜〜中略〜〜

</PreferenceScreen>

ダイアログのレイアウト

ListViewを表示するためのレイアウトを作成する必要があります。これはres/layout/custom_multi_select_list_preference.xmlで定義しています。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical" >

  <ListView
    android:id="@android:id/list"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:choiceMode="multipleChoice" />

</LinearLayout>

CustomMultiSelectListPreferenceクラス

DialogPreferenceを継承して、net.stack3.preferencesamples.custompreference.CustomMultiSelectListPreferenceクラスを作っています。

public class CustomMultiSelectListPreference extends DialogPreference {

MultiSelectListPreferenceではないことに注意してください。MultiSelectListPreferenceでは今回の実装が難しいため、DialogPreferenceからカスタマイズしています。

メンバ変数

// 複数選択入力用のListView
private ListView listView;
// 選択項目
private List<String> entries;
// 選択項目値
private List<String> entryValues;
// 現在の選択値
private Set<String> values;
// 最低チェック数
private int minChecked;
// 最大チェック数
private int maxChecked;

コンストラクタ

属性を読み出しメンバ変数へ設定します。

public CustomMultiSelectListPreference(Context context, AttributeSet attrs) {
    super(context, attrs);

    //
    // 各属性の読み出し
    //
    TypedArray a = context.obtainStyledAttributes(attrs,
            R.styleable.CustomMultiSelectListPreference, 0, 0);
    CharSequence[] rawEntries = a.getTextArray(R.styleable.CustomMultiSelectListPreference_android_entries);
    CharSequence[] rawEntryValues = a.getTextArray(R.styleable.CustomMultiSelectListPreference_android_entryValues);
    minChecked = a.getInt(R.styleable.CustomMultiSelectListPreference_minChecked, 0);
    maxChecked = a.getInt(R.styleable.CustomMultiSelectListPreference_maxChecked, Integer.MAX_VALUE);
    a.recycle();
    //
    // entriesとentryValuesの項目数が異なるならエラーとする
    //
    if (rawEntries.length != rawEntryValues.length) {
        throw new Error("entries and entryValues do not match item count.");
    }
    //
    // CharSequence[]のままだと扱いづらいので、Listに変換してメンバ変数へ代入
    //
    entries = new ArrayList();
    entryValues = new ArrayList();
    for (int i = 0; i < rawEntries.length; i++) {
        entries.add(rawEntries[i].toString());
        entryValues.add(rawEntryValues[i].toString());
    }
    // 現在の選択値を初期化
    values = new HashSet();
}

デフォルト値をvaluesに設定

前述のとおりcustom_preference_screen_sample.xmlでdefaultValueを設定しています。

android:defaultValue="@array/default_color_values"

arrays.xmlでdefault_color_valuesを定義しています。

<string-array name="default_color_values">
  <item>#ffffff</item>
  <item>#0000ff</item>
</string-array>

このデフォルト値を、以下のメソッドで取得してvaluesに設定します。

@Override
protected Object onGetDefaultValue(TypedArray a, int index) {
    CharSequence[] defalutValues = a.getTextArray(index);
    Set stringSet = new HashSet();
    if (defalutValues != null) {
        for (CharSequence defaultValue : defalutValues) {
            stringSet.add(defaultValue.toString());
        }
    }
    return stringSet;
}

前回設定した値をvaluesに設定

設定が保存されているときは以下のメソッドで読み出し、valuesに設定します。

@SuppressWarnings("unchecked")
@Override
protected void onSetInitialValue(boolean restorePersistedValue,
        Object defaultValue) {
    SharedPreferences prefs = getSharedPreferences();
    if (restorePersistedValue) {
        values = prefs.getStringSet(getKey(), new HashSet());
    } else {
        // このキャストでwarningが出るので、@SuppressWarningsを指定している
        values = (Set)defaultValue;
    }
}

今回はSet<String>を保存しているので、上記のようにSharedPreferenceを通じて設定を読み出していますが、intなどであれば単純に以下のメソッドを使って読み出せます。

// 引数の値はデフォルト値
value = getPersistedInt(0);
value = getPersistedFloat(0);
value = getPersistedBoolean(false);
value = getPersistedString("");

ListViewに選択項目を表示

以下のメソッドでListViewのAdapterを設定し選択項目が表示されるようにします。

@Override
protected void onBindDialogView(View view) {
    super.onBindDialogView(view);

    ArrayAdapter adapter = new ArrayAdapter(
                getContext(), 
                android.R.layout.simple_list_item_multiple_choice, 
                entries);

    listView = (ListView)view.findViewById(android.R.id.list);
    listView.setAdapter(adapter);
    listView.setOnItemClickListener(onItemClickListener);
}

そして、表示された時に現在の選択値(values)とそれに準じたOKボタンの状態を以下のメソッドで設定します。

@Override
protected void showDialog(Bundle state) {
    super.showDialog(state);

    setupListViewItemChecked();
    updateOKButton();
}

以下はprivateメソッド。

private void setupListViewItemChecked() {
    for (int i = 0; i < entryValues.size(); i++) {
        String entryValue = entryValues.get(i);
        boolean checked = values.contains(entryValue);
        listView.setItemChecked(i, checked);
    }
}
private void updateOKButton() {
    Dialog dialog = getDialog();

    if (dialog != null && listView != null) {
        int numChecked = listView.getCheckedItemCount();
        Button okButton = (Button)dialog.findViewById(android.R.id.button1);
        okButton.setEnabled(minChecked <= numChecked && numChecked <= maxChecked);
    }
}

項目を選択した時のListener

項目を選択・選択解除したときvaluesの値も更新します。

private OnItemClickListener onItemClickListener = new OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> adapter, View view, int position, long id) {
        values = getStringSetOfCheckedItems();
        updateOKButton();
    }
};

以下はprivateメソッド

private Set getStringSetOfCheckedItems() {
    HashSet stringSet = new HashSet();
    SparseBooleanArray checkedPositions = listView.getCheckedItemPositions();
    for (int i = 0; i < entryValues.size(); i++) {
        String entryValue = entryValues.get(i);
         if (checkedPositions.get(i)) {
            stringSet.add(entryValue);
        }
    }
    return stringSet;
}

OKを押した時の設定への保存

ダイアログが閉じられた時、以下のメソッドが呼ばれます。positiveResultがtrueのときOKを押したということなので、設定へ保存します。

@Override
protected void onDialogClosed(boolean positiveResult) {
    super.onDialogClosed(positiveResult);
        
    if (positiveResult) {
        SharedPreferences.Editor editor = getSharedPreferences().edit();
        editor.putStringSet(getKey(), getStringSetOfCheckedItems());
        editor.commit();
    }
}

保存する値が、intなどの時は単純に以下のメソッドで保存出来ます。

persistInt(value);
persistFloat(value);
persistBoolean(value);
persistString(value);

Stateの保存と復帰

最後に画面回転する、アプリがバックグラウンドのときにシステムがAcitivityを破棄するなどの状況が起きた時のための実装です。

ただ、これはPreferenceのXMLの設定で、android:persistent="false"にしない限りは不要な実装かもしれません。

とりあえず、公式のドキュメントに従って、このように実装しました。

    protected Parcelable onSaveInstanceState() {
        final Parcelable superState = super.onSaveInstanceState();
        if (isPersistent()) {
            return superState;
        }

        final SavedState myState = new SavedState(superState);
        myState.values = values;
        return myState;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (state == null || !state.getClass().equals(SavedState.class)) {
            super.onRestoreInstanceState(state);
            return;
        }

        SavedState myState = (SavedState)state;
        super.onRestoreInstanceState(myState.getSuperState());
        
        values = myState.values;
    };
    
    private static class SavedState extends BaseSavedState {
        private Set<String> values;
        
        public SavedState(Parcelable superState) {
            super(superState);
        }

        public SavedState(Parcel source) {
            super(source);

            List<?> listValue = source.readArrayList(null);
            values = new HashSet<String>();
            for (Object object : listValue) {
                if (object instanceof String) {
                    values.add((String)object);
                }
            }
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            
            List<String> listValue = new ArrayList<String>();
            for (String value : values) {
                listValue.add(value);
            }
            dest.writeList(listValue);
        }

        @SuppressWarnings("unused")
        public static final Parcelable.Creator<SavedState> CREATOR =
                new Parcelable.Creator<SavedState>() {

            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }