[Android] Spinnerの実装あれこれ

# 概要
こんな感じのSpinnerのデザインを実装したい。
![Screenshot_20240923_221321.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3316511/7a419c97-953b-5243-e716-496e9b66d4fe.png)

# 0. (参考)標準Spinnerのデザイン実装方法
発展例を紹介する前に標準的なUIについて確認していきます。
標準SpinnerのUIは以下のような感じになっています。
![Screenshot_20240923_220127.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3316511/dbbbd26f-e527-8c73-e408-cbcfa9595333.png)

①xmlファイルにSpinnerを追加する。
```
<Spinner
    android:id="@+id/sp_default"
    android:layout_width="200dp"
    android:layout_height="50dp"/>
```
②DropDownリストのUIを設定する。(ImageViewを追加したりカスタマイズ可能)
```spinner_dropdown_item.xml
<?xml version="1.0" encoding="utf-8"?>
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="50dp"★xmlのheightに合わせる方が見栄えが良い
    android:paddingStart="8dp"
    android:paddingEnd="8dp"
    android:gravity="center_vertical"
    android:singleLine="true"
    android:ellipsize="marquee"/>
```
③Spinnerの中身を設定する。
```
        val defaultSpinner:Spinner = binding.spDefault
        //or val defaultSpinner:Spinner = view.findViewById(R.id.sp_default)
        
        //表示するアイテム
        val defaultItems = mutableListOf("赤色","青色","黄色")
        val defaultArrayAdapter = ArrayAdapter(requireContext(),
            R.layout.spinner_dropdown_item, defaultItems)
        defaultSpinner.adapter = defaultArrayAdapter
```
※参考 SpinnerのBackgroundには以下が設定されているようです。（使い勝手が悪い・・・。）
```
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
            android:paddingMode="stack"
            android:paddingStart="0dp"
            android:paddingEnd="48dp"★右側に▼のサイズのpaddingを設定
            android:paddingLeft="0dp"
            android:paddingRight="0dp">
    <item★タップエフェクト
        android:gravity="end|fill_vertical"
        android:width="48dp"
        android:drawable="@drawable/control_background_40dp_material" />

    <item★「▼」のマーク
        android:drawable="@drawable/ic_spinner_caret"
        android:gravity="end|center_vertical"
        android:width="24dp"
        android:height="24dp"
        android:end="12dp" />
</layer-list>
```
https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/res/res/drawable/spinner_background_material.xml

# 1. SpinnerのUIを実装する。
ボタンに▼マークが内包されるようなUIを実装していきます。
![Screenshot_20240923_221321_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3316511/cfffc5b9-205b-1f9a-f412-a2a37a9d7485.png)

①xmlファイルにSpinnerを追加する。
```
<Spinner
    android:id="@+id/sp_custom"
    android:layout_width="200dp"
    android:layout_height="50dp"
    android:background="@drawable/bg_spinner"
    android:foreground="@drawable/fg_spinner"/>
```
②背景画像を実装する。(ボタン枠)
```drawable/bg_spinner.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:paddingMode="stack"
    android:paddingStart="24dp"★左側に角丸にした分だけpadding追加(DropDownの座標に影響する)
    android:paddingEnd="48dp"★右側に▼のサイズのpaddingを設定
    android:paddingLeft="0dp"
    android:paddingRight="0dp">
    <item>
        <shape xmlns:android="http://schemas.android.com/apk/res/android"
            android:shape="rectangle">
            <!-- 背景色 -->
            <solid android:color="#ffffffff" />
            <!-- 角丸 -->
            <corners android:radius="24dp" />
        </shape>
    </item>
</layer-list>
```
③重ねる前景画像を実装する。(▼マークとタップエフェクト)
```drawable/fg_spinner.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
    android:paddingMode="stack"
    android:paddingStart="24dp"
    android:paddingEnd="48dp"
    android:paddingLeft="0dp"
    android:paddingRight="0dp">
    <item★タップエフェクト
        android:drawable="@drawable/control_background_40dp_material"
        android:gravity="end|fill_vertical"
        android:width="48dp" />
    <item★「▼」のマーク
        android:drawable="@drawable/ic_spinner_caret"
        android:gravity="end|center_vertical"
        android:width="24dp"
        android:height="24dp"
        android:end="12dp" />
</layer-list>
```

