Spinnerの開閉タイミングをコントロールする

## はじめに

最近、案件で`Android`の`Spinner`クラスについてゴリゴリと実装をし、ある程度知見が溜まったのでメモ書きも含めてやりたいこととその実装例を画像＋テキストベースで説明をしてみたいと思います。
（2019/02/25時点では開閉タイミングのみ）

## TL;DR

- Spinnerの開閉タイミングは`Spinner`クラスをラップする必要がある。標準では不可能
- Spinnerは思うようにコントロールしづらいので使用する際はUIなど細かい仕様にならないように調整する必要がある

### `Spinner`の開閉タイミングをハンドリングする

開閉タイミングをコントロールするには標準の`Spinner`では実現できません。`Spinner`クラスをラップ（厳密には`AppCompatSpinner`クラス）し、以下のソースコードでオリジナルの`Spinner`クラスを生成する必要があります。

使用タイミングとしては、閉じたときだけ（開いたときだけ）文字のサイズを変更したいやViewの画像部分などを非表示にしたいなどに対応できます。

<details><summary>ソースコード</summary><div>

``` Java
public class CustomSpinner extends android.support.v7.widget.AppCompatSpinner {
    public CustomSpinner(Context context) {
        super(context);
    }

    public CustomSpinner(Context context, int mode) {
        super(context, mode);
    }

    public CustomSpinner(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomSpinner(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public CustomSpinner(Context context, AttributeSet attrs, int defStyleAttr, int mode) {
        super(context, attrs, defStyleAttr, mode);
    }

    public CustomSpinner(Context context, AttributeSet attrs, int defStyleAttr, int mode, Resources.Theme popupTheme) {
        super(context, attrs, defStyleAttr, mode, popupTheme);
    }

    /**
     * An interface which a client of this Spinner could use to receive
     * open/closed events for this Spinner.
     */
    public interface OnSpinnerEventsListener {

        /**
         * Callback triggered when the spinner was opened.
         */
        void onSpinnerOpened(Spinner spinner);

        /**
         * Callback triggered when the spinner was closed.
         */
        void onSpinnerClosed(Spinner spinner);

    }

    private OnSpinnerEventsListener mListener;
    private boolean mOpenInitiated = false;

    // implement the Spinner constructors that you need

    @Override
    public boolean performClick() {
        // register that the Spinner was opened so we have a status
        // indicator for when the container holding this Spinner may lose focus
        mOpenInitiated = true;
        if (mListener != null) {
            mListener.onSpinnerOpened(this);
        }
        return super.performClick();
    }

    @Override
    public void onWindowFocusChanged (boolean hasFocus) {
        if (hasBeenOpened() && hasFocus) {
            performClosedEvent();
        }
    }

    /**
     * Register the listener which will listen for events.
     */
    public void setSpinnerEventsListener(OnSpinnerEventsListener onSpinnerEventsListener) {
        mListener = onSpinnerEventsListener;
    }

    /**
     * Propagate the closed Spinner event to the listener from outside if needed.
     */
    public void performClosedEvent() {
        mOpenInitiated = false;
        if (mListener != null) {
            mListener.onSpinnerClosed(this);
        }
    }

    /**
     * A boolean flag indicating that the Spinner triggered an open event.
     *
     * @return true for opened Spinner
     */
    public boolean hasBeenOpened() {
        return mOpenInitiated;
    }
}
```

</div></details>

<details><summary>使い方</summary><div>

``` java
CustomSpinner customSpinner = (CustomSpinner) findViewById(R.id.custom_spinner);
// adapterの設定は省略しています
customSpinner.setSpinnerEventsListener(new CustomSpinner.OnSpinnerEventsListener() {
    @Override
    public void onSpinnerOpened(Spinner spinner) {
        Log.v("opened!!");
    }

    @Override
    public void onSpinnerClosed(Spinner spinner) {
        Log.v("closed!!");
    }
});
```
</div></details>

注意点としては、`AppCompatSpinner`クラスをラップしているので標準の`Spinner`クラスのようなUIにはなりません。レイアウトに` android:dropDownVerticalOffset="30dp" `を追加するか、`customSpinner.setDropDownVerticalOffset(30)`を設定してドロップダウンの場所を調整する必要があります。

![スクリーンショット 2019-02-25 17.27.16.png](https://qiita-image-store.s3.amazonaws.com/0/134845/d1c2df1a-1428-5e3c-12f6-c83bfd63152a.png)