Qiitaビューア制作中にSpinnerで困ったこと

# はじめに
前回の記事では[QiitaビューアアプリQitches](https://play.google.com/store/apps/details?id=com.airdsoft.qitches)制作中に困った事の1つとしてWebViewに触れましたが、
今回はもう1つの困ったことであるSpinnerに触れておこうと思います。
今更超基本的なコントロールであるSpinnerについて書くのもあれなのですが、
使ってみると痒いところに手が届かない感が凄かったので。

## 今回実現したかった仕様
以下の通りです。

* ユーザーが何も選択していない時は「タグを選択してください」メッセージを表示しておく(画像左)
* Spinnerをタップするとプルダウンにタグ一覧を表示する(画像中央)
* タグ一覧は固定ではなく非同期でREST APIを実行して取得した物を表示する
* ユーザーがいずれかのタグを選択したら「タグを選択してください」がタグ名に置き換わる(画像右)

![Spinner.png](https://qiita-image-store.s3.amazonaws.com/0/99570/8ef6def8-6c49-b37e-6696-67e4f8eafbe6.png)


これだけなので楽勝だと思っていたのですが、意外と大変でした。

## 問題点：未選択時のメッセージを設定する機能が無い！
そうなんです。そもそも未選択時のメッセージを設定する機能がありません。
デフォルトではSpinnerのプルダウンの一番上のアイテムが表示されてしまいます。
以下のような流れで対応を入れていきます。

* adapterが持つリストの最終要素に未選択時用の文字列を追加する
* 未選択時の文字列がプルダウンに表示されるのを防ぐ
* 未選択時用の要素を選択してメッセージを表示する

### adapterが持つリストの最終要素に未選択時用の文字列を追加する

Spinnerには、Spinnerに設定したadapterのリストに含まれている要素しか表示できないため、
「タグを選択してください」をadapterのリストに追加してあげる必要があります。
以下のようにadapterが持つリストの最終要素に未選択時用の文字列を追加します。


```MainActivity.java
private void setupSpinner(List<String>tagIconList, List<String> tagList) {

    // ★まずはタグ一覧をリストに追加
    List<TagSpinnerAdapter.SpinnerRowData> list = new ArrayList<>();
    if (tagIconList != null) {
        for (int i = 0; i < tagList.size(); i++) {
            list.add(new TagSpinnerAdapter.SpinnerRowData(tagIconList.get(i), tagList.get(i)));
            }
        }

    // ★「タグを選択してください」を末尾に追加
    list.add(new TagSpinnerAdapter.SpinnerRowData(null, getString(R.string.message_tag_select)));

    // ★adapterにリストを設定
    mTagSpinnerAdapter = new TagSpinnerAdapter(this, R.layout.spinner_item, list);
    mTagSpinnerAdapter.setDropDownViewResource(R.layout.spinner_dropdown_item);
    // ★Spinnerにadapterを設定
    mSpinner.setAdapter(mTagSpinnerAdapter);

    // 以下略
}
```

```TagSpinnerAdapter.java
public class TagSpinnerAdapter extends ArrayAdapter<TagSpinnerAdapter.SpinnerRowData> {
    private List<SpinnerRowData> mRowDataList;

    public TagSpinnerAdapter(Context context, int textViewResourceId, List<SpinnerRowData> rowDataList) {
        super(context, textViewResourceId, rowDataList);
        // ★「タグを選択してください」も含むリストをadapterで保持
        mRowDataList = rowDataList;
    }
    // 以下略
}
```


### 未選択時の文字列がプルダウンに表示されるのを防ぐ

SpinnerはセットされたadapterのgetCountで取得した要素数分だけプルダウンで表示します。
つまり、素直に要素数を返すとリストの末尾に追加した未選択時用の文字列もプルダウンに表示されます。
それを防ぐために、未選択時の文字列を追加したらadapterのgetCountが要素数-1を返すようにします。
以下のソースでは、要素数-1を返すモードに設定する`setDummyCountMode`を追加してフラグを立てています。
また、フラグが立っていたら要素数-1を返すようにgetCountメソッドをOverrideします。

```TagSpinnerAdapter.java
@Override
public int getCount() {

    int count = super.getCount();
    if (mIsEnableDummyCount) {
        // ★フラグが立っていたら要素数-1を返す
        return count > 0 ? count - 1 : count;
    } else {
        return count;
    }
}

// ★要素数-1を返すモードにセットする
public void setDummyCountMode(boolean isEnable) {
    mIsEnableDummyCount = isEnable;
}
```

### 未選択時用の要素を選択してメッセージを表示する

Spinnerの`setSelection`をコードから呼び出して最終要素を選択します。
これで、「タグを選択してください」メッセージを表示することができます。その後、
要素数-1を返すモードに変更してプルダウンに「タグを選択してください」が表示されないようにします。
以下コードにはちょっと余計なコードも入ってしまっていますが重要なところはコメントを振ってあります。

```MainActivity.java
private void setupSpinner(List<String>tagIconList, List<String> tagList) {

    // 省略
    
    if (mTagManager.getCurrentTagPosition() < 0) {
        int lastItem = list.size();
        // ★ユーザーがSpinnerから何も選択していなければ未選択時用の要素を選択する
        mSpinner.setSelection(lastItem - 1);
    } else {
        mSpinner.setSelection(mTagManager.getCurrentTagPosition());
    }

    mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
        @Override
        public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
            // ★Activity起動時に選択イベントが飛んできてしまうのを無視するための処理
            if (!mIsSpinnerInitialized) {
                mIsSpinnerInitialized = true;
                return;
            }

            if (mTagManager.getCurrentTagPosition() != i) {
                if (mTagSpinnerAdapter.getCount() == i) {
                    return;
                }
                String tag = mTagSpinnerAdapter.getSelectedTag(i);
                mTagManager.setCurrentTag(tag);
                mPagerAdapter.startTagSearch(tag);
                mTagManager.setCurrentTagPosition(i);
            }
        }

        @Override
        public void onNothingSelected(AdapterView<?> adapterView) {

        }
    });
    // ★要素数-1を返すモードに変更する。先に未選択時要の要素を選択して表示を切り替えた後
    // にモードを切り替えないとIndexOutOfBounds的な例外が起きるため順番に注意する。
    mTagSpinnerAdapter.setDummyCountMode(true);
}
```

# おわりに
以上で意図した仕様で実装することができました。
それにしてもSpinner、割とメジャーなUIパーツなのにいろいろとイケてなさすぎて辛いです。
使うんじゃなかったなと少し後悔しています。