```drawable/control_background_40dp_material.xml
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="@color/control_highlight_material"
    android:radius="20dp" />
```


```drawable/control_highlight_material.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_checked="true"
        android:state_enabled="true"
        android:alpha="@dimen/highlight_alpha_material_colored"
        android:color="?attr/colorControlActivated" />
    <item android:color="?attr/colorControlHighlight" />
</selector>
```

```drawable/ic_spinner_caret.xml
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24.0"
    android:viewportHeight="24.0"
    android:tint="?attr/colorControlNormal">
    <path
        android:pathData="M7,10l5,5,5-5z"
        android:fillColor="@color/white"/>
</vector>
```

# 2. Spinnerの内部処理を実装する。
①各DropDownアイテムのUIの情報を構成するDropDownItemクラスを実装する
```
class DropDownItem {
    private var mLabel: String //Spinnerで表示する文字
    private var mTextColor = 0 //Spinnerの文字色
    private var mBackgroundColor = 0 //Spinnerの背景色

    constructor(label: String, textColor: Int, backgroundColor: Int) {
        mLabel = label
        mTextColor = textColor
        mBackgroundColor = backgroundColor
    }

    fun getLabel(): String {
        return mLabel
    }

    fun getTextColor(): Int {
        return mTextColor
    }

    fun getBackgroundColor(): Int {
        return mBackgroundColor
    }
}
```
②DropDownを表示した際に、各アイテムごとにUIを変えるCustomAdapterを実装する。
![Screenshot_20240923_221321_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3316511/458350df-51e4-c062-5f19-797fbd39f7b4.png)

```
class CustomAdapter : ArrayAdapter<DropDownItem> {
    //spinner_dropdown_itemは好きなレイアウトに変更して可。
    constructor(context: Context, list: ArrayList<DropDownItem>) :
            super(context, R.layout.spinner_dropdown_item, list)

    //SpinnerのUI
    override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
        val textView = super.getView(position, convertView, parent) as TextView
        textView.text = getItem(position)?.getLabel()
        return textView
    }

    //DropDownアイテムのUI
    override fun getDropDownView(
        position: Int,
        convertView: View?,
        parent: ViewGroup
    ): View {
        val textView = super.getDropDownView(position, convertView, parent) as TextView
        //Label設定
        textView.text = getItem(position)?.getLabel()
        //テキスト色
        getItem(position)?.getTextColor()?.let { textView.setTextColor(it) }
        //背景色
        getItem(position)?.getBackgroundColor()?.let { textView.setBackgroundColor(it) }
        return textView
    }
}
```
③Spinnerのデータ設定・選択されたアイテムのUIをSpinnerに反映させる
(以下の例だと、黄色を選択した際に文字色と▼マークを黒色にして、背景色を黄色にする。)
![Screenshot_20240923_230333.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3316511/c7b9b9b5-b7a5-0dfa-dd40-85d74bb7baa2.png)
```
//データ設定
val customItems = ArrayList<DropDownItem>()
customItems.add(DropDownItem("赤色", Color.WHITE, Color.RED))
customItems.add(DropDownItem("青色", Color.WHITE, Color.BLUE))
customItems.add(DropDownItem("黄色", Color.BLACK, Color.YELLOW))
val arrayAdapter = CustomAdapter(requireContext(), customItems)
customSpinner.adapter = arrayAdapter

//選択されたアイテムごとにSpinnerのUIを変更する
customSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
    override fun onItemSelected(parent: AdapterView<*>, view: View, pos: Int, id: Long) {
        (parent.getChildAt(0) as TextView).setTextColor(customItems[pos].getTextColor())
        customSpinner.foreground.colorFilter =
            BlendModeColorFilter(customItems[pos].getTextColor(), BlendMode.SRC_ATOP)
        customSpinner.background.colorFilter =
            BlendModeColorFilter(customItems[pos].getBackgroundColor(), BlendMode.SRC_ATOP)
    }

    override fun onNothingSelected(parent: AdapterView<*>?) {

    }
}
```

# 最後に
ぐぐったらこういった特殊な実装はできないみたいな古いQAが見つかったので、作ってみました。
こういうやり方すればもっと楽に実装できますよとかあれば、コメントいただけると非常にためになります。